mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 05:31:44 +08:00
feat: 添加 Nginx Stream 支持 (#1210)
* Initial plan * feat: 添加 Nginx Stream 支持 (Server 和 Upstream) Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> * feat: 优化 * fix: 前端优化 * feat: 优化 * feat: 优化 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com> Co-authored-by: 耗子 <haozi@loli.email>
This commit is contained in:
@@ -36,6 +36,15 @@ func (s *App) Route(r chi.Router) {
|
||||
r.Post("/config", s.SaveConfig)
|
||||
r.Get("/error_log", s.ErrorLog)
|
||||
r.Post("/clear_error_log", s.ClearErrorLog)
|
||||
|
||||
r.Get("/stream/servers", s.ListStreamServers)
|
||||
r.Post("/stream/servers", s.CreateStreamServer)
|
||||
r.Put("/stream/servers/{name}", s.UpdateStreamServer)
|
||||
r.Delete("/stream/servers/{name}", s.DeleteStreamServer)
|
||||
r.Get("/stream/upstreams", s.ListStreamUpstreams)
|
||||
r.Post("/stream/upstreams", s.CreateStreamUpstream)
|
||||
r.Put("/stream/upstreams/{name}", s.UpdateStreamUpstream)
|
||||
r.Delete("/stream/upstreams/{name}", s.DeleteStreamUpstream)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
package nginx
|
||||
|
||||
import "time"
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
type StreamServer struct {
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"` // 配置名称,用于文件命名
|
||||
Listen string `form:"listen" json:"listen" validate:"required"` // 监听地址,如: "12345", "0.0.0.0:12345", "[::]:12345"
|
||||
UDP bool `form:"udp" json:"udp"` // 是否 UDP 协议
|
||||
ProxyPass string `form:"proxy_pass" json:"proxy_pass" validate:"required"` // 代理地址,如: "127.0.0.1:3306", "upstream_name"
|
||||
ProxyProtocol bool `form:"proxy_protocol" json:"proxy_protocol"` // 是否启用 PROXY 协议
|
||||
ProxyTimeout time.Duration `form:"proxy_timeout" json:"proxy_timeout"` // 代理超时时间
|
||||
ProxyConnectTimeout time.Duration `form:"proxy_connect_timeout" json:"proxy_connect_timeout"` // 代理连接超时时间
|
||||
SSL bool `form:"ssl" json:"ssl"` // 是否启用 SSL
|
||||
SSLCertificate string `form:"ssl_certificate" json:"ssl_certificate"` // SSL 证书路径
|
||||
SSLCertificateKey string `form:"ssl_certificate_key" json:"ssl_certificate_key"` // SSL 私钥路径
|
||||
}
|
||||
|
||||
type StreamUpstream struct {
|
||||
Name string `form:"name" json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"` // 上游名称
|
||||
Servers map[string]string `form:"servers" json:"servers" validate:"required"` // 上游服务器及配置,如: map["127.0.0.1:3306"] = "weight=5"
|
||||
Algo string `form:"algo" json:"algo"` // 负载均衡算法,如: "least_conn", "hash $remote_addr"
|
||||
Resolver []string `form:"resolver" json:"resolver"` // DNS 解析器,如: ["8.8.8.8", "ipv6=off"]
|
||||
ResolverTimeout time.Duration `form:"resolver_timeout" json:"resolver_timeout"` // DNS 解析超时时间
|
||||
}
|
||||
|
||||
638
internal/apps/nginx/stream.go
Normal file
638
internal/apps/nginx/stream.go
Normal file
@@ -0,0 +1,638 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
"github.com/acepanel/panel/internal/service"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
webserverNginx "github.com/acepanel/panel/pkg/webserver/nginx"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
// ListStreamServers 获取 Stream Server 列表
|
||||
func (s *App) ListStreamServers(w http.ResponseWriter, r *http.Request) {
|
||||
servers, err := s.parseStreamServers()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to list stream servers: %v", err))
|
||||
return
|
||||
}
|
||||
service.Success(w, servers)
|
||||
}
|
||||
|
||||
// CreateStreamServer 创建 Stream Server
|
||||
func (s *App) CreateStreamServer(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[StreamServer](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
configPath := filepath.Join(s.streamDir(), fmt.Sprintf("%s.conf", req.Name))
|
||||
if _, statErr := os.Stat(configPath); statErr == nil {
|
||||
service.Error(w, http.StatusConflict, s.t.Get("stream server config already exists: %s", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.saveStreamServerConfig(configPath, req); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to write stream server config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("nginx"); err != nil {
|
||||
_ = os.Remove(configPath)
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload nginx: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// UpdateStreamServer 更新 Stream Server
|
||||
func (s *App) UpdateStreamServer(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
if name == "" {
|
||||
service.Error(w, http.StatusBadRequest, s.t.Get("name is required"))
|
||||
return
|
||||
}
|
||||
|
||||
req, err := service.Bind[StreamServer](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
configPath := filepath.Join(s.streamDir(), fmt.Sprintf("%s.conf", name))
|
||||
if _, statErr := os.Stat(configPath); os.IsNotExist(statErr) {
|
||||
service.Error(w, http.StatusNotFound, s.t.Get("stream server not found: %s", name))
|
||||
return
|
||||
}
|
||||
|
||||
newConfigPath := configPath
|
||||
if req.Name != name {
|
||||
newConfigPath = filepath.Join(s.streamDir(), fmt.Sprintf("%s.conf", req.Name))
|
||||
if _, statErr := os.Stat(newConfigPath); statErr == nil {
|
||||
service.Error(w, http.StatusConflict, s.t.Get("stream server config already exists: %s", req.Name))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = s.saveStreamServerConfig(newConfigPath, req); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to write stream server config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if newConfigPath != configPath {
|
||||
_ = os.Remove(configPath)
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("nginx"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload nginx: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// DeleteStreamServer 删除 Stream Server
|
||||
func (s *App) DeleteStreamServer(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
if name == "" {
|
||||
service.Error(w, http.StatusBadRequest, s.t.Get("name is required"))
|
||||
return
|
||||
}
|
||||
|
||||
configPath := filepath.Join(s.streamDir(), fmt.Sprintf("%s.conf", name))
|
||||
if _, statErr := os.Stat(configPath); os.IsNotExist(statErr) {
|
||||
service.Error(w, http.StatusNotFound, s.t.Get("stream server not found: %s", name))
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Remove(configPath); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to delete stream server config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Reload("nginx"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload nginx: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ListStreamUpstreams 获取 Stream Upstream 列表
|
||||
func (s *App) ListStreamUpstreams(w http.ResponseWriter, r *http.Request) {
|
||||
upstreams, err := s.parseStreamUpstreams()
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to list stream upstreams: %v", err))
|
||||
return
|
||||
}
|
||||
service.Success(w, upstreams)
|
||||
}
|
||||
|
||||
// CreateStreamUpstream 创建 Stream Upstream
|
||||
func (s *App) CreateStreamUpstream(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[StreamUpstream](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
configPath := filepath.Join(s.streamDir(), fmt.Sprintf("upstream_%s.conf", req.Name))
|
||||
if _, statErr := os.Stat(configPath); statErr == nil {
|
||||
service.Error(w, http.StatusConflict, s.t.Get("stream upstream config already exists: %s", req.Name))
|
||||
return
|
||||
}
|
||||
|
||||
if err = s.saveStreamUpstreamConfig(configPath, req); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to write stream upstream config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("nginx"); err != nil {
|
||||
_ = os.Remove(configPath)
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload nginx: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// UpdateStreamUpstream 更新 Stream Upstream
|
||||
func (s *App) UpdateStreamUpstream(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
if name == "" {
|
||||
service.Error(w, http.StatusBadRequest, s.t.Get("name is required"))
|
||||
return
|
||||
}
|
||||
|
||||
req, err := service.Bind[StreamUpstream](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
configPath := filepath.Join(s.streamDir(), fmt.Sprintf("upstream_%s.conf", name))
|
||||
if _, statErr := os.Stat(configPath); os.IsNotExist(statErr) {
|
||||
service.Error(w, http.StatusNotFound, s.t.Get("stream upstream not found: %s", name))
|
||||
return
|
||||
}
|
||||
|
||||
newConfigPath := configPath
|
||||
if req.Name != name {
|
||||
newConfigPath = filepath.Join(s.streamDir(), fmt.Sprintf("upstream_%s.conf", req.Name))
|
||||
if _, statErr := os.Stat(newConfigPath); statErr == nil {
|
||||
service.Error(w, http.StatusConflict, s.t.Get("stream upstream config already exists: %s", req.Name))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = s.saveStreamUpstreamConfig(newConfigPath, req); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to write stream upstream config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if newConfigPath != configPath {
|
||||
_ = os.Remove(configPath)
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("nginx"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload nginx: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// DeleteStreamUpstream 删除 Stream Upstream
|
||||
func (s *App) DeleteStreamUpstream(w http.ResponseWriter, r *http.Request) {
|
||||
name := chi.URLParam(r, "name")
|
||||
if name == "" {
|
||||
service.Error(w, http.StatusBadRequest, s.t.Get("name is required"))
|
||||
return
|
||||
}
|
||||
|
||||
configPath := filepath.Join(s.streamDir(), fmt.Sprintf("upstream_%s.conf", name))
|
||||
if _, statErr := os.Stat(configPath); os.IsNotExist(statErr) {
|
||||
service.Error(w, http.StatusNotFound, s.t.Get("stream upstream not found: %s", name))
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Remove(configPath); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to delete stream upstream config: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Reload("nginx"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to reload nginx: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// parseStreamServers 解析所有 Stream Server 配置
|
||||
func (s *App) parseStreamServers() ([]StreamServer, error) {
|
||||
entries, err := os.ReadDir(s.streamDir())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
servers := make([]StreamServer, 0)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
fileName := entry.Name()
|
||||
// 跳过 upstream 配置文件
|
||||
if strings.HasPrefix(fileName, "upstream_") {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(fileName, ".conf") {
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.TrimSuffix(fileName, ".conf")
|
||||
configPath := filepath.Join(s.streamDir(), fileName)
|
||||
server, err := s.parseStreamServerFile(configPath, name)
|
||||
if err != nil {
|
||||
continue // 跳过解析失败的文件
|
||||
}
|
||||
if server != nil {
|
||||
servers = append(servers, *server)
|
||||
}
|
||||
}
|
||||
|
||||
// 按名称排序
|
||||
sort.Slice(servers, func(i, j int) bool {
|
||||
return servers[i].Name < servers[j].Name
|
||||
})
|
||||
|
||||
return servers, nil
|
||||
}
|
||||
|
||||
// parseStreamServerFile 解析单个 Stream Server 配置文件
|
||||
func (s *App) parseStreamServerFile(filePath string, name string) (*StreamServer, error) {
|
||||
p, err := webserverNginx.NewParserFromFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
server := &StreamServer{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
// 解析 listen 指令
|
||||
listenDirs, err := p.Find("server.listen")
|
||||
if err == nil && len(listenDirs) > 0 {
|
||||
params := listenDirs[0].GetParameters()
|
||||
if len(params) > 0 {
|
||||
server.Listen = params[0].Value
|
||||
for i := 1; i < len(params); i++ {
|
||||
switch params[i].Value {
|
||||
case "udp":
|
||||
server.UDP = true
|
||||
case "ssl":
|
||||
server.SSL = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 解析 proxy_pass 指令
|
||||
proxyPassDir, err := p.FindOne("server.proxy_pass")
|
||||
if err == nil {
|
||||
params := proxyPassDir.GetParameters()
|
||||
if len(params) > 0 {
|
||||
server.ProxyPass = params[0].Value
|
||||
}
|
||||
}
|
||||
// 解析 proxy_protocol 指令
|
||||
proxyProtocolDir, err := p.FindOne("server.proxy_protocol")
|
||||
if err == nil {
|
||||
params := proxyProtocolDir.GetParameters()
|
||||
if len(params) > 0 && params[0].Value == "on" {
|
||||
server.ProxyProtocol = true
|
||||
}
|
||||
}
|
||||
// 解析 proxy_timeout 指令
|
||||
proxyTimeoutDir, err := p.FindOne("server.proxy_timeout")
|
||||
if err == nil {
|
||||
params := proxyTimeoutDir.GetParameters()
|
||||
if len(params) > 0 {
|
||||
server.ProxyTimeout = parseNginxDuration(params[0].Value)
|
||||
}
|
||||
}
|
||||
// 解析 proxy_connect_timeout 指令
|
||||
proxyConnectTimeoutDir, err := p.FindOne("server.proxy_connect_timeout")
|
||||
if err == nil {
|
||||
params := proxyConnectTimeoutDir.GetParameters()
|
||||
if len(params) > 0 {
|
||||
server.ProxyConnectTimeout = parseNginxDuration(params[0].Value)
|
||||
}
|
||||
}
|
||||
// 解析 ssl_certificate 指令
|
||||
sslCertDir, err := p.FindOne("server.ssl_certificate")
|
||||
if err == nil {
|
||||
params := sslCertDir.GetParameters()
|
||||
if len(params) > 0 {
|
||||
server.SSLCertificate = params[0].Value
|
||||
}
|
||||
}
|
||||
// 解析 ssl_certificate_key 指令
|
||||
sslKeyDir, err := p.FindOne("server.ssl_certificate_key")
|
||||
if err == nil {
|
||||
params := sslKeyDir.GetParameters()
|
||||
if len(params) > 0 {
|
||||
server.SSLCertificateKey = params[0].Value
|
||||
}
|
||||
}
|
||||
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// parseStreamUpstreams 解析所有 Stream Upstream 配置
|
||||
func (s *App) parseStreamUpstreams() ([]StreamUpstream, error) {
|
||||
entries, err := os.ReadDir(s.streamDir())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
upstreams := make([]StreamUpstream, 0)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
fileName := entry.Name()
|
||||
// 只处理 upstream 配置文件
|
||||
if !strings.HasPrefix(fileName, "upstream_") {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(fileName, ".conf") {
|
||||
continue
|
||||
}
|
||||
|
||||
name := strings.TrimPrefix(fileName, "upstream_")
|
||||
name = strings.TrimSuffix(name, ".conf")
|
||||
configPath := filepath.Join(s.streamDir(), fileName)
|
||||
upstream, err := s.parseStreamUpstreamFile(configPath, name)
|
||||
if err != nil {
|
||||
continue // 跳过解析失败的文件
|
||||
}
|
||||
if upstream != nil {
|
||||
upstreams = append(upstreams, *upstream)
|
||||
}
|
||||
}
|
||||
|
||||
// 按名称排序
|
||||
sort.Slice(upstreams, func(i, j int) bool {
|
||||
return upstreams[i].Name < upstreams[j].Name
|
||||
})
|
||||
|
||||
return upstreams, nil
|
||||
}
|
||||
|
||||
// parseStreamUpstreamFile 解析单个 Stream Upstream 配置文件
|
||||
func (s *App) parseStreamUpstreamFile(filePath string, expectedName string) (*StreamUpstream, error) {
|
||||
p, err := webserverNginx.NewParserFromFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := p.Config()
|
||||
if cfg == nil || cfg.Block == nil {
|
||||
return nil, fmt.Errorf("invalid config")
|
||||
}
|
||||
|
||||
// 查找 upstream 块
|
||||
upstreamDirectives := cfg.Block.FindDirectives("upstream")
|
||||
if len(upstreamDirectives) == 0 {
|
||||
return nil, fmt.Errorf("no upstream block found")
|
||||
}
|
||||
|
||||
upstreamDir := upstreamDirectives[0]
|
||||
params := upstreamDir.GetParameters()
|
||||
if len(params) == 0 {
|
||||
return nil, fmt.Errorf("upstream name not found")
|
||||
}
|
||||
|
||||
name := params[0].Value
|
||||
if expectedName != "" && name != expectedName {
|
||||
return nil, fmt.Errorf("upstream name mismatch")
|
||||
}
|
||||
|
||||
upstream := &StreamUpstream{
|
||||
Name: name,
|
||||
Servers: make(map[string]string),
|
||||
Resolver: []string{},
|
||||
}
|
||||
|
||||
upstreamBlock := upstreamDir.GetBlock()
|
||||
if upstreamBlock == nil {
|
||||
return nil, fmt.Errorf("upstream block is empty")
|
||||
}
|
||||
|
||||
// 解析 upstream 块中的指令
|
||||
for _, dir := range upstreamBlock.GetDirectives() {
|
||||
switch dir.GetName() {
|
||||
case "server":
|
||||
dirParams := dir.GetParameters()
|
||||
if len(dirParams) > 0 {
|
||||
addr := dirParams[0].Value
|
||||
var options []string
|
||||
for i := 1; i < len(dirParams); i++ {
|
||||
options = append(options, dirParams[i].Value)
|
||||
}
|
||||
upstream.Servers[addr] = strings.Join(options, " ")
|
||||
}
|
||||
case "least_conn", "ip_hash", "random":
|
||||
upstream.Algo = dir.GetName()
|
||||
case "hash":
|
||||
dirParams := dir.GetParameters()
|
||||
if len(dirParams) > 0 {
|
||||
upstream.Algo = "hash " + dirParams[0].Value
|
||||
// 检查是否有 consistent 参数
|
||||
if len(dirParams) > 1 && dirParams[1].Value == "consistent" {
|
||||
upstream.Algo += " consistent"
|
||||
}
|
||||
}
|
||||
case "least_time":
|
||||
dirParams := dir.GetParameters()
|
||||
if len(dirParams) > 0 {
|
||||
upstream.Algo = "least_time " + dirParams[0].Value
|
||||
}
|
||||
case "resolver":
|
||||
dirParams := dir.GetParameters()
|
||||
for _, param := range dirParams {
|
||||
upstream.Resolver = append(upstream.Resolver, param.Value)
|
||||
}
|
||||
case "resolver_timeout":
|
||||
dirParams := dir.GetParameters()
|
||||
if len(dirParams) > 0 {
|
||||
upstream.ResolverTimeout = parseNginxDuration(dirParams[0].Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return upstream, nil
|
||||
}
|
||||
|
||||
// saveStreamServerConfig 生成并保存 Stream Server 配置
|
||||
func (s *App) saveStreamServerConfig(filePath string, server *StreamServer) error {
|
||||
p, err := webserverNginx.NewParserFromString("server {}")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.SetConfigPath(filePath)
|
||||
|
||||
// listen 指令
|
||||
listenParams := []string{server.Listen}
|
||||
if server.UDP {
|
||||
listenParams = append(listenParams, "udp")
|
||||
}
|
||||
if server.SSL {
|
||||
listenParams = append(listenParams, "ssl")
|
||||
}
|
||||
if err = p.SetOne("server.listen", listenParams); err != nil {
|
||||
return err
|
||||
}
|
||||
// proxy_pass 指令
|
||||
if err = p.SetOne("server.proxy_pass", []string{server.ProxyPass}); err != nil {
|
||||
return err
|
||||
}
|
||||
// proxy_protocol 指令
|
||||
if server.ProxyProtocol {
|
||||
if err = p.SetOne("server.proxy_protocol", []string{"on"}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// proxy_timeout 指令
|
||||
if server.ProxyTimeout > 0 {
|
||||
if err = p.SetOne("server.proxy_timeout", []string{formatNginxDuration(server.ProxyTimeout)}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// proxy_connect_timeout 指令
|
||||
if server.ProxyConnectTimeout > 0 {
|
||||
if err = p.SetOne("server.proxy_connect_timeout", []string{formatNginxDuration(server.ProxyConnectTimeout)}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// SSL 配置
|
||||
if server.SSL {
|
||||
if server.SSLCertificate != "" {
|
||||
if err = p.SetOne("server.ssl_certificate", []string{server.SSLCertificate}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if server.SSLCertificateKey != "" {
|
||||
if err = p.SetOne("server.ssl_certificate_key", []string{server.SSLCertificateKey}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return os.WriteFile(filePath, []byte(p.Dump()), 0600)
|
||||
}
|
||||
|
||||
// saveStreamUpstreamConfig 生成并保存 Stream Upstream 配置
|
||||
func (s *App) saveStreamUpstreamConfig(filePath string, upstream *StreamUpstream) error {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("upstream %s {\n", upstream.Name))
|
||||
|
||||
// 负载均衡算法
|
||||
if upstream.Algo != "" {
|
||||
sb.WriteString(fmt.Sprintf(" %s;\n", upstream.Algo))
|
||||
}
|
||||
|
||||
// resolver 配置
|
||||
if len(upstream.Resolver) > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" resolver %s;\n", strings.Join(upstream.Resolver, " ")))
|
||||
if upstream.ResolverTimeout > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" resolver_timeout %s;\n", formatNginxDuration(upstream.ResolverTimeout)))
|
||||
}
|
||||
}
|
||||
|
||||
// 服务器列表
|
||||
var addrs []string
|
||||
for addr := range upstream.Servers {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
sort.Strings(addrs)
|
||||
|
||||
for _, addr := range addrs {
|
||||
options := upstream.Servers[addr]
|
||||
if options != "" {
|
||||
sb.WriteString(fmt.Sprintf(" server %s %s;\n", addr, options))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf(" server %s;\n", addr))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("}\n")
|
||||
|
||||
return os.WriteFile(filePath, []byte(sb.String()), 0600)
|
||||
}
|
||||
|
||||
// parseNginxDuration 解析 Nginx 时间格式(如 10s, 1m, 1h)
|
||||
func parseNginxDuration(value string) time.Duration {
|
||||
if value == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 尝试解析带单位的时间
|
||||
value = strings.TrimSpace(value)
|
||||
if len(value) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
unit := value[len(value)-1]
|
||||
numStr := value[:len(value)-1]
|
||||
|
||||
var num int
|
||||
_, _ = fmt.Sscanf(numStr, "%d", &num)
|
||||
|
||||
switch unit {
|
||||
case 's':
|
||||
return time.Duration(num) * time.Second
|
||||
case 'm':
|
||||
return time.Duration(num) * time.Minute
|
||||
case 'h':
|
||||
return time.Duration(num) * time.Hour
|
||||
case 'd':
|
||||
return time.Duration(num) * 24 * time.Hour
|
||||
default:
|
||||
// 没有单位,尝试直接解析为秒
|
||||
_, _ = fmt.Sscanf(value, "%d", &num)
|
||||
return time.Duration(num) * time.Second
|
||||
}
|
||||
}
|
||||
|
||||
// formatNginxDuration 格式化时间为 Nginx 格式
|
||||
func formatNginxDuration(d time.Duration) string {
|
||||
if d == 0 {
|
||||
return "0s"
|
||||
}
|
||||
|
||||
seconds := int(d.Seconds())
|
||||
if seconds%3600 == 0 {
|
||||
return fmt.Sprintf("%dh", seconds/3600)
|
||||
}
|
||||
if seconds%60 == 0 {
|
||||
return fmt.Sprintf("%dm", seconds/60)
|
||||
}
|
||||
return fmt.Sprintf("%ds", seconds)
|
||||
}
|
||||
|
||||
// streamDir 返回 stream 配置目录
|
||||
func (s *App) streamDir() string {
|
||||
return filepath.Join(app.Root, "server/nginx/conf/stream")
|
||||
}
|
||||
@@ -47,6 +47,17 @@ func NewParserFromFile(filePath string) (*Parser, error) {
|
||||
return &Parser{cfg: cfg, cfgPath: filePath}, nil
|
||||
}
|
||||
|
||||
// NewParserFromString 从字符串创建解析器
|
||||
func NewParserFromString(content string) (*Parser, error) {
|
||||
p := parser.NewStringParser(content, parser.WithSkipIncludeParsingErr(), parser.WithSkipValidDirectivesErr())
|
||||
cfg, err := p.Parse()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse config: %w", err)
|
||||
}
|
||||
|
||||
return &Parser{cfg: cfg, cfgPath: ""}, nil
|
||||
}
|
||||
|
||||
func (p *Parser) Config() *config.Config {
|
||||
return p.cfg
|
||||
}
|
||||
@@ -184,6 +195,24 @@ func (p *Parser) Set(key string, directives []*config.Directive, after ...string
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetOne 设置单个指令,如: SetOne("server.listen", []string{"80"})
|
||||
func (p *Parser) SetOne(key string, params []string) error {
|
||||
parts := strings.Split(key, ".")
|
||||
if len(parts) < 2 {
|
||||
return fmt.Errorf("key must have at least 2 parts: %s", key)
|
||||
}
|
||||
|
||||
directiveName := parts[len(parts)-1]
|
||||
blockKey := strings.Join(parts[:len(parts)-1], ".")
|
||||
|
||||
return p.Set(blockKey, []*config.Directive{
|
||||
{
|
||||
Name: directiveName,
|
||||
Parameters: p.slices2Parameters(params),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Dump 将指令结构导出为配置内容
|
||||
func (p *Parser) Dump() string {
|
||||
return dumper.DumpConfig(p.cfg, dumper.IndentedStyle)
|
||||
|
||||
@@ -10,5 +10,19 @@ export default {
|
||||
// 获取错误日志
|
||||
errorLog: (): any => http.Get('/apps/nginx/error_log'),
|
||||
// 清空错误日志
|
||||
clearErrorLog: (): any => http.Post('/apps/nginx/clear_error_log')
|
||||
clearErrorLog: (): any => http.Post('/apps/nginx/clear_error_log'),
|
||||
|
||||
// Stream Server 接口
|
||||
stream: {
|
||||
listServers: (): any => http.Get('/apps/nginx/stream/servers'),
|
||||
createServer: (data: any): any => http.Post('/apps/nginx/stream/servers', data),
|
||||
updateServer: (name: string, data: any): any =>
|
||||
http.Put(`/apps/nginx/stream/servers/${name}`, data),
|
||||
deleteServer: (name: string): any => http.Delete(`/apps/nginx/stream/servers/${name}`),
|
||||
listUpstreams: (): any => http.Get('/apps/nginx/stream/upstreams'),
|
||||
createUpstream: (data: any): any => http.Post('/apps/nginx/stream/upstreams', data),
|
||||
updateUpstream: (name: string, data: any): any =>
|
||||
http.Put(`/apps/nginx/stream/upstreams/${name}`, data),
|
||||
deleteUpstream: (name: string): any => http.Delete(`/apps/nginx/stream/upstreams/${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,5 +10,19 @@ export default {
|
||||
// 获取错误日志
|
||||
errorLog: (): any => http.Get('/apps/openresty/error_log'),
|
||||
// 清空错误日志
|
||||
clearErrorLog: (): any => http.Post('/apps/openresty/clear_error_log')
|
||||
clearErrorLog: (): any => http.Post('/apps/openresty/clear_error_log'),
|
||||
|
||||
// Stream Server 接口
|
||||
stream: {
|
||||
listServers: (): any => http.Get('/apps/openresty/stream/servers'),
|
||||
createServer: (data: any): any => http.Post('/apps/openresty/stream/servers', data),
|
||||
updateServer: (name: string, data: any): any =>
|
||||
http.Put(`/apps/openresty/stream/servers/${name}`, data),
|
||||
deleteServer: (name: string): any => http.Delete(`/apps/openresty/stream/servers/${name}`),
|
||||
listUpstreams: (): any => http.Get('/apps/openresty/stream/upstreams'),
|
||||
createUpstream: (data: any): any => http.Post('/apps/openresty/stream/upstreams', data),
|
||||
updateUpstream: (name: string, data: any): any =>
|
||||
http.Put(`/apps/openresty/stream/upstreams/${name}`, data),
|
||||
deleteUpstream: (name: string): any => http.Delete(`/apps/openresty/stream/upstreams/${name}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,135 +3,10 @@ defineOptions({
|
||||
name: 'apps-mariadb-index'
|
||||
})
|
||||
|
||||
import copy2clipboard from '@vavt/copy2clipboard'
|
||||
import { NButton, NDataTable, NInput } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import mariadb from '@/api/apps/mariadb'
|
||||
import ServiceStatus from '@/components/common/ServiceStatus.vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const currentTab = ref('status')
|
||||
|
||||
const { data: rootPassword } = useRequest(mariadb.rootPassword, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: config } = useRequest(mariadb.config, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: slowLog } = useRequest(mariadb.slowLog, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: load } = useRequest(mariadb.load, {
|
||||
initialData: []
|
||||
})
|
||||
|
||||
const loadColumns: any = [
|
||||
{
|
||||
title: $gettext('Property'),
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Current Value'),
|
||||
key: 'value',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
}
|
||||
]
|
||||
|
||||
const handleSaveConfig = () => {
|
||||
useRequest(mariadb.saveConfig(config.value)).onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearLog = () => {
|
||||
useRequest(mariadb.clearLog()).onSuccess(() => {
|
||||
window.$message.success($gettext('Cleared successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearSlowLog = () => {
|
||||
useRequest(mariadb.clearSlowLog()).onSuccess(() => {
|
||||
window.$message.success($gettext('Cleared successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleSetRootPassword = async () => {
|
||||
await mariadb.setRootPassword(rootPassword.value)
|
||||
window.$message.success($gettext('Modified successfully'))
|
||||
}
|
||||
|
||||
const handleCopyRootPassword = () => {
|
||||
copy2clipboard(rootPassword.value).then(() => {
|
||||
window.$message.success($gettext('Copied successfully'))
|
||||
})
|
||||
}
|
||||
import MysqlIndex from '@/views/apps/mysql/MysqlIndex.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="status" :tab="$gettext('Running Status')">
|
||||
<n-flex vertical>
|
||||
<service-status service="mysqld" />
|
||||
<n-card :title="$gettext('Root Password')">
|
||||
<n-flex>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="rootPassword" type="password" show-password-on="click" />
|
||||
<n-button type="primary" ghost @click="handleCopyRootPassword">
|
||||
{{ $gettext('Copy') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<n-button type="primary" @click="handleSetRootPassword">
|
||||
{{ $gettext('Save Changes') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config" :tab="$gettext('Modify Configuration')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="warning">
|
||||
{{
|
||||
$gettext(
|
||||
'This modifies the MariaDB main configuration file. If you do not understand the meaning of each parameter, please do not modify it randomly!'
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<common-editor v-model:value="config" height="60vh" />
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleSaveConfig">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" :tab="$gettext('Load Status')">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="400"
|
||||
:loading="false"
|
||||
:columns="loadColumns"
|
||||
:data="load"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="run-log" :tab="$gettext('Runtime Logs')">
|
||||
<n-button type="primary" @click="handleClearLog">
|
||||
{{ $gettext('Clear Log') }}
|
||||
</n-button>
|
||||
<realtime-log service="mysqld" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="slow-log" :tab="$gettext('Slow Query Log')">
|
||||
<n-button type="primary" @click="handleClearSlowLog">
|
||||
{{ $gettext('Clear Slow Log') }}
|
||||
</n-button>
|
||||
<realtime-log :path="slowLog" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
<mysql-index :api="mariadb" name="MariaDB" />
|
||||
</template>
|
||||
|
||||
@@ -3,135 +3,10 @@ defineOptions({
|
||||
name: 'apps-mysql-index'
|
||||
})
|
||||
|
||||
import copy2clipboard from '@vavt/copy2clipboard'
|
||||
import { NButton, NDataTable, NInput } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import mysql from '@/api/apps/mysql'
|
||||
import ServiceStatus from '@/components/common/ServiceStatus.vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const currentTab = ref('status')
|
||||
|
||||
const { data: rootPassword } = useRequest(mysql.rootPassword, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: config } = useRequest(mysql.config, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: slowLog } = useRequest(mysql.slowLog, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: load } = useRequest(mysql.load, {
|
||||
initialData: []
|
||||
})
|
||||
|
||||
const loadColumns: any = [
|
||||
{
|
||||
title: $gettext('Property'),
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Current Value'),
|
||||
key: 'value',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
}
|
||||
]
|
||||
|
||||
const handleSaveConfig = () => {
|
||||
useRequest(mysql.saveConfig(config.value)).onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearLog = () => {
|
||||
useRequest(mysql.clearLog()).onSuccess(() => {
|
||||
window.$message.success($gettext('Cleared successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearSlowLog = () => {
|
||||
useRequest(mysql.clearSlowLog()).onSuccess(() => {
|
||||
window.$message.success($gettext('Cleared successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleSetRootPassword = async () => {
|
||||
await mysql.setRootPassword(rootPassword.value)
|
||||
window.$message.success($gettext('Modified successfully'))
|
||||
}
|
||||
|
||||
const handleCopyRootPassword = () => {
|
||||
copy2clipboard(rootPassword.value).then(() => {
|
||||
window.$message.success($gettext('Copied successfully'))
|
||||
})
|
||||
}
|
||||
import MysqlIndex from './MysqlIndex.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="status" :tab="$gettext('Running Status')">
|
||||
<n-flex vertical>
|
||||
<service-status service="mysqld" />
|
||||
<n-card :title="$gettext('Root Password')">
|
||||
<n-flex>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="rootPassword" type="password" show-password-on="click" />
|
||||
<n-button type="primary" ghost @click="handleCopyRootPassword">
|
||||
{{ $gettext('Copy') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<n-button type="primary" @click="handleSetRootPassword">
|
||||
{{ $gettext('Save Changes') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config" :tab="$gettext('Modify Configuration')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="warning">
|
||||
{{
|
||||
$gettext(
|
||||
'This modifies the MySQL main configuration file. If you do not understand the meaning of each parameter, please do not modify it randomly!'
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<common-editor v-model:value="config" height="60vh" />
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleSaveConfig">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" :tab="$gettext('Load Status')">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="400"
|
||||
:loading="false"
|
||||
:columns="loadColumns"
|
||||
:data="load"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="run-log" :tab="$gettext('Runtime Logs')">
|
||||
<n-button type="primary" @click="handleClearLog">
|
||||
{{ $gettext('Clear Log') }}
|
||||
</n-button>
|
||||
<realtime-log service="mysqld" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="slow-log" :tab="$gettext('Slow Query Log')">
|
||||
<n-button type="primary" @click="handleClearSlowLog">
|
||||
{{ $gettext('Clear Slow Log') }}
|
||||
</n-button>
|
||||
<realtime-log :path="slowLog" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
<mysql-index :api="mysql" name="MySQL" />
|
||||
</template>
|
||||
|
||||
140
web/src/views/apps/mysql/MysqlIndex.vue
Normal file
140
web/src/views/apps/mysql/MysqlIndex.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<script setup lang="ts">
|
||||
import copy2clipboard from '@vavt/copy2clipboard'
|
||||
import { NButton, NDataTable, NInput } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import mysql from '@/api/apps/mysql'
|
||||
import ServiceStatus from '@/components/common/ServiceStatus.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
api: typeof mysql
|
||||
name: string
|
||||
}>()
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const currentTab = ref('status')
|
||||
|
||||
const { data: rootPassword } = useRequest(props.api.rootPassword, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: config } = useRequest(props.api.config, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: slowLog } = useRequest(props.api.slowLog, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: load } = useRequest(props.api.load, {
|
||||
initialData: []
|
||||
})
|
||||
|
||||
const loadColumns: any = [
|
||||
{
|
||||
title: $gettext('Property'),
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Current Value'),
|
||||
key: 'value',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
}
|
||||
]
|
||||
|
||||
const handleSaveConfig = () => {
|
||||
useRequest(props.api.saveConfig(config.value)).onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearLog = () => {
|
||||
useRequest(props.api.clearLog()).onSuccess(() => {
|
||||
window.$message.success($gettext('Cleared successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearSlowLog = () => {
|
||||
useRequest(props.api.clearSlowLog()).onSuccess(() => {
|
||||
window.$message.success($gettext('Cleared successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleSetRootPassword = () => {
|
||||
useRequest(props.api.setRootPassword(rootPassword.value)).onSuccess(() => {
|
||||
window.$message.success($gettext('Modified successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleCopyRootPassword = () => {
|
||||
copy2clipboard(rootPassword.value).then(() => {
|
||||
window.$message.success($gettext('Copied successfully'))
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="status" :tab="$gettext('Running Status')">
|
||||
<n-flex vertical>
|
||||
<service-status service="mysqld" />
|
||||
<n-card :title="$gettext('Root Password')">
|
||||
<n-flex>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="rootPassword" type="password" show-password-on="click" />
|
||||
<n-button type="primary" ghost @click="handleCopyRootPassword">
|
||||
{{ $gettext('Copy') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<n-button type="primary" @click="handleSetRootPassword">
|
||||
{{ $gettext('Save Changes') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config" :tab="$gettext('Modify Configuration')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="warning">
|
||||
{{
|
||||
$gettext(
|
||||
'This modifies the %{ name } main configuration file. If you do not understand the meaning of each parameter, please do not modify it randomly!',
|
||||
{ name }
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<common-editor v-model:value="config" height="60vh" />
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleSaveConfig">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" :tab="$gettext('Load Status')">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="400"
|
||||
:loading="false"
|
||||
:columns="loadColumns"
|
||||
:data="load"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="run-log" :tab="$gettext('Runtime Logs')">
|
||||
<n-button type="primary" @click="handleClearLog">
|
||||
{{ $gettext('Clear Log') }}
|
||||
</n-button>
|
||||
<realtime-log service="mysqld" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="slow-log" :tab="$gettext('Slow Query Log')">
|
||||
<n-button type="primary" @click="handleClearSlowLog">
|
||||
{{ $gettext('Clear Slow Log') }}
|
||||
</n-button>
|
||||
<realtime-log :path="slowLog" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
</template>
|
||||
@@ -3,100 +3,10 @@ defineOptions({
|
||||
name: 'apps-nginx-index'
|
||||
})
|
||||
|
||||
import { NButton, NDataTable } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import nginx from '@/api/apps/nginx'
|
||||
import ServiceStatus from '@/components/common/ServiceStatus.vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const currentTab = ref('status')
|
||||
|
||||
const { data: config } = useRequest(nginx.config, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: errorLog } = useRequest(nginx.errorLog, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: load } = useRequest(nginx.load, {
|
||||
initialData: []
|
||||
})
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
title: $gettext('Property'),
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Current Value'),
|
||||
key: 'value',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
}
|
||||
]
|
||||
|
||||
const handleSaveConfig = () => {
|
||||
useRequest(nginx.saveConfig(config.value)).onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearErrorLog = () => {
|
||||
useRequest(nginx.clearErrorLog()).onSuccess(() => {
|
||||
window.$message.success($gettext('Cleared successfully'))
|
||||
})
|
||||
}
|
||||
import NginxIndex from './NginxIndex.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="status" :tab="$gettext('Running Status')">
|
||||
<service-status service="nginx" show-reload />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config" :tab="$gettext('Modify Configuration')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="warning">
|
||||
{{
|
||||
$gettext(
|
||||
'This modifies the OpenResty main configuration file. If you do not understand the meaning of each parameter, please do not modify it randomly!'
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<common-editor v-model:value="config" lang="nginx" height="60vh" />
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleSaveConfig">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" :tab="$gettext('Load Status')">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="400"
|
||||
:loading="false"
|
||||
:columns="columns"
|
||||
:data="load"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="run-log" :tab="$gettext('Runtime Logs')">
|
||||
<realtime-log service="nginx" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="error-log" :tab="$gettext('Error Logs')">
|
||||
<n-flex vertical>
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleClearErrorLog">
|
||||
{{ $gettext('Clear Log') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<realtime-log :path="errorLog" />
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
<nginx-index :api="nginx" service="nginx" />
|
||||
</template>
|
||||
|
||||
757
web/src/views/apps/nginx/NginxIndex.vue
Normal file
757
web/src/views/apps/nginx/NginxIndex.vue
Normal file
@@ -0,0 +1,757 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NButton,
|
||||
NDataTable,
|
||||
NDynamicTags,
|
||||
NInputGroup,
|
||||
NInputNumber,
|
||||
NPopconfirm,
|
||||
NSelect
|
||||
} from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import nginx from '@/api/apps/nginx'
|
||||
import ServiceStatus from '@/components/common/ServiceStatus.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
api: typeof nginx
|
||||
service: string
|
||||
}>()
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const currentTab = ref('status')
|
||||
const streamTab = ref('server')
|
||||
|
||||
// 时间单位常量(纳秒)
|
||||
const SECOND = 1000000000
|
||||
const MINUTE = 60 * SECOND
|
||||
const HOUR = 60 * MINUTE
|
||||
|
||||
// 从纳秒解析为 {value, unit} 格式
|
||||
const parseDuration = (ns: number): { value: number; unit: string } => {
|
||||
if (!ns || ns <= 0) return { value: 5, unit: 's' }
|
||||
|
||||
if (ns >= HOUR && ns % HOUR === 0) {
|
||||
return { value: ns / HOUR, unit: 'h' }
|
||||
}
|
||||
if (ns >= MINUTE && ns % MINUTE === 0) {
|
||||
return { value: ns / MINUTE, unit: 'm' }
|
||||
}
|
||||
return { value: Math.floor(ns / SECOND), unit: 's' }
|
||||
}
|
||||
|
||||
// 构建纳秒时间
|
||||
const buildDuration = (value: number, unit: string): number => {
|
||||
switch (unit) {
|
||||
case 'h':
|
||||
return value * HOUR
|
||||
case 'm':
|
||||
return value * MINUTE
|
||||
default:
|
||||
return value * SECOND
|
||||
}
|
||||
}
|
||||
|
||||
// 更新超时时间值
|
||||
const updateResolverTimeoutValue = (value: number) => {
|
||||
const parsed = parseDuration(streamUpstreamModel.value.resolver_timeout)
|
||||
streamUpstreamModel.value.resolver_timeout = buildDuration(value, parsed.unit)
|
||||
}
|
||||
|
||||
// 更新超时时间单位
|
||||
const updateResolverTimeoutUnit = (unit: string) => {
|
||||
const parsed = parseDuration(streamUpstreamModel.value.resolver_timeout)
|
||||
streamUpstreamModel.value.resolver_timeout = buildDuration(parsed.value, unit)
|
||||
}
|
||||
|
||||
const { data: config } = useRequest(props.api.config, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: errorLog } = useRequest(props.api.errorLog, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: load } = useRequest(props.api.load, {
|
||||
initialData: []
|
||||
})
|
||||
|
||||
// Stream Server 数据
|
||||
const {
|
||||
data: streamServers,
|
||||
loading: streamServersLoading,
|
||||
refresh: loadStreamServers
|
||||
} = usePagination(props.api.stream.listServers, {
|
||||
initialData: []
|
||||
})
|
||||
|
||||
// Stream Upstream 数据
|
||||
const {
|
||||
data: streamUpstreams,
|
||||
loading: streamUpstreamsLoading,
|
||||
refresh: loadStreamUpstreams
|
||||
} = usePagination(props.api.stream.listUpstreams, {
|
||||
initialData: []
|
||||
})
|
||||
|
||||
// 创建/编辑 Stream Server 模态框
|
||||
const streamServerModal = ref(false)
|
||||
const streamServerModalTitle = ref('')
|
||||
const streamServerEditName = ref('')
|
||||
const streamServerModel = ref({
|
||||
name: '',
|
||||
listen: '',
|
||||
udp: false,
|
||||
proxy_pass: '',
|
||||
proxy_protocol: false,
|
||||
proxy_timeout: 0,
|
||||
proxy_connect_timeout: 0,
|
||||
ssl: false,
|
||||
ssl_certificate: '',
|
||||
ssl_certificate_key: ''
|
||||
})
|
||||
|
||||
// 创建/编辑 Stream Upstream 模态框
|
||||
const streamUpstreamModal = ref(false)
|
||||
const streamUpstreamModalTitle = ref('')
|
||||
const streamUpstreamEditName = ref('')
|
||||
const streamUpstreamModel = ref({
|
||||
name: '',
|
||||
algo: '',
|
||||
servers: {} as Record<string, string>,
|
||||
resolver: [] as string[],
|
||||
resolver_timeout: 5 * SECOND
|
||||
})
|
||||
|
||||
// Upstream 服务器编辑
|
||||
const upstreamServerAddr = ref('')
|
||||
const upstreamServerOptions = ref('')
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
title: $gettext('Property'),
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Current Value'),
|
||||
key: 'value',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
}
|
||||
]
|
||||
|
||||
// Stream Server 列表列
|
||||
const streamServerColumns: any = [
|
||||
{
|
||||
title: $gettext('Name'),
|
||||
key: 'name',
|
||||
minWidth: 150,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Listen'),
|
||||
key: 'listen',
|
||||
minWidth: 120,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Protocol'),
|
||||
key: 'protocol',
|
||||
minWidth: 80,
|
||||
render(row: any) {
|
||||
return row.udp ? 'UDP' : 'TCP'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Proxy Pass'),
|
||||
key: 'proxy_pass',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: 'SSL',
|
||||
key: 'ssl',
|
||||
minWidth: 60,
|
||||
render(row: any) {
|
||||
return row.ssl ? $gettext('Yes') : $gettext('No')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Actions'),
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
render(row: any) {
|
||||
return [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'info',
|
||||
onClick: () => handleEditStreamServer(row)
|
||||
},
|
||||
{
|
||||
default: () => $gettext('Edit')
|
||||
}
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleDeleteStreamServer(row.name)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return $gettext('Are you sure you want to delete %{ name }?', { name: row.name })
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 15px'
|
||||
},
|
||||
{
|
||||
default: () => $gettext('Delete')
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// Stream Upstream 列表列
|
||||
const streamUpstreamColumns: any = [
|
||||
{
|
||||
title: $gettext('Name'),
|
||||
key: 'name',
|
||||
minWidth: 150,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Algorithm'),
|
||||
key: 'algo',
|
||||
minWidth: 120,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any) {
|
||||
return row.algo || $gettext('Round Robin')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Servers'),
|
||||
key: 'servers',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true },
|
||||
render(row: any) {
|
||||
const servers = row.servers || {}
|
||||
return Object.keys(servers).length + $gettext(' server(s)')
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Actions'),
|
||||
key: 'actions',
|
||||
width: 200,
|
||||
render(row: any) {
|
||||
return [
|
||||
h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'info',
|
||||
onClick: () => handleEditStreamUpstream(row)
|
||||
},
|
||||
{
|
||||
default: () => $gettext('Edit')
|
||||
}
|
||||
),
|
||||
h(
|
||||
NPopconfirm,
|
||||
{
|
||||
onPositiveClick: () => handleDeleteStreamUpstream(row.name)
|
||||
},
|
||||
{
|
||||
default: () => {
|
||||
return $gettext('Are you sure you want to delete %{ name }?', { name: row.name })
|
||||
},
|
||||
trigger: () => {
|
||||
return h(
|
||||
NButton,
|
||||
{
|
||||
size: 'small',
|
||||
type: 'error',
|
||||
style: 'margin-left: 15px'
|
||||
},
|
||||
{
|
||||
default: () => $gettext('Delete')
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
// 监听标签页切换
|
||||
watch(currentTab, (val) => {
|
||||
if (val === 'stream') {
|
||||
loadStreamServers()
|
||||
loadStreamUpstreams()
|
||||
}
|
||||
})
|
||||
|
||||
watch(streamTab, (val) => {
|
||||
if (val === 'server') {
|
||||
loadStreamServers()
|
||||
} else if (val === 'upstream') {
|
||||
loadStreamUpstreams()
|
||||
}
|
||||
})
|
||||
|
||||
const handleSaveConfig = () => {
|
||||
useRequest(props.api.saveConfig(config.value)).onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearErrorLog = () => {
|
||||
useRequest(props.api.clearErrorLog()).onSuccess(() => {
|
||||
window.$message.success($gettext('Cleared successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
// Stream Server 操作
|
||||
const handleCreateStreamServer = () => {
|
||||
streamServerModalTitle.value = $gettext('Add Stream Server')
|
||||
streamServerEditName.value = ''
|
||||
streamServerModel.value = {
|
||||
name: '',
|
||||
listen: '',
|
||||
udp: false,
|
||||
proxy_pass: '',
|
||||
proxy_protocol: false,
|
||||
proxy_timeout: 0,
|
||||
proxy_connect_timeout: 0,
|
||||
ssl: false,
|
||||
ssl_certificate: '',
|
||||
ssl_certificate_key: ''
|
||||
}
|
||||
streamServerModal.value = true
|
||||
}
|
||||
|
||||
const handleEditStreamServer = (row: any) => {
|
||||
streamServerModalTitle.value = $gettext('Edit Stream Server')
|
||||
streamServerEditName.value = row.name
|
||||
streamServerModel.value = {
|
||||
name: row.name,
|
||||
listen: row.listen,
|
||||
udp: row.udp || false,
|
||||
proxy_pass: row.proxy_pass,
|
||||
proxy_protocol: row.proxy_protocol || false,
|
||||
proxy_timeout: row.proxy_timeout ? row.proxy_timeout / 1000000000 : 0,
|
||||
proxy_connect_timeout: row.proxy_connect_timeout ? row.proxy_connect_timeout / 1000000000 : 0,
|
||||
ssl: row.ssl || false,
|
||||
ssl_certificate: row.ssl_certificate || '',
|
||||
ssl_certificate_key: row.ssl_certificate_key || ''
|
||||
}
|
||||
streamServerModal.value = true
|
||||
}
|
||||
|
||||
const handleSaveStreamServer = () => {
|
||||
const data = {
|
||||
...streamServerModel.value,
|
||||
proxy_timeout: streamServerModel.value.proxy_timeout * 1000000000,
|
||||
proxy_connect_timeout: streamServerModel.value.proxy_connect_timeout * 1000000000
|
||||
}
|
||||
|
||||
const request = streamServerEditName.value
|
||||
? props.api.stream.updateServer(streamServerEditName.value, data)
|
||||
: props.api.stream.createServer(data)
|
||||
|
||||
useRequest(request).onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
streamServerModal.value = false
|
||||
loadStreamServers()
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteStreamServer = (name: string) => {
|
||||
useRequest(props.api.stream.deleteServer(name)).onSuccess(() => {
|
||||
window.$message.success($gettext('Deleted successfully'))
|
||||
loadStreamServers()
|
||||
})
|
||||
}
|
||||
|
||||
// Stream Upstream 操作
|
||||
const handleCreateStreamUpstream = () => {
|
||||
streamUpstreamModalTitle.value = $gettext('Add Stream Upstream')
|
||||
streamUpstreamEditName.value = ''
|
||||
streamUpstreamModel.value = {
|
||||
name: '',
|
||||
algo: '',
|
||||
servers: {},
|
||||
resolver: [],
|
||||
resolver_timeout: 5 * SECOND
|
||||
}
|
||||
upstreamServerAddr.value = ''
|
||||
upstreamServerOptions.value = ''
|
||||
streamUpstreamModal.value = true
|
||||
}
|
||||
|
||||
const handleEditStreamUpstream = (row: any) => {
|
||||
streamUpstreamModalTitle.value = $gettext('Edit Stream Upstream')
|
||||
streamUpstreamEditName.value = row.name
|
||||
streamUpstreamModel.value = {
|
||||
name: row.name,
|
||||
algo: row.algo || '',
|
||||
servers: { ...row.servers },
|
||||
resolver: row.resolver,
|
||||
resolver_timeout: row.resolver_timeout || 5 * SECOND
|
||||
}
|
||||
upstreamServerAddr.value = ''
|
||||
upstreamServerOptions.value = ''
|
||||
streamUpstreamModal.value = true
|
||||
}
|
||||
|
||||
const handleAddUpstreamServer = () => {
|
||||
if (!upstreamServerAddr.value) {
|
||||
window.$message.warning($gettext('Please enter server address'))
|
||||
return
|
||||
}
|
||||
streamUpstreamModel.value.servers[upstreamServerAddr.value] = upstreamServerOptions.value
|
||||
upstreamServerAddr.value = ''
|
||||
upstreamServerOptions.value = ''
|
||||
}
|
||||
|
||||
const handleRemoveUpstreamServer = (addr: string) => {
|
||||
delete streamUpstreamModel.value.servers[addr]
|
||||
}
|
||||
|
||||
const handleSaveStreamUpstream = () => {
|
||||
if (Object.keys(streamUpstreamModel.value.servers).length === 0) {
|
||||
window.$message.warning($gettext('Please add at least one server'))
|
||||
return
|
||||
}
|
||||
|
||||
const data = {
|
||||
name: streamUpstreamModel.value.name,
|
||||
algo: streamUpstreamModel.value.algo,
|
||||
servers: streamUpstreamModel.value.servers,
|
||||
resolver: streamUpstreamModel.value.resolver,
|
||||
resolver_timeout: streamUpstreamModel.value.resolver_timeout
|
||||
}
|
||||
|
||||
const request = streamUpstreamEditName.value
|
||||
? props.api.stream.updateUpstream(streamUpstreamEditName.value, data)
|
||||
: props.api.stream.createUpstream(data)
|
||||
|
||||
useRequest(request).onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
streamUpstreamModal.value = false
|
||||
loadStreamUpstreams()
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteStreamUpstream = (name: string) => {
|
||||
useRequest(props.api.stream.deleteUpstream(name)).onSuccess(() => {
|
||||
window.$message.success($gettext('Deleted successfully'))
|
||||
loadStreamUpstreams()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="status" :tab="$gettext('Running Status')">
|
||||
<service-status :service="service" show-reload />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config" :tab="$gettext('Modify Configuration')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="warning">
|
||||
{{
|
||||
$gettext(
|
||||
'This modifies the OpenResty main configuration file. If you do not understand the meaning of each parameter, please do not modify it randomly!'
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<common-editor v-model:value="config" lang="nginx" height="60vh" />
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleSaveConfig">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="stream" :tab="$gettext('Stream')">
|
||||
<n-tabs v-model:value="streamTab" type="line" placement="left" animated>
|
||||
<n-tab-pane name="server" :tab="$gettext('Server')">
|
||||
<n-flex vertical>
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleCreateStreamServer">
|
||||
{{ $gettext('Add Server') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-data-table
|
||||
striped
|
||||
:scroll-x="800"
|
||||
:loading="streamServersLoading"
|
||||
:columns="streamServerColumns"
|
||||
:data="streamServers"
|
||||
:row-key="(row: any) => row.name"
|
||||
/>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="upstream" :tab="$gettext('Upstream')">
|
||||
<n-flex vertical>
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleCreateStreamUpstream">
|
||||
{{ $gettext('Add Upstream') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-data-table
|
||||
striped
|
||||
:scroll-x="600"
|
||||
:loading="streamUpstreamsLoading"
|
||||
:columns="streamUpstreamColumns"
|
||||
:data="streamUpstreams"
|
||||
:row-key="(row: any) => row.name"
|
||||
/>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" :tab="$gettext('Load Status')">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="400"
|
||||
:loading="false"
|
||||
:columns="columns"
|
||||
:data="load"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="run-log" :tab="$gettext('Runtime Logs')">
|
||||
<realtime-log :service="service" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="error-log" :tab="$gettext('Error Logs')">
|
||||
<n-flex vertical>
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleClearErrorLog">
|
||||
{{ $gettext('Clear Log') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<realtime-log :path="errorLog" />
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
<!-- Stream Server 模态框 -->
|
||||
<n-modal
|
||||
v-model:show="streamServerModal"
|
||||
preset="card"
|
||||
:title="streamServerModalTitle"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
@close="streamServerModal = false"
|
||||
>
|
||||
<n-form :model="streamServerModel">
|
||||
<n-form-item path="name" :label="$gettext('Name')">
|
||||
<n-input
|
||||
v-model:value="streamServerModel.name"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
:placeholder="$gettext('Only letters, numbers, underscores and hyphens')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="listen" :label="$gettext('Listen Address')">
|
||||
<n-input
|
||||
v-model:value="streamServerModel.listen"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
:placeholder="$gettext('e.g. 12345 or 0.0.0.0:12345')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="proxy_pass" :label="$gettext('Proxy Pass')">
|
||||
<n-input
|
||||
v-model:value="streamServerModel.proxy_pass"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
:placeholder="$gettext('e.g. 127.0.0.1:3306 or upstream_name')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="udp" :label="$gettext('UDP Protocol')">
|
||||
<n-switch v-model:value="streamServerModel.udp" />
|
||||
</n-form-item>
|
||||
<n-form-item path="proxy_protocol" :label="$gettext('Proxy Protocol')">
|
||||
<n-switch v-model:value="streamServerModel.proxy_protocol" />
|
||||
</n-form-item>
|
||||
<n-form-item path="proxy_timeout" :label="$gettext('Proxy Timeout (seconds)')">
|
||||
<n-input-number v-model:value="streamServerModel.proxy_timeout" :min="0" />
|
||||
</n-form-item>
|
||||
<n-form-item path="proxy_connect_timeout" :label="$gettext('Connect Timeout (seconds)')">
|
||||
<n-input-number v-model:value="streamServerModel.proxy_connect_timeout" :min="0" />
|
||||
</n-form-item>
|
||||
<n-form-item path="ssl" :label="$gettext('Enable SSL')">
|
||||
<n-switch v-model:value="streamServerModel.ssl" />
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
v-if="streamServerModel.ssl"
|
||||
path="ssl_certificate"
|
||||
:label="$gettext('SSL Certificate Path')"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="streamServerModel.ssl_certificate"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
:placeholder="$gettext('e.g. /path/to/cert.pem')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
v-if="streamServerModel.ssl"
|
||||
path="ssl_certificate_key"
|
||||
:label="$gettext('SSL Private Key Path')"
|
||||
>
|
||||
<n-input
|
||||
v-model:value="streamServerModel.ssl_certificate_key"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
:placeholder="$gettext('e.g. /path/to/key.pem')"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-button type="info" block @click="handleSaveStreamServer">{{ $gettext('Submit') }}</n-button>
|
||||
</n-modal>
|
||||
<!-- Stream Upstream 模态框 -->
|
||||
<n-modal
|
||||
v-model:show="streamUpstreamModal"
|
||||
preset="card"
|
||||
:title="streamUpstreamModalTitle"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
@close="streamUpstreamModal = false"
|
||||
>
|
||||
<n-form :model="streamUpstreamModel">
|
||||
<n-form-item path="name" :label="$gettext('Name')">
|
||||
<n-input
|
||||
v-model:value="streamUpstreamModel.name"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
:placeholder="$gettext('Only letters, numbers, underscores and hyphens')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item path="algo" :label="$gettext('Load Balancing Algorithm')">
|
||||
<n-select
|
||||
v-model:value="streamUpstreamModel.algo"
|
||||
:options="[
|
||||
{ label: $gettext('Round Robin (Default)'), value: '' },
|
||||
{ label: 'least_conn', value: 'least_conn' },
|
||||
{ label: 'ip_hash', value: 'ip_hash' },
|
||||
{ label: 'hash $remote_addr', value: 'hash $remote_addr' },
|
||||
{ label: 'random', value: 'random' },
|
||||
{ label: 'least_time connect', value: 'least_time connect' },
|
||||
{ label: 'least_time first_byte', value: 'least_time first_byte' }
|
||||
]"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Servers')">
|
||||
<n-flex vertical wh-full>
|
||||
<n-flex>
|
||||
<n-input
|
||||
v-model:value="upstreamServerAddr"
|
||||
type="text"
|
||||
flex-1
|
||||
:placeholder="$gettext('Server address, e.g. 127.0.0.1:3306')"
|
||||
/>
|
||||
<n-input
|
||||
v-model:value="upstreamServerOptions"
|
||||
type="text"
|
||||
flex-1
|
||||
:placeholder="$gettext('Options (optional), e.g. weight=5 backup')"
|
||||
/>
|
||||
<n-button type="primary" @click="handleAddUpstreamServer">
|
||||
{{ $gettext('Add') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-table :bordered="false" :single-line="false" size="small">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $gettext('Address') }}</th>
|
||||
<th>{{ $gettext('Options') }}</th>
|
||||
<th w-100>{{ $gettext('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(options, addr) in streamUpstreamModel.servers" :key="addr">
|
||||
<td>{{ addr }}</td>
|
||||
<td>{{ options || '-' }}</td>
|
||||
<td>
|
||||
<n-button
|
||||
size="small"
|
||||
type="error"
|
||||
@click="handleRemoveUpstreamServer(addr as string)"
|
||||
>
|
||||
{{ $gettext('Delete') }}
|
||||
</n-button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="Object.keys(streamUpstreamModel.servers).length === 0">
|
||||
<td colspan="3" text-center>
|
||||
{{ $gettext('No servers added yet') }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</n-flex>
|
||||
</n-form-item>
|
||||
<n-form-item path="resolver" :label="$gettext('DNS Resolver')">
|
||||
<n-dynamic-tags
|
||||
v-model:value="streamUpstreamModel.resolver"
|
||||
:placeholder="$gettext('e.g., 8.8.8.8')"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item
|
||||
v-if="streamUpstreamModel.resolver.length"
|
||||
path="resolver_timeout"
|
||||
:label="$gettext('Resolver Timeout')"
|
||||
>
|
||||
<n-input-group>
|
||||
<n-input-number
|
||||
:value="parseDuration(streamUpstreamModel.resolver_timeout).value"
|
||||
:min="1"
|
||||
:max="3600"
|
||||
style="flex: 1"
|
||||
@update:value="(v: number | null) => updateResolverTimeoutValue(v ?? 5)"
|
||||
/>
|
||||
<n-select
|
||||
:value="parseDuration(streamUpstreamModel.resolver_timeout).unit"
|
||||
:options="[
|
||||
{ label: $gettext('Seconds'), value: 's' },
|
||||
{ label: $gettext('Minutes'), value: 'm' },
|
||||
{ label: $gettext('Hours'), value: 'h' }
|
||||
]"
|
||||
style="width: 100px"
|
||||
@update:value="(v: string) => updateResolverTimeoutUnit(v)"
|
||||
/>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-button type="info" block @click="handleSaveStreamUpstream">
|
||||
{{ $gettext('Submit') }}
|
||||
</n-button>
|
||||
</n-modal>
|
||||
</template>
|
||||
@@ -3,100 +3,10 @@ defineOptions({
|
||||
name: 'apps-openresty-index'
|
||||
})
|
||||
|
||||
import { NButton, NDataTable } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import openresty from '@/api/apps/openresty'
|
||||
import ServiceStatus from '@/components/common/ServiceStatus.vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const currentTab = ref('status')
|
||||
|
||||
const { data: config } = useRequest(openresty.config, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: errorLog } = useRequest(openresty.errorLog, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: load } = useRequest(openresty.load, {
|
||||
initialData: []
|
||||
})
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
title: $gettext('Property'),
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Current Value'),
|
||||
key: 'value',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
}
|
||||
]
|
||||
|
||||
const handleSaveConfig = () => {
|
||||
useRequest(openresty.saveConfig(config.value)).onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearErrorLog = () => {
|
||||
useRequest(openresty.clearErrorLog()).onSuccess(() => {
|
||||
window.$message.success($gettext('Cleared successfully'))
|
||||
})
|
||||
}
|
||||
import NginxIndex from '@/views/apps/nginx/NginxIndex.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="status" :tab="$gettext('Running Status')">
|
||||
<service-status service="nginx" show-reload />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config" :tab="$gettext('Modify Configuration')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="warning">
|
||||
{{
|
||||
$gettext(
|
||||
'This modifies the OpenResty main configuration file. If you do not understand the meaning of each parameter, please do not modify it randomly!'
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<common-editor v-model:value="config" lang="nginx" height="60vh" />
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleSaveConfig">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" :tab="$gettext('Load Status')">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="400"
|
||||
:loading="false"
|
||||
:columns="columns"
|
||||
:data="load"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="run-log" :tab="$gettext('Runtime Logs')">
|
||||
<realtime-log service="nginx" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="error-log" :tab="$gettext('Error Logs')">
|
||||
<n-flex vertical>
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleClearErrorLog">
|
||||
{{ $gettext('Clear Log') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<realtime-log :path="errorLog" />
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
<nginx-index :api="openresty" service="openresty" />
|
||||
</template>
|
||||
|
||||
@@ -3,135 +3,10 @@ defineOptions({
|
||||
name: 'apps-percona-index'
|
||||
})
|
||||
|
||||
import copy2clipboard from '@vavt/copy2clipboard'
|
||||
import { NButton, NDataTable, NInput } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import percona from '@/api/apps/percona'
|
||||
import ServiceStatus from '@/components/common/ServiceStatus.vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const currentTab = ref('status')
|
||||
|
||||
const { data: rootPassword } = useRequest(percona.rootPassword, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: config } = useRequest(percona.config, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: slowLog } = useRequest(percona.slowLog, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: load } = useRequest(percona.load, {
|
||||
initialData: []
|
||||
})
|
||||
|
||||
const loadColumns: any = [
|
||||
{
|
||||
title: $gettext('Property'),
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Current Value'),
|
||||
key: 'value',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
}
|
||||
]
|
||||
|
||||
const handleSaveConfig = () => {
|
||||
useRequest(percona.saveConfig(config.value)).onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearLog = () => {
|
||||
useRequest(percona.clearLog()).onSuccess(() => {
|
||||
window.$message.success($gettext('Cleared successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearSlowLog = () => {
|
||||
useRequest(percona.clearSlowLog()).onSuccess(() => {
|
||||
window.$message.success($gettext('Cleared successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleSetRootPassword = async () => {
|
||||
await percona.setRootPassword(rootPassword.value)
|
||||
window.$message.success($gettext('Modified successfully'))
|
||||
}
|
||||
|
||||
const handleCopyRootPassword = () => {
|
||||
copy2clipboard(rootPassword.value).then(() => {
|
||||
window.$message.success($gettext('Copied successfully'))
|
||||
})
|
||||
}
|
||||
import MysqlIndex from '@/views/apps/mysql/MysqlIndex.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="status" :tab="$gettext('Running Status')">
|
||||
<n-flex vertical>
|
||||
<service-status service="mysqld" />
|
||||
<n-card :title="$gettext('Root Password')">
|
||||
<n-flex>
|
||||
<n-input-group>
|
||||
<n-input v-model:value="rootPassword" type="password" show-password-on="click" />
|
||||
<n-button type="primary" ghost @click="handleCopyRootPassword">
|
||||
{{ $gettext('Copy') }}
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
<n-button type="primary" @click="handleSetRootPassword">
|
||||
{{ $gettext('Save Changes') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config" :tab="$gettext('Modify Configuration')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="warning">
|
||||
{{
|
||||
$gettext(
|
||||
'This modifies the Percona main configuration file. If you do not understand the meaning of each parameter, please do not modify it randomly!'
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<common-editor v-model:value="config" height="60vh" />
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleSaveConfig">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" :tab="$gettext('Load Status')">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="400"
|
||||
:loading="false"
|
||||
:columns="loadColumns"
|
||||
:data="load"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="run-log" :tab="$gettext('Runtime Logs')">
|
||||
<n-button type="primary" @click="handleClearLog">
|
||||
{{ $gettext('Clear Log') }}
|
||||
</n-button>
|
||||
<realtime-log service="mysqld" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="slow-log" :tab="$gettext('Slow Query Log')">
|
||||
<n-button type="primary" @click="handleClearSlowLog">
|
||||
{{ $gettext('Clear Slow Log') }}
|
||||
</n-button>
|
||||
<realtime-log :path="slowLog" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
<mysql-index :api="percona" name="Percona" />
|
||||
</template>
|
||||
|
||||
@@ -205,7 +205,7 @@ watch(isLogin, async () => {
|
||||
<n-image
|
||||
:src="'data:image/png;base64,' + captchaImage"
|
||||
preview-disabled
|
||||
class="cursor-pointer h-50"
|
||||
class="h-50 cursor-pointer"
|
||||
style="border-radius: 4px"
|
||||
@click="refreshCaptcha"
|
||||
/>
|
||||
|
||||
@@ -1,206 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { NInput } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
const config = defineModel<string>('config', { type: String, required: true })
|
||||
const setting = ref({
|
||||
auto_resolve: true,
|
||||
sni: true,
|
||||
cache: false,
|
||||
cache_time: 1,
|
||||
no_buffer: false,
|
||||
proxy_pass: '',
|
||||
host: '$host',
|
||||
match_type: '^~',
|
||||
match: '/',
|
||||
replace: []
|
||||
})
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (setting.value.cache && setting.value.no_buffer) {
|
||||
window.$message.error(
|
||||
$gettext('Disabled buffer and enabled cache cannot be used simultaneously')
|
||||
)
|
||||
return
|
||||
}
|
||||
if (setting.value.match.length === 0) {
|
||||
window.$message.error($gettext('Matching expression cannot be empty'))
|
||||
return
|
||||
}
|
||||
if (setting.value.proxy_pass.length === 0) {
|
||||
window.$message.error($gettext('Proxy address cannot be empty'))
|
||||
return
|
||||
}
|
||||
if (setting.value.match_type === '=' && setting.value.match[0] !== '/') {
|
||||
window.$message.error($gettext('Exact match expression must start with /'))
|
||||
return
|
||||
}
|
||||
if (
|
||||
(setting.value.match_type === '^~' || setting.value.match_type === ' ') &&
|
||||
setting.value.match[0] !== '/'
|
||||
) {
|
||||
window.$message.error($gettext('Prefix match expression must start with /'))
|
||||
return
|
||||
}
|
||||
try {
|
||||
new URL(setting.value.proxy_pass)
|
||||
} catch (error) {
|
||||
window.$message.error($gettext('Proxy address format error'))
|
||||
return
|
||||
}
|
||||
|
||||
let builder: string
|
||||
builder = 'location'
|
||||
switch (setting.value.match_type) {
|
||||
case '=':
|
||||
builder += ' ='
|
||||
break
|
||||
case '^~':
|
||||
builder += ' ^~'
|
||||
break
|
||||
case '~':
|
||||
builder += ' ~'
|
||||
break
|
||||
case '~*':
|
||||
builder += ' ~*'
|
||||
break
|
||||
}
|
||||
builder += ` ${setting.value.match}\n{\n`
|
||||
if (setting.value.auto_resolve) {
|
||||
builder += ` set $empty "";\n proxy_pass ${setting.value.proxy_pass}$empty;\n`
|
||||
} else {
|
||||
builder += ` proxy_pass ${setting.value.proxy_pass};\n`
|
||||
}
|
||||
if (setting.value.host) {
|
||||
builder += ` proxy_set_header Host ${setting.value.host};\n`
|
||||
}
|
||||
builder += ` proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Host $host;\n proxy_set_header X-Forwarded-Port $server_port;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_set_header X-Forwarded-Scheme $scheme;\n`
|
||||
builder += ` proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection $http_connection;\n proxy_set_header Early-Data $ssl_early_data;\n proxy_set_header Accept-Encoding "";\n proxy_http_version 1.1;\n proxy_ssl_protocols TLSv1.2 TLSv1.3;\n proxy_ssl_session_reuse off;\n`
|
||||
if (setting.value.sni) {
|
||||
builder += ` proxy_ssl_server_name on;\n`
|
||||
}
|
||||
if (setting.value.auto_resolve) {
|
||||
builder += ` resolver 8.8.8.8 ipv6=off;\n resolver_timeout 10s;\n`
|
||||
}
|
||||
if (setting.value.cache) {
|
||||
builder += ` proxy_ignore_headers X-Accel-Expires Expires Cache-Control Set-Cookie;\n proxy_cache cache_one;\n proxy_cache_key $scheme$host$uri$is_args$args;\n proxy_cache_valid 200 304 301 302 ${setting.value.cache_time}m;\n proxy_cache_lock on;\n proxy_cache_lock_timeout 5s;\n proxy_cache_lock_age 5s;\n proxy_cache_background_update on;\n proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504;\n proxy_cache_revalidate on;\n add_header X-Cache $upstream_cache_status;\n`
|
||||
}
|
||||
if (setting.value.no_buffer) {
|
||||
builder += ` proxy_buffering off;\n proxy_request_buffering off;\n`
|
||||
}
|
||||
if (setting.value.replace.length > 0) {
|
||||
builder += ` sub_filter_once off;\n sub_filter_types *;\n`
|
||||
for (const item of setting.value.replace) {
|
||||
builder += ` sub_filter "${(item as any).key}" "${(item as any).value}";\n`
|
||||
}
|
||||
}
|
||||
builder += `}\n`
|
||||
config.value = builder
|
||||
show.value = false
|
||||
window.$message.success($gettext('Configuration generated successfully'))
|
||||
}
|
||||
|
||||
// 通过代理地址尝试自动获取发送域名
|
||||
watch(
|
||||
() => setting.value.proxy_pass,
|
||||
(val) => {
|
||||
if (val.length > 0) {
|
||||
try {
|
||||
const url = new URL(val)
|
||||
setting.value.host = url.hostname
|
||||
} catch (error) {}
|
||||
}
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
preset="card"
|
||||
:title="$gettext('Generate Reverse Proxy Configuration')"
|
||||
style="width: 40vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
>
|
||||
<n-flex vertical>
|
||||
<n-alert type="warning">
|
||||
{{
|
||||
$gettext(
|
||||
'After generating the reverse proxy configuration, the original rewrite rules will be overwritten.'
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<n-alert type="info">
|
||||
{{
|
||||
$gettext(
|
||||
'If you need to proxy static resources like JS/CSS, please remove the static log recording part from the original configuration.'
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<n-form inline>
|
||||
<n-form-item :label="$gettext('Auto Refresh Resolution')">
|
||||
<n-switch v-model:value="setting.auto_resolve" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Enable SNI')">
|
||||
<n-switch v-model:value="setting.sni" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Enable Cache')">
|
||||
<n-switch v-model:value="setting.cache" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Disable Buffer')">
|
||||
<n-switch v-model:value="setting.no_buffer" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-form>
|
||||
<n-form-item :label="$gettext('Match Type')">
|
||||
<n-select
|
||||
v-model:value="setting.match_type"
|
||||
:options="[
|
||||
{ label: $gettext('Exact Match (=)'), value: '=' },
|
||||
{ label: $gettext('Priority Prefix Match (^~)'), value: '^~' },
|
||||
{ label: $gettext('Normal Prefix Match ( )'), value: ' ' },
|
||||
{ label: $gettext('Case Sensitive Regex Match (~)'), value: '~' },
|
||||
{ label: $gettext('Case Insensitive Regex Match (~*)'), value: '~*' }
|
||||
]"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Match Expression')">
|
||||
<n-input v-model:value="setting.match" placeholder="/" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Proxy Address')">
|
||||
<n-input v-model:value="setting.proxy_pass" placeholder="http://127.0.0.1:3000" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Send Domain')">
|
||||
<n-input v-model:value="setting.host" placeholder="$host" />
|
||||
</n-form-item>
|
||||
<n-form-item v-if="setting.cache" :label="$gettext('Cache Time')">
|
||||
<n-input-number
|
||||
v-model:value="setting.cache_time"
|
||||
w-full
|
||||
:min="1"
|
||||
:step="1"
|
||||
:placeholder="$gettext('Cache time (minutes)')"
|
||||
>
|
||||
<template #suffix> {{ $gettext('minutes') }} </template>
|
||||
</n-input-number>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Content Replacement')">
|
||||
<n-dynamic-input
|
||||
v-model:value="setting.replace"
|
||||
preset="pair"
|
||||
:max="5"
|
||||
:key-placeholder="$gettext('Target content')"
|
||||
:value-placeholder="$gettext('Replacement content')"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-button type="info" block @click="handleSubmit"> {{ $gettext('Submit') }} </n-button>
|
||||
</n-flex>
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
Reference in New Issue
Block a user