Skip to content

Commit e2b66fd

Browse files
committed
feat(dashboard): add sites navigation #1054
1 parent 8f053fa commit e2b66fd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+4131
-284
lines changed

api/cluster/namespace.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,4 +139,4 @@ func RestartNginx(c *gin.Context) {
139139
c.JSON(http.StatusOK, gin.H{
140140
"message": "ok",
141141
})
142-
}
142+
}

api/config/list.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717

1818
// ConfigFileEntity represents a generic configuration file entity
1919
type ConfigFileEntity struct {
20-
path string
20+
path string
2121
namespaceID uint64
2222
namespace *model.Namespace
2323
}
@@ -68,7 +68,7 @@ func GetConfigs(c *gin.Context) {
6868
Search: search,
6969
OrderBy: sortBy,
7070
Sort: order,
71-
NamespaceID: namespaceID,
71+
NamespaceID: namespaceID,
7272
IncludeDirs: true, // Keep directories for the list.go endpoint
7373
}
7474

@@ -90,7 +90,7 @@ func GetConfigs(c *gin.Context) {
9090
// For generic config files, we don't have database records
9191
// so namespaceID and namespace will be 0 and nil
9292
entity := &ConfigFileEntity{
93-
path: filepath.Join(nginx.GetConfPath(dir), file.Name()),
93+
path: filepath.Join(nginx.GetConfPath(dir), file.Name()),
9494
namespaceID: 0,
9595
namespace: nil,
9696
}
@@ -124,14 +124,14 @@ func GetConfigs(c *gin.Context) {
124124
func createConfigBuilder(dir string) config.ConfigBuilder {
125125
return func(fileName string, fileInfo os.FileInfo, status config.ConfigStatus, namespaceID uint64, namespace *model.Namespace) config.Config {
126126
return config.Config{
127-
Name: fileName,
128-
ModifiedAt: fileInfo.ModTime(),
129-
Size: fileInfo.Size(),
130-
IsDir: fileInfo.IsDir(),
131-
Status: status,
127+
Name: fileName,
128+
ModifiedAt: fileInfo.ModTime(),
129+
Size: fileInfo.Size(),
130+
IsDir: fileInfo.IsDir(),
131+
Status: status,
132132
NamespaceID: namespaceID,
133133
Namespace: namespace,
134-
Dir: dir,
134+
Dir: dir,
135135
}
136136
}
137137
}

api/openai/router.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package openai
22

33
import "github.com/gin-gonic/gin"
44

5-
65
func InitRouter(r *gin.RouterGroup) {
76
// ChatGPT
87
r.POST("chatgpt", MakeChatCompletionRequest)

api/sites/list.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ import (
1313
func GetSiteList(c *gin.Context) {
1414
// Parse query parameters
1515
options := &site.ListOptions{
16-
Search: c.Query("search"),
17-
Name: c.Query("name"),
18-
Status: c.Query("status"),
19-
OrderBy: c.Query("sort_by"),
20-
Sort: c.DefaultQuery("order", "desc"),
16+
Search: c.Query("search"),
17+
Name: c.Query("name"),
18+
Status: c.Query("status"),
19+
OrderBy: c.Query("sort_by"),
20+
Sort: c.DefaultQuery("order", "desc"),
2121
NamespaceID: cast.ToUint64(c.Query("env_group_id")),
2222
}
2323

api/sites/router.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,25 @@ package sites
33
import "github.com/gin-gonic/gin"
44

55
func InitRouter(r *gin.RouterGroup) {
6+
// Initialize WebSocket notifications for site checking
7+
InitWebSocketNotifications()
8+
69
r.GET("sites", GetSiteList)
710
r.GET("sites/:name", GetSite)
811
r.PUT("sites", BatchUpdateSites)
912
r.POST("sites/:name/advance", DomainEditByAdvancedMode)
1013
r.POST("auto_cert/:name", AddDomainToAutoCert)
1114
r.DELETE("auto_cert/:name", RemoveDomainFromAutoCert)
1215

16+
// site navigation endpoints
17+
r.GET("site_navigation", GetSiteNavigation)
18+
r.GET("site_navigation/status", GetSiteNavigationStatus)
19+
r.POST("site_navigation/order", UpdateSiteOrder)
20+
r.GET("site_navigation/health_check/:id", GetHealthCheck)
21+
r.PUT("site_navigation/health_check/:id", UpdateHealthCheck)
22+
r.POST("site_navigation/test_health_check/:id", TestHealthCheck)
23+
r.GET("site_navigation_ws", SiteNavigationWebSocket)
24+
1325
// rename site
1426
r.POST("sites/:name/rename", RenameSite)
1527
// enable site

api/sites/site.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
// buildProxyTargets processes proxy targets similar to list.go logic
2121
func buildProxyTargets(fileName string) []site.ProxyTarget {
2222
indexedSite := site.GetIndexedSite(fileName)
23-
23+
2424
// Convert proxy targets, expanding upstream references
2525
var proxyTargets []site.ProxyTarget
2626
upstreamService := upstream.GetUpstreamService()
@@ -132,7 +132,7 @@ func SaveSite(c *gin.Context) {
132132

133133
var json struct {
134134
Content string `json:"content" binding:"required"`
135-
NamespaceID uint64 `json:"env_group_id"`
135+
NamespaceID uint64 `json:"env_group_id"`
136136
SyncNodeIDs []uint64 `json:"sync_node_ids"`
137137
Overwrite bool `json:"overwrite"`
138138
PostAction string `json:"post_action"`

api/sites/sitecheck.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package sites
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"time"
7+
8+
"github.com/0xJacky/Nginx-UI/internal/sitecheck"
9+
"github.com/0xJacky/Nginx-UI/model"
10+
"github.com/0xJacky/Nginx-UI/query"
11+
"github.com/gin-gonic/gin"
12+
"github.com/spf13/cast"
13+
"github.com/uozi-tech/cosy"
14+
"github.com/uozi-tech/cosy/logger"
15+
)
16+
17+
// GetSiteNavigation returns all sites for navigation dashboard
18+
func GetSiteNavigation(c *gin.Context) {
19+
service := sitecheck.GetService()
20+
sites := service.GetSites()
21+
22+
c.JSON(http.StatusOK, gin.H{
23+
"data": sites,
24+
})
25+
}
26+
27+
// GetSiteNavigationStatus returns the status of site checking service
28+
func GetSiteNavigationStatus(c *gin.Context) {
29+
service := sitecheck.GetService()
30+
31+
c.JSON(http.StatusOK, gin.H{
32+
"running": service.IsRunning(),
33+
})
34+
}
35+
36+
// UpdateSiteOrder updates the custom order of sites
37+
func UpdateSiteOrder(c *gin.Context) {
38+
var req struct {
39+
OrderedIds []uint64 `json:"ordered_ids" binding:"required"`
40+
}
41+
42+
if !cosy.BindAndValid(c, &req) {
43+
return
44+
}
45+
46+
if err := updateSiteOrderBatchByIds(req.OrderedIds); err != nil {
47+
cosy.ErrHandler(c, err)
48+
return
49+
}
50+
51+
c.JSON(http.StatusOK, gin.H{
52+
"message": "Order updated successfully",
53+
})
54+
}
55+
56+
// updateSiteOrderBatchByIds updates site order in batch using IDs
57+
func updateSiteOrderBatchByIds(orderedIds []uint64) error {
58+
sc := query.SiteConfig
59+
60+
for i, id := range orderedIds {
61+
if _, err := sc.Where(sc.ID.Eq(id)).Update(sc.CustomOrder, i); err != nil {
62+
return err
63+
}
64+
}
65+
66+
return nil
67+
}
68+
69+
// GetHealthCheck gets health check configuration for a site
70+
func GetHealthCheck(c *gin.Context) {
71+
id := cast.ToUint64(c.Param("id"))
72+
73+
sc := query.SiteConfig
74+
siteConfig, err := sc.Where(sc.ID.Eq(id)).First()
75+
if err != nil {
76+
cosy.ErrHandler(c, err)
77+
return
78+
}
79+
80+
ensureHealthCheckConfig(siteConfig)
81+
82+
c.JSON(http.StatusOK, siteConfig)
83+
}
84+
85+
// createDefaultHealthCheckConfig creates default health check configuration
86+
func createDefaultHealthCheckConfig() *model.HealthCheckConfig {
87+
return &model.HealthCheckConfig{
88+
Protocol: "http",
89+
Method: "GET",
90+
Path: "/",
91+
ExpectedStatus: []int{200},
92+
GRPCMethod: "Check",
93+
}
94+
}
95+
96+
// ensureHealthCheckConfig ensures health check config is not nil
97+
func ensureHealthCheckConfig(siteConfig *model.SiteConfig) {
98+
if siteConfig.HealthCheckConfig == nil {
99+
siteConfig.HealthCheckConfig = createDefaultHealthCheckConfig()
100+
}
101+
}
102+
103+
// UpdateHealthCheck updates health check configuration for a site
104+
func UpdateHealthCheck(c *gin.Context) {
105+
id := cast.ToUint64(c.Param("id"))
106+
107+
var req model.SiteConfig
108+
109+
if !cosy.BindAndValid(c, &req) {
110+
return
111+
}
112+
113+
sc := query.SiteConfig
114+
siteConfig, err := sc.Where(sc.ID.Eq(id)).First()
115+
if err != nil {
116+
cosy.ErrHandler(c, err)
117+
return
118+
}
119+
120+
siteConfig.HealthCheckEnabled = req.HealthCheckEnabled
121+
siteConfig.CheckInterval = req.CheckInterval
122+
siteConfig.Timeout = req.Timeout
123+
siteConfig.UserAgent = req.UserAgent
124+
siteConfig.MaxRedirects = req.MaxRedirects
125+
siteConfig.FollowRedirects = req.FollowRedirects
126+
siteConfig.CheckFavicon = req.CheckFavicon
127+
128+
if req.HealthCheckConfig != nil {
129+
siteConfig.HealthCheckConfig = req.HealthCheckConfig
130+
}
131+
132+
if err = query.SiteConfig.Save(siteConfig); err != nil {
133+
cosy.ErrHandler(c, err)
134+
return
135+
}
136+
137+
c.JSON(http.StatusOK, gin.H{
138+
"message": "Health check configuration updated successfully",
139+
})
140+
}
141+
142+
// TestHealthCheck tests a health check configuration without saving it
143+
func TestHealthCheck(c *gin.Context) {
144+
id := cast.ToUint64(c.Param("id"))
145+
146+
var req struct {
147+
Config *model.HealthCheckConfig `json:"config" binding:"required"`
148+
}
149+
150+
if !cosy.BindAndValid(c, &req) {
151+
return
152+
}
153+
154+
// Get site config to determine the host for testing
155+
sc := query.SiteConfig
156+
siteConfig, err := sc.Where(sc.ID.Eq(id)).First()
157+
if err != nil {
158+
cosy.ErrHandler(c, err)
159+
return
160+
}
161+
162+
// Create enhanced checker and test the configuration
163+
enhancedChecker := sitecheck.NewEnhancedSiteChecker()
164+
165+
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
166+
defer cancel()
167+
168+
// Convert host to URL for testing
169+
testURL := siteConfig.Scheme + "://" + siteConfig.Host
170+
result, err := enhancedChecker.CheckSiteWithConfig(ctx, testURL, req.Config)
171+
172+
if err != nil {
173+
logger.Errorf("Health check test failed for %s: %v", siteConfig.Host, err)
174+
c.JSON(http.StatusOK, gin.H{
175+
"success": false,
176+
"error": err.Error(),
177+
"response_time": 0,
178+
})
179+
return
180+
}
181+
182+
success := result.Status == "online"
183+
errorMsg := ""
184+
if !success && result.Error != "" {
185+
errorMsg = result.Error
186+
}
187+
188+
c.JSON(http.StatusOK, gin.H{
189+
"success": success,
190+
"response_time": result.ResponseTime,
191+
"status": result.Status,
192+
"status_code": result.StatusCode,
193+
"error": errorMsg,
194+
})
195+
}

0 commit comments

Comments
 (0)