mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-10 17:26:38 +08:00
feat: add geo metadata to nodes and dashboard
- Extend DashboardNodeHealth and NodeItem interfaces to include geo_name, geo_latitude, and geo_longitude. - Update NodeDetailPage and NodesPage components to handle geo metadata in forms and payloads. - Implement validation for geo fields in NodesPage using Zod. - Enhance dashboard overview tests to include geo metadata for nodes. - Create WorldStage component to visualize node health and geo locations on a world map. - Update design and development documentation to reflect the addition of geo metadata for nodes.
This commit is contained in:
@@ -86,6 +86,9 @@ type NodeView struct {
|
||||
NodeID string `json:"node_id"`
|
||||
Name string `json:"name"`
|
||||
IP string `json:"ip"`
|
||||
GeoName string `json:"geo_name"`
|
||||
GeoLatitude *float64 `json:"geo_latitude"`
|
||||
GeoLongitude *float64 `json:"geo_longitude"`
|
||||
AgentToken string `json:"agent_token"`
|
||||
AutoUpdateEnabled bool `json:"auto_update_enabled"`
|
||||
UpdateRequested bool `json:"update_requested"`
|
||||
|
||||
@@ -102,6 +102,9 @@ type DashboardNodeHealth struct {
|
||||
ID uint `json:"id"`
|
||||
NodeID string `json:"node_id"`
|
||||
Name string `json:"name"`
|
||||
GeoName string `json:"geo_name"`
|
||||
GeoLatitude *float64 `json:"geo_latitude"`
|
||||
GeoLongitude *float64 `json:"geo_longitude"`
|
||||
Status string `json:"status"`
|
||||
OpenrestyStatus string `json:"openresty_status"`
|
||||
CurrentVersion string `json:"current_version"`
|
||||
@@ -204,6 +207,9 @@ func GetDashboardOverview() (*DashboardOverviewView, error) {
|
||||
ID: node.ID,
|
||||
NodeID: node.NodeID,
|
||||
Name: node.Name,
|
||||
GeoName: node.GeoName,
|
||||
GeoLatitude: node.GeoLatitude,
|
||||
GeoLongitude: node.GeoLongitude,
|
||||
Status: computedStatus,
|
||||
OpenrestyStatus: node.OpenrestyStatus,
|
||||
CurrentVersion: node.CurrentVersion,
|
||||
|
||||
@@ -13,8 +13,11 @@ import (
|
||||
)
|
||||
|
||||
type NodeInput struct {
|
||||
Name string `json:"name"`
|
||||
AutoUpdateEnabled bool `json:"auto_update_enabled"`
|
||||
Name string `json:"name"`
|
||||
AutoUpdateEnabled bool `json:"auto_update_enabled"`
|
||||
GeoName string `json:"geo_name"`
|
||||
GeoLatitude *float64 `json:"geo_latitude"`
|
||||
GeoLongitude *float64 `json:"geo_longitude"`
|
||||
}
|
||||
|
||||
type NodeAgentUpdateInput struct {
|
||||
@@ -47,19 +50,21 @@ type AgentRegistrationResponse struct {
|
||||
}
|
||||
|
||||
func CreateNode(input NodeInput) (*NodeView, error) {
|
||||
name := strings.TrimSpace(input.Name)
|
||||
name, geoName, geoLatitude, geoLongitude, err := normalizeNodeInput(input)
|
||||
if name == "" {
|
||||
return nil, errors.New("节点名不能为空")
|
||||
}
|
||||
node := &model.Node{
|
||||
Name: name,
|
||||
IP: "",
|
||||
GeoName: geoName,
|
||||
GeoLatitude: geoLatitude,
|
||||
GeoLongitude: geoLongitude,
|
||||
AgentVersion: "",
|
||||
NginxVersion: "",
|
||||
Status: NodeStatusPending,
|
||||
AutoUpdateEnabled: input.AutoUpdateEnabled,
|
||||
}
|
||||
var err error
|
||||
node.NodeID, err = newServerNodeID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -79,7 +84,7 @@ func CreateNode(input NodeInput) (*NodeView, error) {
|
||||
}
|
||||
|
||||
func UpdateNode(id uint, input NodeInput) (*NodeView, error) {
|
||||
name := strings.TrimSpace(input.Name)
|
||||
name, geoName, geoLatitude, geoLongitude, err := normalizeNodeInput(input)
|
||||
if name == "" {
|
||||
return nil, errors.New("节点名不能为空")
|
||||
}
|
||||
@@ -88,6 +93,9 @@ func UpdateNode(id uint, input NodeInput) (*NodeView, error) {
|
||||
return nil, err
|
||||
}
|
||||
node.Name = name
|
||||
node.GeoName = geoName
|
||||
node.GeoLatitude = geoLatitude
|
||||
node.GeoLongitude = geoLongitude
|
||||
node.AutoUpdateEnabled = input.AutoUpdateEnabled
|
||||
if err = node.Update(); err != nil {
|
||||
return nil, err
|
||||
@@ -231,6 +239,9 @@ func buildNodeView(node *model.Node) *NodeView {
|
||||
NodeID: node.NodeID,
|
||||
Name: node.Name,
|
||||
IP: node.IP,
|
||||
GeoName: strings.TrimSpace(node.GeoName),
|
||||
GeoLatitude: node.GeoLatitude,
|
||||
GeoLongitude: node.GeoLongitude,
|
||||
AgentToken: node.AgentToken,
|
||||
UpdateChannel: strings.TrimSpace(node.UpdateChannel),
|
||||
UpdateTag: strings.TrimSpace(node.UpdateTag),
|
||||
@@ -254,6 +265,36 @@ func buildNodeView(node *model.Node) *NodeView {
|
||||
return view
|
||||
}
|
||||
|
||||
func normalizeNodeInput(input NodeInput) (string, string, *float64, *float64, error) {
|
||||
name := strings.TrimSpace(input.Name)
|
||||
geoName := strings.TrimSpace(input.GeoName)
|
||||
if len(geoName) > 128 {
|
||||
return "", "", nil, nil, errors.New("节点位置名不能超过 128 个字符")
|
||||
}
|
||||
|
||||
geoLatitude := cloneCoordinate(input.GeoLatitude)
|
||||
geoLongitude := cloneCoordinate(input.GeoLongitude)
|
||||
if (geoLatitude == nil) != (geoLongitude == nil) {
|
||||
return "", "", nil, nil, errors.New("地图坐标必须同时填写纬度和经度")
|
||||
}
|
||||
if geoLatitude != nil && (*geoLatitude < -90 || *geoLatitude > 90) {
|
||||
return "", "", nil, nil, errors.New("纬度必须在 -90 到 90 之间")
|
||||
}
|
||||
if geoLongitude != nil && (*geoLongitude < -180 || *geoLongitude > 180) {
|
||||
return "", "", nil, nil, errors.New("经度必须在 -180 到 180 之间")
|
||||
}
|
||||
|
||||
return name, geoName, geoLatitude, geoLongitude, nil
|
||||
}
|
||||
|
||||
func cloneCoordinate(value *float64) *float64 {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
cloned := *value
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func buildNodeAgentReleaseView(node *model.Node, release *githubReleaseResponse, channel ReleaseChannel) *NodeAgentReleaseInfo {
|
||||
currentVersion := strings.TrimSpace(node.AgentVersion)
|
||||
view := &NodeAgentReleaseInfo{
|
||||
|
||||
@@ -20,10 +20,20 @@ func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
func TestRequestNodeAgentPreviewUpdate(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
node, err := CreateNode(NodeInput{Name: "preview-edge-1"})
|
||||
latitude := 31.2304
|
||||
longitude := 121.4737
|
||||
node, err := CreateNode(NodeInput{
|
||||
Name: "preview-edge-1",
|
||||
GeoName: "Shanghai",
|
||||
GeoLatitude: &latitude,
|
||||
GeoLongitude: &longitude,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create node: %v", err)
|
||||
}
|
||||
if node.GeoName != "Shanghai" || node.GeoLatitude == nil || node.GeoLongitude == nil {
|
||||
t.Fatalf("expected geo metadata to be returned, got %+v", node)
|
||||
}
|
||||
|
||||
originalClient := UpdateHTTPClientForTest()
|
||||
SetUpdateHTTPClientForTest(&http.Client{
|
||||
@@ -150,6 +160,56 @@ func TestHeartbeatNodeReturnsPreviewUpdateSettings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateNodeValidatesAndPersistsGeoMetadata(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
node, err := CreateNode(NodeInput{Name: "geo-edge"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create node: %v", err)
|
||||
}
|
||||
|
||||
latitude := 37.7749
|
||||
longitude := -122.4194
|
||||
updated, err := UpdateNode(node.ID, NodeInput{
|
||||
Name: "geo-edge-updated",
|
||||
AutoUpdateEnabled: true,
|
||||
GeoName: "San Francisco",
|
||||
GeoLatitude: &latitude,
|
||||
GeoLongitude: &longitude,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("expected node update to succeed: %v", err)
|
||||
}
|
||||
if updated.GeoName != "San Francisco" || updated.GeoLatitude == nil || updated.GeoLongitude == nil {
|
||||
t.Fatalf("expected geo metadata in view, got %+v", updated)
|
||||
}
|
||||
|
||||
stored, err := model.GetNodeByID(node.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to load node: %v", err)
|
||||
}
|
||||
if stored.GeoName != "San Francisco" || stored.GeoLatitude == nil || stored.GeoLongitude == nil {
|
||||
t.Fatalf("expected geo metadata persisted, got %+v", stored)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateNodeRejectsPartialGeoMetadata(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
node, err := CreateNode(NodeInput{Name: "geo-edge-invalid"})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create node: %v", err)
|
||||
}
|
||||
|
||||
latitude := 37.7749
|
||||
if _, err = UpdateNode(node.ID, NodeInput{
|
||||
Name: "geo-edge-invalid",
|
||||
GeoLatitude: &latitude,
|
||||
}); err == nil {
|
||||
t.Fatal("expected partial geo metadata to be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestNodeOpenrestyRestart(t *testing.T) {
|
||||
setupServiceTestDB(t)
|
||||
|
||||
@@ -176,6 +236,7 @@ func TestListNodeViewsIncludesLatestApplyLogsForMultipleNodes(t *testing.T) {
|
||||
NodeID: "node-a",
|
||||
Name: "edge-a",
|
||||
IP: "10.0.0.11",
|
||||
GeoName: "Shanghai",
|
||||
AgentToken: "token-a",
|
||||
AgentVersion: "v0.5.0",
|
||||
NginxVersion: "1.27.1.2",
|
||||
@@ -225,6 +286,9 @@ func TestListNodeViewsIncludesLatestApplyLogsForMultipleNodes(t *testing.T) {
|
||||
if views[0].NodeID != "node-a" || views[0].LatestApplyResult != ApplyResultFailed || views[0].LatestApplyMessage != "latest failure" {
|
||||
t.Fatalf("unexpected latest apply log for node-a: %+v", views[0])
|
||||
}
|
||||
if views[0].GeoName != "Shanghai" {
|
||||
t.Fatalf("expected geo name to be exposed on node view, got %+v", views[0])
|
||||
}
|
||||
if views[1].NodeID != "node-b" || views[1].LatestApplyResult != ApplyResultOK || views[1].LatestApplyMessage != "latest success" {
|
||||
t.Fatalf("unexpected latest apply log for node-b: %+v", views[1])
|
||||
}
|
||||
@@ -658,6 +722,7 @@ func TestGetDashboardOverview(t *testing.T) {
|
||||
NodeID: "node-dashboard-a",
|
||||
Name: "edge-a",
|
||||
IP: "10.0.0.71",
|
||||
GeoName: "Shanghai",
|
||||
AgentToken: "token-a",
|
||||
AgentVersion: "v0.6.0",
|
||||
NginxVersion: "1.27.1.2",
|
||||
@@ -670,6 +735,7 @@ func TestGetDashboardOverview(t *testing.T) {
|
||||
NodeID: "node-dashboard-b",
|
||||
Name: "edge-b",
|
||||
IP: "10.0.0.72",
|
||||
GeoName: "San Francisco",
|
||||
AgentToken: "token-b",
|
||||
AgentVersion: "v0.6.0",
|
||||
NginxVersion: "1.27.1.2",
|
||||
@@ -782,6 +848,9 @@ func TestGetDashboardOverview(t *testing.T) {
|
||||
if len(view.Nodes) != 2 || len(view.ActiveAlerts) != 1 {
|
||||
t.Fatalf("unexpected dashboard nodes/alerts: %+v %+v", view.Nodes, view.ActiveAlerts)
|
||||
}
|
||||
if view.Nodes[0].GeoName == "" && view.Nodes[1].GeoName == "" {
|
||||
t.Fatalf("expected dashboard nodes to expose geo metadata: %+v", view.Nodes)
|
||||
}
|
||||
if len(view.Trends.Traffic24h) != 24 || len(view.Trends.Capacity24h) != 24 || len(view.Trends.Network24h) != 24 || len(view.Trends.DiskIO24h) != 24 {
|
||||
t.Fatalf("expected 24-point dashboard trends, got %+v", view.Trends)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user