1
0
Fork 0
mirror of https://github.com/chrislusf/seaweedfs synced 2025-09-10 05:12:47 +02:00
seaweedfs/weed/admin/view/layout/layout.templ
Chris Lu 891a2fb6eb
Admin: misc improvements on admin server and workers. EC now works. (#7055)
* initial design

* added simulation as tests

* reorganized the codebase to move the simulation framework and tests into their own dedicated package

* integration test. ec worker task

* remove "enhanced" reference

* start master, volume servers, filer

Current Status
 Master: Healthy and running (port 9333)
 Filer: Healthy and running (port 8888)
 Volume Servers: All 6 servers running (ports 8080-8085)
🔄 Admin/Workers: Will start when dependencies are ready

* generate write load

* tasks are assigned

* admin start wtih grpc port. worker has its own working directory

* Update .gitignore

* working worker and admin. Task detection is not working yet.

* compiles, detection uses volumeSizeLimitMB from master

* compiles

* worker retries connecting to admin

* build and restart

* rendering pending tasks

* skip task ID column

* sticky worker id

* test canScheduleTaskNow

* worker reconnect to admin

* clean up logs

* worker register itself first

* worker can run ec work and report status

but:
1. one volume should not be repeatedly worked on.
2. ec shards needs to be distributed and source data should be deleted.

* move ec task logic

* listing ec shards

* local copy, ec. Need to distribute.

* ec is mostly working now

* distribution of ec shards needs improvement
* need configuration to enable ec

* show ec volumes

* interval field UI component

* rename

* integration test with vauuming

* garbage percentage threshold

* fix warning

* display ec shard sizes

* fix ec volumes list

* Update ui.go

* show default values

* ensure correct default value

* MaintenanceConfig use ConfigField

* use schema defined defaults

* config

* reduce duplication

* refactor to use BaseUIProvider

* each task register its schema

* checkECEncodingCandidate use ecDetector

* use vacuumDetector

* use volumeSizeLimitMB

* remove

remove

* remove unused

* refactor

* use new framework

* remove v2 reference

* refactor

* left menu can scroll now

* The maintenance manager was not being initialized when no data directory was configured for persistent storage.

* saving config

* Update task_config_schema_templ.go

* enable/disable tasks

* protobuf encoded task configurations

* fix system settings

* use ui component

* remove logs

* interface{} Reduction

* reduce interface{}

* reduce interface{}

* avoid from/to map

* reduce interface{}

* refactor

* keep it DRY

* added logging

* debug messages

* debug level

* debug

* show the log caller line

* use configured task policy

* log level

* handle admin heartbeat response

* Update worker.go

* fix EC rack and dc count

* Report task status to admin server

* fix task logging, simplify interface checking, use erasure_coding constants

* factor in empty volume server during task planning

* volume.list adds disk id

* track disk id also

* fix locking scheduled and manual scanning

* add active topology

* simplify task detector

* ec task completed, but shards are not showing up

* implement ec in ec_typed.go

* adjust log level

* dedup

* implementing ec copying shards and only ecx files

* use disk id when distributing ec shards

🎯 Planning: ActiveTopology creates DestinationPlan with specific TargetDisk
📦 Task Creation: maintenance_integration.go creates ECDestination with DiskId
🚀 Task Execution: EC task passes DiskId in VolumeEcShardsCopyRequest
💾 Volume Server: Receives disk_id and stores shards on specific disk (vs.store.Locations[req.DiskId])
📂 File System: EC shards and metadata land in the exact disk directory planned

* Delete original volume from all locations

* clean up existing shard locations

* local encoding and distributing

* Update docker/admin_integration/EC-TESTING-README.md

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* check volume id range

* simplify

* fix tests

* fix types

* clean up logs and tests

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-07-30 12:38:03 -07:00

406 lines
No EOL
21 KiB
Text

package layout
import (
"fmt"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/seaweedfs/seaweedfs/weed/util/version"
)
templ Layout(c *gin.Context, content templ.Component) {
{{
username := c.GetString("username")
if username == "" {
username = "admin"
}
// Detect if we're on a configuration page to keep submenu expanded
currentPath := c.Request.URL.Path
isConfigPage := strings.HasPrefix(currentPath, "/maintenance/config") || currentPath == "/config"
// Detect if we're on a message queue page to keep submenu expanded
isMQPage := strings.HasPrefix(currentPath, "/mq/")
}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SeaweedFS Admin</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
<!-- Bootstrap CSS -->
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome CSS -->
<link href="/static/css/fontawesome.min.css" rel="stylesheet">
<!-- HTMX -->
<script src="/static/js/htmx.min.js"></script>
<!-- Custom CSS -->
<link rel="stylesheet" href="/static/css/admin.css">
</head>
<body>
<div class="container-fluid">
<!-- Header -->
<header class="navbar navbar-expand-lg navbar-dark bg-primary sticky-top">
<div class="container-fluid">
<a class="navbar-brand fw-bold" href="/admin">
<i class="fas fa-server me-2"></i>
SeaweedFS Admin
<span class="badge bg-warning text-dark ms-2">ALPHA</span>
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-user me-1"></i>{username}
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/logout">
<i class="fas fa-sign-out-alt me-2"></i>Logout
</a></li>
</ul>
</li>
</ul>
</div>
</div>
</header>
<div class="row g-0">
<!-- Sidebar -->
<div class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
<div class="position-sticky pt-3">
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted">
<span>MAIN</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="/admin">
<i class="fas fa-tachometer-alt me-2"></i>Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-bs-toggle="collapse" data-bs-target="#clusterSubmenu" aria-expanded="false" aria-controls="clusterSubmenu">
<i class="fas fa-sitemap me-2"></i>Cluster
<i class="fas fa-chevron-down ms-auto"></i>
</a>
<div class="collapse" id="clusterSubmenu">
<ul class="nav flex-column ms-3">
<li class="nav-item">
<a class="nav-link py-2" href="/cluster/masters">
<i class="fas fa-crown me-2"></i>Masters
</a>
</li>
<li class="nav-item">
<a class="nav-link py-2" href="/cluster/volume-servers">
<i class="fas fa-server me-2"></i>Volume Servers
</a>
</li>
<li class="nav-item">
<a class="nav-link py-2" href="/cluster/filers">
<i class="fas fa-folder-open me-2"></i>Filers
</a>
</li>
<li class="nav-item">
<a class="nav-link py-2" href="/cluster/volumes">
<i class="fas fa-database me-2"></i>Volumes
</a>
</li>
<li class="nav-item">
<a class="nav-link py-2" href="/cluster/ec-shards">
<i class="fas fa-th-large me-2"></i>EC Volumes
</a>
</li>
<li class="nav-item">
<a class="nav-link py-2" href="/cluster/collections">
<i class="fas fa-layer-group me-2"></i>Collections
</a>
</li>
</ul>
</div>
</li>
</ul>
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted">
<span>MANAGEMENT</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link" href="/files">
<i class="fas fa-folder me-2"></i>File Browser
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-bs-toggle="collapse" data-bs-target="#objectStoreSubmenu" aria-expanded="false" aria-controls="objectStoreSubmenu">
<i class="fas fa-cloud me-2"></i>Object Store
<i class="fas fa-chevron-down ms-auto"></i>
</a>
<div class="collapse" id="objectStoreSubmenu">
<ul class="nav flex-column ms-3">
<li class="nav-item">
<a class="nav-link py-2" href="/object-store/buckets">
<i class="fas fa-cube me-2"></i>Buckets
</a>
</li>
<li class="nav-item">
<a class="nav-link py-2" href="/object-store/users">
<i class="fas fa-users me-2"></i>Users
</a>
</li>
<li class="nav-item">
<a class="nav-link py-2" href="/object-store/policies">
<i class="fas fa-shield-alt me-2"></i>Policies
</a>
</li>
</ul>
</div>
</li>
<li class="nav-item">
if isMQPage {
<a class="nav-link" href="#" data-bs-toggle="collapse" data-bs-target="#messageQueueSubmenu" aria-expanded="true" aria-controls="messageQueueSubmenu">
<i class="fas fa-comments me-2"></i>Message Queue
<i class="fas fa-chevron-down ms-auto"></i>
</a>
} else {
<a class="nav-link collapsed" href="#" data-bs-toggle="collapse" data-bs-target="#messageQueueSubmenu" aria-expanded="false" aria-controls="messageQueueSubmenu">
<i class="fas fa-comments me-2"></i>Message Queue
<i class="fas fa-chevron-down ms-auto"></i>
</a>
}
if isMQPage {
<div class="collapse show" id="messageQueueSubmenu">
<ul class="nav flex-column ms-3">
<li class="nav-item">
if currentPath == "/mq/brokers" {
<a class="nav-link py-2 active" href="/mq/brokers">
<i class="fas fa-server me-2"></i>Brokers
</a>
} else {
<a class="nav-link py-2" href="/mq/brokers">
<i class="fas fa-server me-2"></i>Brokers
</a>
}
</li>
<li class="nav-item">
if currentPath == "/mq/topics" {
<a class="nav-link py-2 active" href="/mq/topics">
<i class="fas fa-list-alt me-2"></i>Topics
</a>
} else {
<a class="nav-link py-2" href="/mq/topics">
<i class="fas fa-list-alt me-2"></i>Topics
</a>
}
</li>
</ul>
</div>
} else {
<div class="collapse" id="messageQueueSubmenu">
<ul class="nav flex-column ms-3">
<li class="nav-item">
<a class="nav-link py-2" href="/mq/brokers">
<i class="fas fa-server me-2"></i>Brokers
</a>
</li>
<li class="nav-item">
<a class="nav-link py-2" href="/mq/topics">
<i class="fas fa-list-alt me-2"></i>Topics
</a>
</li>
</ul>
</div>
}
</li>
<li class="nav-item">
<a class="nav-link" href="/metrics">
<i class="fas fa-chart-line me-2"></i>Metrics
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/logs">
<i class="fas fa-file-alt me-2"></i>Logs
</a>
</li>
</ul>
<h6 class="sidebar-heading px-3 mt-4 mb-1 text-muted">
<span>SYSTEM</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
if isConfigPage {
<a class="nav-link" href="#" data-bs-toggle="collapse" data-bs-target="#configurationSubmenu" aria-expanded="true" aria-controls="configurationSubmenu">
<i class="fas fa-cogs me-2"></i>Configuration
<i class="fas fa-chevron-down ms-auto"></i>
</a>
} else {
<a class="nav-link collapsed" href="#" data-bs-toggle="collapse" data-bs-target="#configurationSubmenu" aria-expanded="false" aria-controls="configurationSubmenu">
<i class="fas fa-cogs me-2"></i>Configuration
<i class="fas fa-chevron-right ms-auto"></i>
</a>
}
if isConfigPage {
<div class="collapse show" id="configurationSubmenu">
<ul class="nav flex-column ms-3">
for _, menuItem := range GetConfigurationMenuItems() {
{{
isActiveItem := currentPath == menuItem.URL
}}
<li class="nav-item">
if isActiveItem {
<a class="nav-link py-2 active" href={templ.SafeURL(menuItem.URL)}>
<i class={menuItem.Icon + " me-2"}></i>{menuItem.Name}
</a>
} else {
<a class="nav-link py-2" href={templ.SafeURL(menuItem.URL)}>
<i class={menuItem.Icon + " me-2"}></i>{menuItem.Name}
</a>
}
</li>
}
</ul>
</div>
} else {
<div class="collapse" id="configurationSubmenu">
<ul class="nav flex-column ms-3">
for _, menuItem := range GetConfigurationMenuItems() {
<li class="nav-item">
<a class="nav-link py-2" href={templ.SafeURL(menuItem.URL)}>
<i class={menuItem.Icon + " me-2"}></i>{menuItem.Name}
</a>
</li>
}
</ul>
</div>
}
</li>
<li class="nav-item">
if currentPath == "/maintenance" {
<a class="nav-link active" href="/maintenance">
<i class="fas fa-list me-2"></i>Maintenance Queue
</a>
} else {
<a class="nav-link" href="/maintenance">
<i class="fas fa-list me-2"></i>Maintenance Queue
</a>
}
</li>
<li class="nav-item">
if currentPath == "/maintenance/workers" {
<a class="nav-link active" href="/maintenance/workers">
<i class="fas fa-user-cog me-2"></i>Maintenance Workers
</a>
} else {
<a class="nav-link" href="/maintenance/workers">
<i class="fas fa-user-cog me-2"></i>Maintenance Workers
</a>
}
</li>
</ul>
</div>
</div>
<!-- Main content -->
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<div class="pt-3">
@content
</div>
</main>
</div>
</div>
<!-- Footer -->
<footer class="footer mt-auto py-3 bg-light">
<div class="container-fluid text-center">
<small class="text-muted">
&copy; {fmt.Sprintf("%d", time.Now().Year())} SeaweedFS Admin v{version.VERSION_NUMBER}
if !strings.Contains(version.VERSION, "enterprise") {
<span class="mx-2">•</span>
<a href="https://seaweedfs.com" target="_blank" class="text-decoration-none">
<i class="fas fa-star me-1"></i>Enterprise Version Available
</a>
}
</small>
</div>
</footer>
<!-- Bootstrap JS -->
<script src="/static/js/bootstrap.bundle.min.js"></script>
<!-- Custom JS -->
<script src="/static/js/admin.js"></script>
</body>
</html>
}
templ LoginForm(c *gin.Context, title string, errorMessage string) {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{title} - Login</title>
<link rel="icon" href="/static/favicon.ico" type="image/x-icon">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/css/bootstrap.min.css" rel="stylesheet">
<link href="/static/css/fontawesome.min.css" rel="stylesheet">
</head>
<body class="bg-light">
<div class="container">
<div class="row justify-content-center min-vh-100 align-items-center">
<div class="col-md-6 col-lg-4">
<div class="card shadow">
<div class="card-body p-5">
<div class="text-center mb-4">
<i class="fas fa-server fa-3x text-primary mb-3"></i>
<h4 class="card-title">{title}</h4>
<p class="text-muted">Please sign in to continue</p>
</div>
if errorMessage != "" {
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-triangle me-2"></i>
{errorMessage}
</div>
}
<form method="POST" action="/login">
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-user"></i>
</span>
<input type="text" class="form-control" id="username" name="username" required>
</div>
</div>
<div class="mb-4">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-lock"></i>
</span>
<input type="password" class="form-control" id="password" name="password" required>
</div>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-sign-in-alt me-2"></i>Sign In
</button>
</form>
</div>
</div>
</div>
</div>
</div>
<script src="/static/js/bootstrap.bundle.min.js"></script>
</body>
</html>
}