* add ui for maintenance * valid config loading. fix workers page. * refactor * grpc between admin and workers * add a long-running bidirectional grpc call between admin and worker * use the grpc call to heartbeat * use the grpc call to communicate * worker can remove the http client * admin uses http port + 10000 as its default grpc port * one task one package * handles connection failures gracefully with exponential backoff * grpc with insecure tls * grpc with optional tls * fix detecting tls * change time config from nano seconds to seconds * add tasks with 3 interfaces * compiles reducing hard coded * remove a couple of tasks * remove hard coded references * reduce hard coded values * remove hard coded values * remove hard coded from templ * refactor maintenance package * fix import cycle * simplify * simplify * auto register * auto register factory * auto register task types * self register types * refactor * simplify * remove one task * register ui * lazy init executor factories * use registered task types * DefaultWorkerConfig remove hard coded task types * remove more hard coded * implement get maintenance task * dynamic task configuration * "System Settings" should only have system level settings * adjust menu for tasks * ensure menu not collapsed * render job configuration well * use templ for ui of task configuration * fix ordering * fix bugs * saving duration in seconds * use value and unit for duration * Delete WORKER_REFACTORING_PLAN.md * Delete maintenance.json * Delete custom_worker_example.go * remove address from workers * remove old code from ec task * remove creating collection button * reconnect with exponential backoff * worker use security.toml * start admin server with tls info from security.toml * fix "weed admin" cli description
265 lines
12 KiB
Plaintext
265 lines
12 KiB
Plaintext
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
|
|
)
|
|
|
|
templ ClusterCollections(data dash.ClusterCollectionsData) {
|
|
<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="fas fa-layer-group me-2"></i>Cluster Collections
|
|
</h1>
|
|
<div class="btn-toolbar mb-2 mb-md-0">
|
|
<div class="btn-group me-2">
|
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="exportCollections()">
|
|
<i class="fas fa-download me-1"></i>Export
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="collections-content">
|
|
<!-- Summary Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-xl-3 col-md-6 mb-4">
|
|
<div class="card border-left-primary shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
|
Total Collections
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{fmt.Sprintf("%d", data.TotalCollections)}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-layer-group fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-3 col-md-6 mb-4">
|
|
<div class="card border-left-info shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
|
|
Total Volumes
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{fmt.Sprintf("%d", data.TotalVolumes)}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-database fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-3 col-md-6 mb-4">
|
|
<div class="card border-left-warning shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
|
|
Total Files
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{fmt.Sprintf("%d", data.TotalFiles)}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-file fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-xl-3 col-md-6 mb-4">
|
|
<div class="card border-left-secondary shadow h-100 py-2">
|
|
<div class="card-body">
|
|
<div class="row no-gutters align-items-center">
|
|
<div class="col mr-2">
|
|
<div class="text-xs font-weight-bold text-secondary text-uppercase mb-1">
|
|
Total Storage Size
|
|
</div>
|
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
|
{formatBytes(data.TotalSize)}
|
|
</div>
|
|
</div>
|
|
<div class="col-auto">
|
|
<i class="fas fa-hdd fa-2x text-gray-300"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Collections Table -->
|
|
<div class="card shadow mb-4">
|
|
<div class="card-header py-3">
|
|
<h6 class="m-0 font-weight-bold text-primary">
|
|
<i class="fas fa-layer-group me-2"></i>Collection Details
|
|
</h6>
|
|
</div>
|
|
<div class="card-body">
|
|
if len(data.Collections) > 0 {
|
|
<div class="table-responsive">
|
|
<table class="table table-hover" id="collectionsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Collection Name</th>
|
|
<th>Volumes</th>
|
|
<th>Files</th>
|
|
<th>Size</th>
|
|
<th>Disk Types</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
for _, collection := range data.Collections {
|
|
<tr>
|
|
<td>
|
|
<a href={templ.SafeURL(fmt.Sprintf("/cluster/volumes?collection=%s", collection.Name))} class="text-decoration-none">
|
|
<strong>{collection.Name}</strong>
|
|
</a>
|
|
</td>
|
|
<td>
|
|
<a href={templ.SafeURL(fmt.Sprintf("/cluster/volumes?collection=%s", collection.Name))} class="text-decoration-none">
|
|
<div class="d-flex align-items-center">
|
|
<i class="fas fa-database me-2 text-muted"></i>
|
|
{fmt.Sprintf("%d", collection.VolumeCount)}
|
|
</div>
|
|
</a>
|
|
</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<i class="fas fa-file me-2 text-muted"></i>
|
|
{fmt.Sprintf("%d", collection.FileCount)}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<i class="fas fa-hdd me-2 text-muted"></i>
|
|
{formatBytes(collection.TotalSize)}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
for i, diskType := range collection.DiskTypes {
|
|
if i > 0 {
|
|
<span class="me-1"></span>
|
|
}
|
|
<span class={fmt.Sprintf("badge bg-%s me-1", getDiskTypeColor(diskType))}>{diskType}</span>
|
|
}
|
|
if len(collection.DiskTypes) == 0 {
|
|
<span class="text-muted">Unknown</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<button type="button" class="btn btn-outline-primary btn-sm"
|
|
title="View Details">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
|
title="Edit">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger btn-sm"
|
|
title="Delete"
|
|
data-collection-name={collection.Name}
|
|
onclick="confirmDeleteCollection(this)">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
} else {
|
|
<div class="text-center py-5">
|
|
<i class="fas fa-layer-group fa-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">No Collections Found</h5>
|
|
<p class="text-muted">No collections are currently configured in the cluster.</p>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Last Updated -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<small class="text-muted">
|
|
<i class="fas fa-clock me-1"></i>
|
|
Last updated: {data.LastUpdated.Format("2006-01-02 15:04:05")}
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
<div class="modal fade" id="deleteCollectionModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title text-danger">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>Delete Collection
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Are you sure you want to delete the collection <strong id="deleteCollectionName"></strong>?</p>
|
|
<div class="alert alert-warning">
|
|
<i class="fas fa-warning me-2"></i>
|
|
This action cannot be undone. All volumes in this collection will be affected.
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-danger" id="confirmDeleteCollection">Delete Collection</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
func getDiskTypeColor(diskType string) string {
|
|
switch diskType {
|
|
case "ssd":
|
|
return "primary"
|
|
case "hdd", "":
|
|
return "secondary"
|
|
default:
|
|
return "info"
|
|
}
|
|
}
|
|
|
|
func formatDiskTypes(diskTypes []string) string {
|
|
if len(diskTypes) == 0 {
|
|
return "Unknown"
|
|
}
|
|
if len(diskTypes) == 1 {
|
|
return diskTypes[0]
|
|
}
|
|
// For multiple disk types, join with comma
|
|
result := ""
|
|
for i, diskType := range diskTypes {
|
|
if i > 0 {
|
|
result += ", "
|
|
}
|
|
result += diskType
|
|
}
|
|
return result
|
|
} |