mirror of
https://github.com/chrislusf/seaweedfs
synced 2025-07-04 18:52:47 +02:00
318 lines
No EOL
16 KiB
Text
318 lines
No EOL
16 KiB
Text
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>
|
|
<button type="button" class="btn btn-sm btn-success" data-bs-toggle="modal" data-bs-target="#createCollectionModal">
|
|
<i class="fas fa-plus me-1"></i>Create Collection
|
|
</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>
|
|
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createCollectionModal">
|
|
<i class="fas fa-plus me-2"></i>Create First Collection
|
|
</button>
|
|
</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>
|
|
|
|
<!-- Create Collection Modal -->
|
|
<div class="modal fade" id="createCollectionModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-plus me-2"></i>Create New Collection
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<form id="createCollectionForm">
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label for="collectionName" class="form-label">Collection Name</label>
|
|
<input type="text" class="form-control" id="collectionName" name="name" required>
|
|
<div class="form-text">Enter a unique name for the collection</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="replication" class="form-label">Replication</label>
|
|
<select class="form-select" id="replication" name="replication" required>
|
|
<option value="000">000 - No replication</option>
|
|
<option value="001" selected>001 - Replicate once on same rack</option>
|
|
<option value="010">010 - Replicate once on different rack</option>
|
|
<option value="100">100 - Replicate once on different data center</option>
|
|
<option value="200">200 - Replicate twice on different data centers</option>
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="ttl" class="form-label">TTL (Time To Live)</label>
|
|
<input type="text" class="form-control" id="ttl" name="ttl" placeholder="e.g., 1d, 7d, 30d">
|
|
<div class="form-text">Optional: Specify how long files should be kept</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="diskType" class="form-label">Disk Type</label>
|
|
<select class="form-select" id="diskType" name="diskType">
|
|
<option value="hdd" selected>HDD</option>
|
|
<option value="ssd">SSD</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" class="btn btn-primary">Create Collection</button>
|
|
</div>
|
|
</form>
|
|
</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
|
|
} |