1
0
Fork 0
mirror of https://github.com/chrislusf/seaweedfs synced 2025-07-26 05:22:46 +02:00
seaweedfs/weed/admin/view/app/cluster_volumes.templ
Chris Lu 1defee3d68
Add admin component (#6928)
* init version

* relocate

* add s3 bucket link

* refactor handlers into weed/admin folder

* fix login logout

* adding favicon

* remove fall back to http get topology

* grpc dial option, disk total capacity

* show filer count

* fix each volume disk usage

* add filers to dashboard

* adding hosts, volumes, collections

* refactor code and menu

* remove "refresh" button

* fix data for collections

* rename cluster hosts into volume servers

* add masters, filers

* reorder

* adding file browser

* create folder and upload files

* add filer version, created at time

* remove mock data

* remove fields

* fix submenu item highlighting

* fix bucket creation

* purge files

* delete multiple

* fix bucket creation

* remove region from buckets

* add object store with buckets and users

* rendering permission

* refactor

* get bucket objects and size

* link to file browser

* add file size and count for collections page

* paginate the volumes

* fix possible SSRF

https://github.com/seaweedfs/seaweedfs/pull/6928/checks?check_run_id=45108469801

* Update weed/command/admin.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update weed/command/admin.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix build

* import

* remove filer CLI option

* remove filer option

* remove CLI options

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-01 01:28:09 -07:00

414 lines
No EOL
20 KiB
Text

package app
import (
"fmt"
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
)
templ ClusterVolumes(data dash.ClusterVolumesData) {
<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-database me-2"></i>Cluster Volumes
</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<select class="form-select form-select-sm me-2" id="pageSizeSelect" onchange="changePageSize()" style="width: auto;">
<option value="50" if data.PageSize == 50 { selected="selected" }>50 per page</option>
<option value="100" if data.PageSize == 100 { selected="selected" }>100 per page</option>
<option value="200" if data.PageSize == 200 { selected="selected" }>200 per page</option>
<option value="500" if data.PageSize == 500 { selected="selected" }>500 per page</option>
</select>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="exportVolumes()">
<i class="fas fa-download me-1"></i>Export
</button>
</div>
</div>
</div>
<div id="volumes-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 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-success 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-success text-uppercase mb-1">
Active Volumes
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{fmt.Sprintf("%d", countActiveVolumes(data.Volumes))}
</div>
</div>
<div class="col-auto">
<i class="fas fa-check-circle 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">
Data Centers
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{fmt.Sprintf("%d", countUniqueDataCenters(data.Volumes))}
</div>
</div>
<div class="col-auto">
<i class="fas fa-building 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 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>
<!-- Volumes 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-database me-2"></i>Volume Details
</h6>
</div>
<div class="card-body">
if len(data.Volumes) > 0 {
<div class="table-responsive">
<table class="table table-hover" id="volumesTable">
<thead>
<tr>
<th>
<a href="#" onclick="sortTable('id')" class="text-decoration-none text-dark">
Volume ID
@getSortIcon("id", data.SortBy, data.SortOrder)
</a>
</th>
<th>
<a href="#" onclick="sortTable('server')" class="text-decoration-none text-dark">
Server
@getSortIcon("server", data.SortBy, data.SortOrder)
</a>
</th>
<th>
<a href="#" onclick="sortTable('datacenter')" class="text-decoration-none text-dark">
Data Center
@getSortIcon("datacenter", data.SortBy, data.SortOrder)
</a>
</th>
<th>
<a href="#" onclick="sortTable('rack')" class="text-decoration-none text-dark">
Rack
@getSortIcon("rack", data.SortBy, data.SortOrder)
</a>
</th>
<th>
<a href="#" onclick="sortTable('collection')" class="text-decoration-none text-dark">
Collection
@getSortIcon("collection", data.SortBy, data.SortOrder)
</a>
</th>
<th>
<a href="#" onclick="sortTable('size')" class="text-decoration-none text-dark">
Size
@getSortIcon("size", data.SortBy, data.SortOrder)
</a>
</th>
<th>
<a href="#" onclick="sortTable('filecount')" class="text-decoration-none text-dark">
File Count
@getSortIcon("filecount", data.SortBy, data.SortOrder)
</a>
</th>
<th>
<a href="#" onclick="sortTable('replication')" class="text-decoration-none text-dark">
Replication
@getSortIcon("replication", data.SortBy, data.SortOrder)
</a>
</th>
<th>
<a href="#" onclick="sortTable('status')" class="text-decoration-none text-dark">
Status
@getSortIcon("status", data.SortBy, data.SortOrder)
</a>
</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
for _, volume := range data.Volumes {
<tr>
<td>
<code>{fmt.Sprintf("%d", volume.ID)}</code>
</td>
<td>
<a href={templ.SafeURL(fmt.Sprintf("http://%s", volume.Server))} target="_blank" class="text-decoration-none">
{volume.Server}
<i class="fas fa-external-link-alt ms-1 text-muted"></i>
</a>
</td>
<td>
<span class="badge bg-light text-dark">{volume.DataCenter}</span>
</td>
<td>
<span class="badge bg-light text-dark">{volume.Rack}</span>
</td>
<td>
<span class="badge bg-secondary">{volume.Collection}</span>
</td>
<td>{formatBytes(volume.Size)}</td>
<td>{fmt.Sprintf("%d", volume.FileCount)}</td>
<td>
<span class="badge bg-info">{volume.Replication}</span>
</td>
<td>
<span class={fmt.Sprintf("badge bg-%s", getStatusColor(volume.Status))}>
{volume.Status}
</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="Compact">
<i class="fas fa-compress-alt"></i>
</button>
<button type="button" class="btn btn-outline-warning btn-sm"
title="Fix">
<i class="fas fa-wrench"></i>
</button>
</div>
</td>
</tr>
}
</tbody>
</table>
</div>
<!-- Volume Summary -->
<div class="d-flex justify-content-between align-items-center mt-3">
<div>
<small class="text-muted">
Showing {fmt.Sprintf("%d", (data.CurrentPage-1)*data.PageSize + 1)} to {fmt.Sprintf("%d", minInt(data.CurrentPage*data.PageSize, data.TotalVolumes))} of {fmt.Sprintf("%d", data.TotalVolumes)} volumes
</small>
</div>
if data.TotalPages > 1 {
<div>
<small class="text-muted">
Page {fmt.Sprintf("%d", data.CurrentPage)} of {fmt.Sprintf("%d", data.TotalPages)}
</small>
</div>
}
</div>
<!-- Pagination Controls -->
if data.TotalPages > 1 {
<div class="d-flex justify-content-center mt-3">
<nav aria-label="Volumes pagination">
<ul class="pagination pagination-sm mb-0">
<!-- Previous Button -->
if data.CurrentPage > 1 {
<li class="page-item">
<a class="page-link pagination-link" href="#" data-page={fmt.Sprintf("%d", data.CurrentPage-1)}>
<i class="fas fa-chevron-left"></i>
</a>
</li>
} else {
<li class="page-item disabled">
<span class="page-link">
<i class="fas fa-chevron-left"></i>
</span>
</li>
}
<!-- Page Numbers -->
for i := maxInt(1, data.CurrentPage-2); i <= minInt(data.TotalPages, data.CurrentPage+2); i++ {
if i == data.CurrentPage {
<li class="page-item active">
<span class="page-link">{fmt.Sprintf("%d", i)}</span>
</li>
} else {
<li class="page-item">
<a class="page-link pagination-link" href="#" data-page={fmt.Sprintf("%d", i)}>{fmt.Sprintf("%d", i)}</a>
</li>
}
}
<!-- Next Button -->
if data.CurrentPage < data.TotalPages {
<li class="page-item">
<a class="page-link pagination-link" href="#" data-page={fmt.Sprintf("%d", data.CurrentPage+1)}>
<i class="fas fa-chevron-right"></i>
</a>
</li>
} else {
<li class="page-item disabled">
<span class="page-link">
<i class="fas fa-chevron-right"></i>
</span>
</li>
}
</ul>
</nav>
</div>
}
} else {
<div class="text-center py-5">
<i class="fas fa-database fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No Volumes Found</h5>
<p class="text-muted">No volumes are currently available 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>
<!-- JavaScript for pagination and sorting -->
<script>
// Initialize pagination links when page loads
document.addEventListener('DOMContentLoaded', function() {
// Add click handlers to pagination links
document.querySelectorAll('.pagination-link').forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const page = this.getAttribute('data-page');
goToPage(page);
});
});
});
function goToPage(page) {
const url = new URL(window.location);
url.searchParams.set('page', page);
window.location.href = url.toString();
}
function changePageSize() {
const pageSize = document.getElementById('pageSizeSelect').value;
const url = new URL(window.location);
url.searchParams.set('pageSize', pageSize);
url.searchParams.set('page', '1'); // Reset to first page
window.location.href = url.toString();
}
function sortTable(column) {
const url = new URL(window.location);
const currentSort = url.searchParams.get('sortBy');
const currentOrder = url.searchParams.get('sortOrder') || 'asc';
let newOrder = 'asc';
if (currentSort === column && currentOrder === 'asc') {
newOrder = 'desc';
}
url.searchParams.set('sortBy', column);
url.searchParams.set('sortOrder', newOrder);
url.searchParams.set('page', '1'); // Reset to first page
window.location.href = url.toString();
}
function exportVolumes() {
// TODO: Implement volume export functionality
alert('Export functionality to be implemented');
}
</script>
}
func countActiveVolumes(volumes []dash.VolumeInfo) int {
count := 0
for _, volume := range volumes {
if volume.Status == "active" {
count++
}
}
return count
}
func countUniqueDataCenters(volumes []dash.VolumeInfo) int {
dcMap := make(map[string]bool)
for _, volume := range volumes {
dcMap[volume.DataCenter] = true
}
return len(dcMap)
}
templ getSortIcon(column, currentSort, currentOrder string) {
if column != currentSort {
<i class="fas fa-sort text-muted ms-1"></i>
} else if currentOrder == "asc" {
<i class="fas fa-sort-up text-primary ms-1"></i>
} else {
<i class="fas fa-sort-down text-primary ms-1"></i>
}
}
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}