admin: Cluster → Vector Servers page

Lists the weed-vector engines registered with the master, with version
and health, next to the other server lists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011UpvafvK8JpagF94MHWqwK
This commit is contained in:
Chris LuandClaude Opus 5.5 committed 2026-11-12 22:23:42 -08:00
1 parent baaa60b24a
commit 70f5d93e3e
7 files changed
+518 -302

No files matched your search

+1
View File
@@ -131,6 +131,7 @@ func (h *AdminHandlers) registerUIRoutes(r *mux.Router) {
r.HandleFunc("/cluster/masters", h.clusterHandlers.ShowClusterMasters).Methods(http.MethodGet)
r.HandleFunc("/cluster/filers", h.clusterHandlers.ShowClusterFilers).Methods(http.MethodGet)
r.HandleFunc("/cluster/s3", h.clusterHandlers.ShowClusterS3Servers).Methods(http.MethodGet)
r.HandleFunc("/cluster/vector-servers", h.clusterHandlers.ShowClusterVectorServers).Methods(http.MethodGet)
r.HandleFunc("/cluster/volume-servers", h.clusterHandlers.ShowClusterVolumeServers).Methods(http.MethodGet)
r.HandleFunc("/cluster/mount-clients", h.clusterHandlers.ShowMountClients).Methods(http.MethodGet)
+10
View File
@@ -388,6 +388,16 @@ func (h *ClusterHandlers) ShowClusterS3Servers(w http.ResponseWriter, r *http.Re
}
}
// ShowClusterVectorServers renders the vector engines registered with the master
func (h *ClusterHandlers) ShowClusterVectorServers(w http.ResponseWriter, r *http.Request) {
data := h.adminServer.GetVectorOperationsData(r.Context())
w.Header().Set("Content-Type", "text/html")
viewCtx := layout.NewViewContext(r, usernameOrDefault(r), dash.CSRFTokenFromContext(r.Context()))
if err := layout.Layout(viewCtx, app.ClusterVectorServers(data)).Render(r.Context(), w); err != nil {
writeJSONError(w, http.StatusInternalServerError, "Failed to render template: "+err.Error())
}
}
// GetClusterTopology returns the cluster topology as JSON
func (h *ClusterHandlers) GetClusterTopology(w http.ResponseWriter, r *http.Request) {
topology, err := h.adminServer.GetClusterTopology()
@@ -0,0 +1,42 @@
package app
import "github.com/seaweedfs/seaweedfs/weed/admin/dash"
templ ClusterVectorServers(data dash.VectorOperationsData) {
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">
<i class="bi bi-arrow-up-right me-2"></i>Vector Servers
</h1>
</div>
<div class="card shadow mb-4">
<div class="card-header">Vector engines</div>
<div class="card-body">
if data.DiscoveryError != "" {
<div class="alert alert-warning mb-0">Engine discovery failed: { data.DiscoveryError }</div>
} else if len(data.Engines) == 0 {
<p class="mb-0 text-muted">No weed-vector engine is registered with the master.</p>
} else {
<div class="table-responsive">
<table class="table table-hover mb-0" id="vectorServersTable">
<thead>
<tr>
<th>Address</th>
<th>Version</th>
<th>Status</th>
</tr>
</thead>
<tbody>
for _, engine := range data.Engines {
<tr>
<td>{ engine.Address }</td>
<td><span class="badge bg-light text-dark">{ engine.Version }</span></td>
<td>{ engine.Status }</td>
</tr>
}
</tbody>
</table>
</div>
}
</div>
</div>
}
@@ -0,0 +1,121 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.3.1100
package app
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import "github.com/seaweedfs/seaweedfs/weed/admin/dash"
func ClusterVectorServers(data dash.VectorOperationsData) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
return templ_7745c5c3_CtxErr
}
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
if !templ_7745c5c3_IsBuffer {
defer func() {
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
if templ_7745c5c3_Err == nil {
templ_7745c5c3_Err = templ_7745c5c3_BufErr
}
}()
}
ctx = templ.InitializeContext(ctx)
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom\"><h1 class=\"h2\"><i class=\"bi bi-arrow-up-right me-2\"></i>Vector Servers</h1></div><div class=\"card shadow mb-4\"><div class=\"card-header\">Vector engines</div><div class=\"card-body\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.DiscoveryError != "" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"alert alert-warning mb-0\">Engine discovery failed: ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.DiscoveryError)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_vector_servers.templ`, Line: 15, Col: 88}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else if len(data.Engines) == 0 {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<p class=\"mb-0 text-muted\">No weed-vector engine is registered with the master.</p>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"table-responsive\"><table class=\"table table-hover mb-0\" id=\"vectorServersTable\"><thead><tr><th>Address</th><th>Version</th><th>Status</th></tr></thead> <tbody>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, engine := range data.Engines {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<tr><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(engine.Address)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_vector_servers.templ`, Line: 31, Col: 29}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</td><td><span class=\"badge bg-light text-dark\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(engine.Version)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_vector_servers.templ`, Line: 32, Col: 68}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span></td><td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(engine.Status)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_vector_servers.templ`, Line: 33, Col: 28}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td></tr>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</tbody></table></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
return nil
})
}
var _ = templruntime.GeneratedTemplate
@@ -0,0 +1,24 @@
package app
import (
"bytes"
"testing"
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
"github.com/stretchr/testify/require"
)
func TestClusterVectorServers(t *testing.T) {
var out bytes.Buffer
require.NoError(t, ClusterVectorServers(dash.VectorOperationsData{Engines: []dash.VectorNode{{Address: "10.0.0.6:18888", Version: "0.1.0", Status: "Ready"}}}).Render(t.Context(), &out))
require.Contains(t, out.String(), "10.0.0.6:18888")
require.Contains(t, out.String(), "Ready")
out.Reset()
require.NoError(t, ClusterVectorServers(dash.VectorOperationsData{}).Render(t.Context(), &out))
require.Contains(t, out.String(), "No weed-vector engine is registered")
out.Reset()
require.NoError(t, ClusterVectorServers(dash.VectorOperationsData{DiscoveryError: "no master"}).Render(t.Context(), &out))
require.Contains(t, out.String(), "Engine discovery failed: no master")
}
+6 -1
View File
@@ -32,7 +32,7 @@ templ Layout(view ViewContext, content templ.Component) {
isStoragePage := strings.HasPrefix(currentPath, "/storage/volumes") || strings.HasPrefix(currentPath, "/storage/ec-shards") || strings.HasPrefix(currentPath, "/storage/collections")
// Detect if we're on a cluster page (but not storage page) to keep submenu expanded
isClusterPage := (strings.HasPrefix(currentPath, "/cluster/masters") || strings.HasPrefix(currentPath, "/cluster/volume-servers") || strings.HasPrefix(currentPath, "/cluster/filers") || strings.HasPrefix(currentPath, "/cluster/s3") || strings.HasPrefix(currentPath, "/cluster/mount-clients"))
isClusterPage := (strings.HasPrefix(currentPath, "/cluster/masters") || strings.HasPrefix(currentPath, "/cluster/volume-servers") || strings.HasPrefix(currentPath, "/cluster/filers") || strings.HasPrefix(currentPath, "/cluster/s3") || strings.HasPrefix(currentPath, "/cluster/vector-servers") || strings.HasPrefix(currentPath, "/cluster/mount-clients"))
isDefaultWorkerPage := currentPath == "/plugin" ||
strings.HasPrefix(currentPath, "/plugin/configuration") ||
@@ -148,6 +148,11 @@ templ Layout(view ViewContext, content templ.Component) {
<i class="bi bi-cloud me-2"></i>S3 Servers
</a>
</li>
<li class="nav-item">
<a class="nav-link py-2" href={ view.P("/cluster/vector-servers") }>
<i class="bi bi-arrow-up-right me-2"></i>Vector Servers
</a>
</li>
<li class="nav-item">
<a class="nav-link py-2" href={ view.P("/cluster/mount-clients") }>
<i class="bi bi-plug me-2"></i>Mount Clients
File diff suppressed because it is too large. Load diff