Browse Source
Add admin component (#6928)
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>testing-sdx-generation
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 13095 additions and 14 deletions
-
42Makefile
-
16go.mod
-
31go.sum
-
321weed/admin/DESIGN.md
-
165weed/admin/Makefile
-
96weed/admin/NAVIGATION_TEST.md
-
279weed/admin/README.md
-
174weed/admin/S3_BUCKETS.md
-
247weed/admin/admin.go
-
1146weed/admin/dash/admin_server.go
-
350weed/admin/dash/file_browser.go
-
373weed/admin/dash/handler_admin.go
-
128weed/admin/dash/handler_auth.go
-
27weed/admin/dash/middleware.go
-
45weed/admin/handlers/auth.go
-
202weed/admin/handlers/cluster_handlers.go
-
447weed/admin/handlers/file_browser_handlers.go
-
320weed/admin/handlers/handlers.go
-
217weed/admin/static/css/admin.css
-
BINweed/admin/static/favicon.ico
-
1576weed/admin/static/js/admin.js
-
351weed/admin/view/app/admin.templ
-
555weed/admin/view/app/admin_templ.go
-
360weed/admin/view/app/cluster_collections.templ
-
346weed/admin/view/app/cluster_collections_templ.go
-
163weed/admin/view/app/cluster_filers.templ
-
252weed/admin/view/app/cluster_filers_templ.go
-
209weed/admin/view/app/cluster_masters.templ
-
247weed/admin/view/app/cluster_masters_templ.go
-
221weed/admin/view/app/cluster_volume_servers.templ
-
306weed/admin/view/app/cluster_volume_servers_templ.go
-
414weed/admin/view/app/cluster_volumes.templ
-
661weed/admin/view/app/cluster_volumes_templ.go
-
438weed/admin/view/app/file_browser.templ
-
607weed/admin/view/app/file_browser_templ.go
-
214weed/admin/view/app/object_store_users.templ
-
237weed/admin/view/app/object_store_users_templ.go
-
302weed/admin/view/app/s3_buckets.templ
-
277weed/admin/view/app/s3_buckets_templ.go
-
84weed/admin/view/app/template_helpers.go
-
263weed/admin/view/layout/layout.templ
-
163weed/admin/view/layout/layout_templ.go
-
236weed/command/admin.go
-
1weed/command/command.go
@ -0,0 +1,321 @@ |
|||
# SeaweedFS Admin Interface Web Component Design |
|||
|
|||
## Overview |
|||
|
|||
The SeaweedFS Admin Interface is a modern web-based administration interface for SeaweedFS clusters, following the **Gin + Templ + HTMX** architecture pattern. It provides comprehensive cluster management, monitoring, and maintenance capabilities through an intuitive web interface. |
|||
|
|||
## Architecture |
|||
|
|||
### Technology Stack |
|||
|
|||
- **Backend Framework**: Gin (Go HTTP web framework) |
|||
- **Template Engine**: Templ (Type-safe Go templates) |
|||
- **Frontend Enhancement**: HTMX (Dynamic interactions without JavaScript frameworks) |
|||
- **CSS Framework**: Bootstrap 5 (Modern responsive design) |
|||
- **Icons**: Font Awesome 6 (Comprehensive icon library) |
|||
- **Authentication**: Session-based with configurable credentials |
|||
|
|||
### Directory Structure |
|||
|
|||
``` |
|||
weed/admin/ |
|||
├── admin.go # Main entry point & router setup |
|||
├── dash/ # Core admin logic |
|||
│ ├── admin_server.go # Server struct & cluster operations |
|||
│ ├── handler_auth.go # Authentication handlers |
|||
│ ├── handler_admin.go # Main admin handlers |
|||
│ ├── middleware.go # Authentication middleware |
|||
│ └── ... # Additional handlers |
|||
├── view/ # Template components |
|||
│ ├── layout/ |
|||
│ │ └── layout.templ # Base layout & login form |
|||
│ └── app/ |
|||
│ ├── admin.templ # Admin page template |
|||
│ └── template_helpers.go # Formatting utilities |
|||
├── static/ # Static assets |
|||
│ ├── css/ |
|||
│ │ └── admin.css # Custom styles |
|||
│ └── js/ |
|||
│ └── admin.js # Interactive functionality |
|||
└── templates/ # Embedded templates |
|||
``` |
|||
|
|||
## Core Features |
|||
|
|||
### 1. **Cluster Management** |
|||
|
|||
#### Topology Visualization |
|||
- **Data Center/Rack/Node Hierarchy**: Visual representation of cluster topology |
|||
- **Real-time Status Monitoring**: Live status updates for all cluster components |
|||
- **Capacity Planning**: Volume utilization and capacity tracking |
|||
- **Health Assessment**: Automated health scoring and alerts |
|||
|
|||
#### Master Node Management |
|||
- **Leader/Follower Status**: Clear indication of Raft leadership |
|||
- **Master Configuration**: View and modify master settings |
|||
- **Cluster Membership**: Add/remove master nodes |
|||
- **Heartbeat Monitoring**: Track master node availability |
|||
|
|||
#### Volume Server Operations |
|||
- **Server Registration**: Automatic detection of new volume servers |
|||
- **Disk Usage Monitoring**: Real-time disk space and volume tracking |
|||
- **Performance Metrics**: I/O statistics and throughput monitoring |
|||
- **Maintenance Mode**: Graceful server shutdown and maintenance |
|||
|
|||
### 2. **Volume Management** |
|||
|
|||
#### Volume Operations |
|||
- **Volume Creation**: Create new volumes with replication settings |
|||
- **Volume Listing**: Comprehensive volume inventory with search/filter |
|||
- **Volume Details**: Detailed information per volume (files, size, replicas) |
|||
- **Volume Migration**: Move volumes between servers |
|||
- **Volume Deletion**: Safe volume removal with confirmation |
|||
|
|||
#### Storage Operations |
|||
- **Volume Growing**: Automatic volume expansion based on policies |
|||
- **Vacuum Operations**: Reclaim deleted file space |
|||
- **Compaction**: Optimize volume storage efficiency |
|||
- **Rebalancing**: Distribute volumes evenly across servers |
|||
|
|||
### 3. **File Management** |
|||
|
|||
#### File Browser |
|||
- **Directory Navigation**: Browse filer directories with breadcrumbs |
|||
- **File Operations**: Upload, download, delete, rename files |
|||
- **Batch Operations**: Multi-file operations with progress tracking |
|||
- **Metadata Display**: File attributes, timestamps, permissions |
|||
- **Search Functionality**: Find files by name, type, or content |
|||
|
|||
#### Storage Analytics |
|||
- **Usage Statistics**: File count, size distribution, growth trends |
|||
- **Access Patterns**: Popular files and access frequency |
|||
- **Storage Efficiency**: Compression ratios and duplicate detection |
|||
|
|||
### 4. **Monitoring & Metrics** |
|||
|
|||
#### Real-time Dashboards |
|||
- **System Overview**: Cluster health at a glance |
|||
- **Performance Metrics**: Throughput, latency, and error rates |
|||
- **Resource Utilization**: CPU, memory, disk, and network usage |
|||
- **Historical Trends**: Long-term performance analysis |
|||
|
|||
#### Alerting System |
|||
- **Threshold Monitoring**: Configurable alerts for key metrics |
|||
- **Health Checks**: Automated health assessment and scoring |
|||
- **Notification Channels**: Email, webhook, and dashboard notifications |
|||
|
|||
### 5. **Configuration Management** |
|||
|
|||
#### Cluster Configuration |
|||
- **Master Settings**: Replication, security, and operational parameters |
|||
- **Volume Server Config**: Storage paths, limits, and performance settings |
|||
- **Filer Configuration**: Metadata storage and caching options |
|||
- **Security Settings**: Authentication, authorization, and encryption |
|||
|
|||
#### Backup & Restore |
|||
- **Configuration Backup**: Export cluster configuration |
|||
- **Configuration Restore**: Import and apply saved configurations |
|||
- **Version Control**: Track configuration changes over time |
|||
|
|||
### 6. **System Maintenance** |
|||
|
|||
#### Maintenance Operations |
|||
- **Garbage Collection**: Clean up orphaned files and metadata |
|||
- **Volume Repair**: Fix corrupted or inconsistent volumes |
|||
- **Cluster Validation**: Verify cluster integrity and consistency |
|||
- **Performance Tuning**: Optimize cluster performance parameters |
|||
|
|||
#### Log Management |
|||
- **Log Aggregation**: Centralized logging from all cluster components |
|||
- **Log Analysis**: Search, filter, and analyze system logs |
|||
- **Error Tracking**: Identify and track system errors and warnings |
|||
- **Log Export**: Download logs for external analysis |
|||
|
|||
## User Interface Design |
|||
|
|||
### Layout Components |
|||
|
|||
#### Header Navigation |
|||
- **Cluster Status Indicator**: Quick health overview |
|||
- **User Information**: Current user and session details |
|||
- **Quick Actions**: Frequently used operations |
|||
- **Search Bar**: Global search across cluster resources |
|||
|
|||
#### Sidebar Navigation |
|||
- **Cluster Section**: Topology, status, and management |
|||
- **Management Section**: Files, volumes, and operations |
|||
- **System Section**: Configuration, logs, and maintenance |
|||
- **Contextual Actions**: Dynamic actions based on current view |
|||
|
|||
#### Main Content Area |
|||
- **Dashboard Cards**: Key metrics and status summaries |
|||
- **Data Tables**: Sortable, filterable resource listings |
|||
- **Interactive Charts**: Real-time metrics visualization |
|||
- **Action Panels**: Operation forms and bulk actions |
|||
|
|||
### Responsive Design |
|||
- **Mobile Responsive**: Optimized for tablets and mobile devices |
|||
- **Progressive Enhancement**: Works with JavaScript disabled |
|||
- **Accessibility**: WCAG 2.1 compliant interface |
|||
- **Theme Support**: Light/dark mode support |
|||
|
|||
## Security Features |
|||
|
|||
### Authentication & Authorization |
|||
- **Configurable Authentication**: Optional password protection |
|||
- **Session Management**: Secure session handling with timeouts |
|||
- **Role-based Access**: Different permission levels for users |
|||
- **Audit Logging**: Track all administrative actions |
|||
|
|||
### Security Hardening |
|||
- **HTTPS Support**: TLS encryption for all communications |
|||
- **CSRF Protection**: Cross-site request forgery prevention |
|||
- **Input Validation**: Comprehensive input sanitization |
|||
- **Rate Limiting**: Prevent abuse and DoS attacks |
|||
|
|||
## API Design |
|||
|
|||
### RESTful Endpoints |
|||
```go |
|||
// Public endpoints |
|||
GET /health # Health check |
|||
GET /login # Login form |
|||
POST /login # Process login |
|||
GET /logout # Logout |
|||
|
|||
// Protected endpoints |
|||
GET /admin # Main admin interface |
|||
GET /overview # Cluster overview API |
|||
|
|||
// Cluster management |
|||
GET /cluster # Cluster topology view |
|||
GET /cluster/topology # Topology data API |
|||
GET /cluster/status # Cluster status API |
|||
POST /cluster/grow # Grow volumes |
|||
POST /cluster/vacuum # Vacuum operation |
|||
POST /cluster/rebalance # Rebalance cluster |
|||
|
|||
// Volume management |
|||
GET /volumes # Volumes list page |
|||
GET /volumes/list # Volumes data API |
|||
GET /volumes/details/:id # Volume details |
|||
POST /volumes/create # Create volume |
|||
DELETE /volumes/delete/:id # Delete volume |
|||
|
|||
// File management |
|||
GET /filer # File browser page |
|||
GET /filer/browser # File browser interface |
|||
GET /filer/browser/api/* # File operations API |
|||
POST /filer/upload # File upload |
|||
DELETE /filer/delete # File deletion |
|||
|
|||
// Monitoring |
|||
GET /metrics # Metrics dashboard |
|||
GET /metrics/data # Metrics data API |
|||
GET /metrics/realtime # Real-time metrics |
|||
GET /logs # Logs viewer |
|||
GET /logs/download/:type # Download logs |
|||
|
|||
// Configuration |
|||
GET /config # Configuration page |
|||
GET /config/current # Current configuration |
|||
POST /config/update # Update configuration |
|||
GET /config/backup # Backup configuration |
|||
|
|||
// Maintenance |
|||
GET /maintenance # Maintenance page |
|||
POST /maintenance/gc # Garbage collection |
|||
POST /maintenance/compact # Volume compaction |
|||
GET /maintenance/status # Maintenance status |
|||
``` |
|||
|
|||
## Development Guidelines |
|||
|
|||
### Code Organization |
|||
- **Handler Separation**: Separate files for different functional areas |
|||
- **Type Safety**: Use strongly typed structures for all data |
|||
- **Error Handling**: Comprehensive error handling and user feedback |
|||
- **Testing**: Unit and integration tests for all components |
|||
|
|||
### Performance Considerations |
|||
- **Caching Strategy**: Intelligent caching of cluster topology and metrics |
|||
- **Lazy Loading**: Load data on demand to improve responsiveness |
|||
- **Batch Operations**: Efficient bulk operations for large datasets |
|||
- **Compression**: Gzip compression for API responses |
|||
|
|||
### Monitoring Integration |
|||
- **Metrics Export**: Prometheus-compatible metrics endpoint |
|||
- **Health Checks**: Kubernetes-style health and readiness probes |
|||
- **Distributed Tracing**: OpenTelemetry integration for request tracing |
|||
- **Structured Logging**: JSON logging for better observability |
|||
|
|||
## Deployment Options |
|||
|
|||
### Standalone Deployment |
|||
```bash |
|||
# Start dashboard server |
|||
./weed dashboard -port=9999 \ |
|||
-masters="master1:9333,master2:9333" \ |
|||
-filer="filer:8888" \ |
|||
-adminUser="admin" \ |
|||
-adminPassword="secretpassword" |
|||
``` |
|||
|
|||
### Docker Deployment |
|||
```yaml |
|||
# docker-compose.yml |
|||
version: '3.8' |
|||
services: |
|||
dashboard: |
|||
image: seaweedfs:latest |
|||
command: dashboard -port=9999 -masters=master:9333 -filer=filer:8888 |
|||
ports: |
|||
- "9999:9999" |
|||
environment: |
|||
- ADMIN_USER=admin |
|||
- ADMIN_PASSWORD=secretpassword |
|||
``` |
|||
|
|||
### Kubernetes Deployment |
|||
```yaml |
|||
apiVersion: apps/v1 |
|||
kind: Deployment |
|||
metadata: |
|||
name: seaweedfs-dashboard |
|||
spec: |
|||
replicas: 1 |
|||
selector: |
|||
matchLabels: |
|||
app: seaweedfs-dashboard |
|||
template: |
|||
metadata: |
|||
labels: |
|||
app: seaweedfs-dashboard |
|||
spec: |
|||
containers: |
|||
- name: dashboard |
|||
image: seaweedfs:latest |
|||
command: ["weed", "dashboard"] |
|||
args: |
|||
- "-port=9999" |
|||
- "-masters=seaweedfs-master:9333" |
|||
- "-filer=seaweedfs-filer:8888" |
|||
ports: |
|||
- containerPort: 9999 |
|||
``` |
|||
|
|||
## Future Enhancements |
|||
|
|||
### Advanced Features |
|||
- **Multi-cluster Management**: Manage multiple SeaweedFS clusters |
|||
- **Advanced Analytics**: Machine learning-powered insights |
|||
- **Custom Dashboards**: User-configurable dashboard layouts |
|||
- **API Integration**: Webhook integration with external systems |
|||
|
|||
### Enterprise Features |
|||
- **SSO Integration**: LDAP, OAuth, and SAML authentication |
|||
- **Advanced RBAC**: Fine-grained permission system |
|||
- **Audit Compliance**: SOX, HIPAA, and PCI compliance features |
|||
- **High Availability**: Multi-instance dashboard deployment |
|||
|
|||
This design provides a comprehensive, modern, and scalable web interface for SeaweedFS administration, following industry best practices and providing an excellent user experience for cluster operators and administrators. |
@ -0,0 +1,165 @@ |
|||
# SeaweedFS Admin Component Makefile
|
|||
|
|||
# Variables
|
|||
ADMIN_DIR := . |
|||
VIEW_DIR := $(ADMIN_DIR)/view |
|||
STATIC_DIR := $(ADMIN_DIR)/static |
|||
TEMPL_FILES := $(shell find $(VIEW_DIR) -name "*.templ") |
|||
TEMPL_GO_FILES := $(TEMPL_FILES:.templ=_templ.go) |
|||
GO_FILES := $(shell find $(ADMIN_DIR) -name "*.go" -not -name "*_templ.go") |
|||
BUILD_DIR := ../.. |
|||
WEED_BINARY := $(BUILD_DIR)/weed |
|||
|
|||
# Default target
|
|||
.PHONY: all |
|||
all: build |
|||
|
|||
# Install templ if not present
|
|||
.PHONY: install-templ |
|||
install-templ: |
|||
@which templ > /dev/null || (echo "Installing templ..." && go install github.com/a-h/templ/cmd/templ@latest) |
|||
|
|||
# Generate templ files
|
|||
.PHONY: generate |
|||
generate: install-templ |
|||
@echo "Generating templ files..." |
|||
@templ generate |
|||
@echo "Generated: $(TEMPL_GO_FILES)" |
|||
|
|||
# Clean generated files
|
|||
.PHONY: clean-templ |
|||
clean-templ: |
|||
@echo "Cleaning generated templ files..." |
|||
@find $(VIEW_DIR) -name "*_templ.go" -delete |
|||
@echo "Cleaned templ files" |
|||
|
|||
# Watch for changes and regenerate
|
|||
.PHONY: watch |
|||
watch: install-templ |
|||
@echo "Watching for templ file changes..." |
|||
@templ generate --watch |
|||
|
|||
# Build the main weed binary with admin component
|
|||
.PHONY: build |
|||
build: generate |
|||
@echo "Building weed binary with admin component..." |
|||
@cd $(BUILD_DIR) && go build -o weed ./weed |
|||
@echo "Built: $(BUILD_DIR)/weed" |
|||
|
|||
# Test the admin component
|
|||
.PHONY: test |
|||
test: generate |
|||
@echo "Running admin component tests..." |
|||
@go test ./... |
|||
|
|||
# Run the admin server via weed command
|
|||
.PHONY: run |
|||
run: build |
|||
@echo "Starting admin server via weed command..." |
|||
@cd $(BUILD_DIR) && ./weed admin |
|||
|
|||
# Development server with auto-reload
|
|||
.PHONY: dev |
|||
dev: generate |
|||
@echo "Starting development server with auto-reload..." |
|||
@echo "Note: You'll need to manually restart the server when Go files change" |
|||
@cd $(BUILD_DIR) && ./weed admin -port=23647 & |
|||
@$(MAKE) watch |
|||
|
|||
# Lint the code
|
|||
.PHONY: lint |
|||
lint: |
|||
@echo "Linting admin component..." |
|||
@golangci-lint run ./... |
|||
|
|||
# Format the code
|
|||
.PHONY: fmt |
|||
fmt: |
|||
@echo "Formatting Go code..." |
|||
@go fmt ./... |
|||
@echo "Formatting templ files..." |
|||
@templ fmt $(VIEW_DIR) |
|||
|
|||
# Validate static files exist
|
|||
.PHONY: validate-static |
|||
validate-static: |
|||
@echo "Validating static files..." |
|||
@test -f $(STATIC_DIR)/css/admin.css || (echo "Missing: admin.css" && exit 1) |
|||
@test -f $(STATIC_DIR)/js/admin.js || (echo "Missing: admin.js" && exit 1) |
|||
@echo "Static files validated" |
|||
|
|||
# Check dependencies
|
|||
.PHONY: deps |
|||
deps: |
|||
@echo "Checking dependencies..." |
|||
@go mod tidy |
|||
@go mod verify |
|||
|
|||
# Clean all build artifacts
|
|||
.PHONY: clean |
|||
clean: clean-templ |
|||
@echo "Cleaning build artifacts..." |
|||
@rm -f $(BUILD_DIR)/weed 2>/dev/null || true |
|||
@echo "Cleaned build artifacts" |
|||
|
|||
# Install dependencies
|
|||
.PHONY: install-deps |
|||
install-deps: |
|||
@echo "Installing Go dependencies..." |
|||
@go mod download |
|||
@$(MAKE) install-templ |
|||
|
|||
# Production build
|
|||
.PHONY: build-prod |
|||
build-prod: clean generate validate-static |
|||
@echo "Building production binary..." |
|||
@cd $(BUILD_DIR) && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o weed-linux-amd64 ./weed |
|||
@echo "Built production binary: $(BUILD_DIR)/weed-linux-amd64" |
|||
|
|||
# Docker build (if needed)
|
|||
.PHONY: docker-build |
|||
docker-build: generate |
|||
@echo "Building Docker image with admin component..." |
|||
@cd $(BUILD_DIR) && docker build -t seaweedfs/seaweedfs:latest . |
|||
|
|||
# Help target
|
|||
.PHONY: help |
|||
help: |
|||
@echo "SeaweedFS Admin Component Makefile" |
|||
@echo "" |
|||
@echo "Available targets:" |
|||
@echo " all - Build the weed binary with admin component (default)" |
|||
@echo " generate - Generate templ files from templates" |
|||
@echo " build - Build weed binary with admin component" |
|||
@echo " build-prod - Build production binary" |
|||
@echo " run - Run admin server via weed command" |
|||
@echo " dev - Start development server with template watching" |
|||
@echo " test - Run tests" |
|||
@echo " watch - Watch for template changes and regenerate" |
|||
@echo " clean - Clean all build artifacts" |
|||
@echo " clean-templ - Clean generated template files" |
|||
@echo " fmt - Format Go and templ code" |
|||
@echo " lint - Lint the code" |
|||
@echo " deps - Check and tidy dependencies" |
|||
@echo " install-deps - Install all dependencies" |
|||
@echo " install-templ - Install templ compiler" |
|||
@echo " validate-static - Validate static files exist" |
|||
@echo " docker-build - Build Docker image" |
|||
@echo " help - Show this help message" |
|||
@echo "" |
|||
@echo "Examples:" |
|||
@echo " make generate # Generate templates" |
|||
@echo " make build # Build weed binary" |
|||
@echo " make run # Start admin server" |
|||
@echo " make dev # Development mode with auto-reload" |
|||
|
|||
# Make sure generated files are up to date before building
|
|||
$(WEED_BINARY): $(TEMPL_GO_FILES) $(GO_FILES) |
|||
@$(MAKE) build |
|||
|
|||
# Auto-generate templ files when .templ files change
|
|||
%_templ.go: %.templ |
|||
@echo "Regenerating $@ from $<" |
|||
@templ generate |
|||
|
|||
.PHONY: $(TEMPL_GO_FILES) |
@ -0,0 +1,96 @@ |
|||
# Navigation Menu Test |
|||
|
|||
## Quick Test Guide |
|||
|
|||
To verify that the S3 Buckets link appears in the navigation menu: |
|||
|
|||
### 1. Start the Admin Server |
|||
```bash |
|||
# Start with minimal setup |
|||
weed admin -port=23646 -masters=localhost:9333 -filer=localhost:8888 |
|||
|
|||
# Or with dummy values for testing UI only |
|||
weed admin -port=23646 -masters=dummy:9333 -filer=dummy:8888 |
|||
``` |
|||
|
|||
### 2. Open Browser |
|||
Navigate to: `http://localhost:23646` |
|||
|
|||
### 3. Check Navigation Menu |
|||
Look for the sidebar navigation on the left side. You should see: |
|||
|
|||
**CLUSTER Section:** |
|||
- Admin |
|||
- Cluster |
|||
- Volumes |
|||
|
|||
**MANAGEMENT Section:** |
|||
- **S3 Buckets** ← This should be visible! |
|||
- File Browser |
|||
- Metrics |
|||
- Logs |
|||
|
|||
**SYSTEM Section:** |
|||
- Configuration |
|||
- Maintenance |
|||
|
|||
### 4. Test S3 Buckets Link |
|||
- Click on "S3 Buckets" in the sidebar |
|||
- Should navigate to `/s3/buckets` |
|||
- Should show the S3 bucket management page |
|||
- The "S3 Buckets" menu item should be highlighted as active |
|||
|
|||
### 5. Expected Behavior |
|||
- Menu item has cube icon: `📦 S3 Buckets` |
|||
- Link points to `/s3/buckets` |
|||
- Active state highlighting works |
|||
- Page loads S3 bucket management interface |
|||
|
|||
## Troubleshooting |
|||
|
|||
If the S3 Buckets link is not visible: |
|||
|
|||
1. **Check Template Generation:** |
|||
```bash |
|||
cd weed/admin |
|||
templ generate |
|||
``` |
|||
|
|||
2. **Rebuild Binary:** |
|||
```bash |
|||
cd ../.. |
|||
go build -o weed weed/weed.go |
|||
``` |
|||
|
|||
3. **Check Browser Console:** |
|||
- Open Developer Tools (F12) |
|||
- Look for any JavaScript errors |
|||
- Check Network tab for failed requests |
|||
|
|||
4. **Verify File Structure:** |
|||
```bash |
|||
ls -la weed/admin/view/layout/layout_templ.go |
|||
``` |
|||
|
|||
5. **Check Server Logs:** |
|||
- Look for any error messages when starting admin server |
|||
- Check for template compilation errors |
|||
|
|||
## Files Modified |
|||
|
|||
- `weed/admin/view/layout/layout.templ` - Added S3 Buckets menu item |
|||
- `weed/admin/static/js/admin.js` - Updated navigation highlighting |
|||
- `weed/command/admin.go` - Added S3 routes |
|||
|
|||
## Expected Navigation Structure |
|||
|
|||
```html |
|||
<ul class="nav flex-column"> |
|||
<li class="nav-item"> |
|||
<a class="nav-link" href="/s3/buckets"> |
|||
<i class="fas fa-cube me-2"></i>S3 Buckets |
|||
</a> |
|||
</li> |
|||
<!-- ... other menu items ... --> |
|||
</ul> |
|||
``` |
@ -0,0 +1,279 @@ |
|||
# SeaweedFS Admin Component |
|||
|
|||
A modern web-based administration interface for SeaweedFS clusters built with Go, Gin, Templ, and Bootstrap. |
|||
|
|||
## Features |
|||
|
|||
- **Dashboard**: Real-time cluster status and metrics |
|||
- **Master Management**: Monitor master nodes and leadership status |
|||
- **Volume Server Management**: View volume servers, capacity, and health |
|||
- **S3 Bucket Management**: Create, delete, and manage S3 buckets with web interface |
|||
- **System Health**: Overall cluster health monitoring |
|||
- **Responsive Design**: Bootstrap-based UI that works on all devices |
|||
- **Authentication**: Optional user authentication with sessions |
|||
- **TLS Support**: HTTPS support for production deployments |
|||
|
|||
## Building |
|||
|
|||
### Using the Admin Makefile |
|||
|
|||
The admin component has its own Makefile for development and building: |
|||
|
|||
```bash |
|||
# Navigate to admin directory |
|||
cd weed/admin |
|||
|
|||
# View all available targets |
|||
make help |
|||
|
|||
# Generate templates and build |
|||
make build |
|||
|
|||
# Development mode with template watching |
|||
make dev |
|||
|
|||
# Run the admin server |
|||
make run |
|||
|
|||
# Clean build artifacts |
|||
make clean |
|||
``` |
|||
|
|||
### Using the Root Makefile |
|||
|
|||
The root SeaweedFS Makefile automatically integrates the admin component: |
|||
|
|||
```bash |
|||
# From the root directory |
|||
make install # Builds weed with admin component |
|||
make full_install # Full build with all tags |
|||
make test # Runs tests including admin component |
|||
|
|||
# Admin-specific targets from root |
|||
make admin-generate # Generate admin templates |
|||
make admin-build # Build admin component |
|||
make admin-run # Run admin server |
|||
make admin-dev # Development mode |
|||
make admin-clean # Clean admin artifacts |
|||
``` |
|||
|
|||
### Manual Building |
|||
|
|||
If you prefer to build manually: |
|||
|
|||
```bash |
|||
# Install templ compiler |
|||
go install github.com/a-h/templ/cmd/templ@latest |
|||
|
|||
# Generate templates |
|||
templ generate |
|||
|
|||
# Build the main weed binary |
|||
cd ../../../ |
|||
go build -o weed ./weed |
|||
``` |
|||
|
|||
## Development |
|||
|
|||
### Template Development |
|||
|
|||
The admin interface uses [Templ](https://templ.guide/) for type-safe HTML templates: |
|||
|
|||
```bash |
|||
# Watch for template changes and auto-regenerate |
|||
make watch |
|||
|
|||
# Or manually generate templates |
|||
make generate |
|||
|
|||
# Format templates |
|||
make fmt |
|||
``` |
|||
|
|||
### File Structure |
|||
|
|||
``` |
|||
weed/admin/ |
|||
├── Makefile # Admin-specific build tasks |
|||
├── README.md # This file |
|||
├── S3_BUCKETS.md # S3 bucket management documentation |
|||
├── admin.go # Main application entry point |
|||
├── dash/ # Server and handler logic |
|||
│ ├── admin_server.go # HTTP server setup |
|||
│ ├── handler_admin.go # Admin dashboard handlers |
|||
│ ├── handler_auth.go # Authentication handlers |
|||
│ └── middleware.go # HTTP middleware |
|||
├── static/ # Static assets |
|||
│ ├── css/admin.css # Admin-specific styles |
|||
│ └── js/admin.js # Admin-specific JavaScript |
|||
└── view/ # Templates |
|||
├── app/ # Application templates |
|||
│ ├── admin.templ # Main dashboard template |
|||
│ ├── s3_buckets.templ # S3 bucket management template |
|||
│ └── *_templ.go # Generated Go code |
|||
└── layout/ # Layout templates |
|||
├── layout.templ # Base layout template |
|||
└── layout_templ.go # Generated Go code |
|||
``` |
|||
|
|||
### S3 Bucket Management |
|||
|
|||
The admin interface includes comprehensive S3 bucket management capabilities. See [S3_BUCKETS.md](S3_BUCKETS.md) for detailed documentation on: |
|||
|
|||
- Creating and deleting S3 buckets |
|||
- Viewing bucket contents and metadata |
|||
- Managing bucket permissions and settings |
|||
- API endpoints for programmatic access |
|||
|
|||
## Usage |
|||
|
|||
### Basic Usage |
|||
|
|||
```bash |
|||
# Start admin interface on default port (23646) |
|||
weed admin |
|||
|
|||
# Start with custom configuration |
|||
weed admin -port=8080 -masters="master1:9333,master2:9333" -filer="filer:8888" |
|||
|
|||
# Start with authentication |
|||
weed admin -adminUser=admin -adminPassword=secret123 |
|||
|
|||
# Start with HTTPS |
|||
weed admin -port=443 -tlsCert=/path/to/cert.pem -tlsKey=/path/to/key.pem |
|||
``` |
|||
|
|||
### Configuration Options |
|||
|
|||
| Option | Default | Description | |
|||
|--------|---------|-------------| |
|||
| `-port` | 23646 | Admin server port | |
|||
| `-masters` | localhost:9333 | Comma-separated master servers | |
|||
| `-adminUser` | admin | Admin username (if auth enabled) | |
|||
| `-adminPassword` | "" | Admin password (empty = no auth) | |
|||
| `-tlsCert` | "" | Path to TLS certificate | |
|||
| `-tlsKey` | "" | Path to TLS private key | |
|||
|
|||
### Docker Usage |
|||
|
|||
```bash |
|||
# Build Docker image with admin component |
|||
make docker-build |
|||
|
|||
# Run with Docker |
|||
docker run -p 23646:23646 seaweedfs/seaweedfs:latest admin -masters=host.docker.internal:9333 |
|||
``` |
|||
|
|||
## Development Workflow |
|||
|
|||
### Quick Start |
|||
|
|||
```bash |
|||
# Clone and setup |
|||
git clone <seaweedfs-repo> |
|||
cd seaweedfs/weed/admin |
|||
|
|||
# Install dependencies and build |
|||
make install-deps |
|||
make build |
|||
|
|||
# Start development server |
|||
make dev |
|||
``` |
|||
|
|||
### Making Changes |
|||
|
|||
1. **Template Changes**: Edit `.templ` files in `view/` |
|||
- Templates auto-regenerate in development mode |
|||
- Use `make generate` to manually regenerate |
|||
|
|||
2. **Go Code Changes**: Edit `.go` files |
|||
- Restart the server to see changes |
|||
- Use `make build` to rebuild |
|||
|
|||
3. **Static Assets**: Edit files in `static/` |
|||
- Changes are served immediately |
|||
|
|||
### Testing |
|||
|
|||
```bash |
|||
# Run admin component tests |
|||
make test |
|||
|
|||
# Run from root directory |
|||
make admin-test |
|||
|
|||
# Lint code |
|||
make lint |
|||
|
|||
# Format code |
|||
make fmt |
|||
``` |
|||
|
|||
## Production Deployment |
|||
|
|||
### Security Considerations |
|||
|
|||
1. **Authentication**: Always set `adminPassword` in production |
|||
2. **HTTPS**: Use TLS certificates for encrypted connections |
|||
3. **Firewall**: Restrict admin interface access to authorized networks |
|||
|
|||
### Example Production Setup |
|||
|
|||
```bash |
|||
# Production deployment with security |
|||
weed admin \ |
|||
-port=443 \ |
|||
-masters="master1:9333,master2:9333,master3:9333" \ |
|||
-adminUser=admin \ |
|||
-adminPassword=your-secure-password \ |
|||
-tlsCert=/etc/ssl/certs/admin.crt \ |
|||
-tlsKey=/etc/ssl/private/admin.key |
|||
``` |
|||
|
|||
### Monitoring |
|||
|
|||
The admin interface provides endpoints for monitoring: |
|||
|
|||
- `GET /health` - Health check endpoint |
|||
- `GET /metrics` - Prometheus metrics (if enabled) |
|||
- `GET /api/status` - JSON status information |
|||
|
|||
## Troubleshooting |
|||
|
|||
### Common Issues |
|||
|
|||
1. **Templates not found**: Run `make generate` to create template files |
|||
2. **Build errors**: Ensure `templ` is installed with `make install-templ` |
|||
3. **Static files not loading**: Check that `static/` directory exists and has proper files |
|||
4. **Connection errors**: Verify master and filer addresses are correct |
|||
|
|||
### Debug Mode |
|||
|
|||
```bash |
|||
# Enable debug logging |
|||
weed -v=2 admin |
|||
|
|||
# Check generated templates |
|||
ls -la view/app/*_templ.go view/layout/*_templ.go |
|||
``` |
|||
|
|||
## Contributing |
|||
|
|||
1. Fork the repository |
|||
2. Create a feature branch |
|||
3. Make your changes |
|||
4. Run tests: `make test` |
|||
5. Format code: `make fmt` |
|||
6. Submit a pull request |
|||
|
|||
## Architecture |
|||
|
|||
The admin component follows a clean architecture: |
|||
|
|||
- **Presentation Layer**: Templ templates + Bootstrap CSS |
|||
- **HTTP Layer**: Gin router with middleware |
|||
- **Business Logic**: Handler functions in `dash/` package |
|||
- **Data Layer**: Communicates with SeaweedFS masters and filers |
|||
|
|||
This separation makes the code maintainable and testable. |
@ -0,0 +1,174 @@ |
|||
# S3 Bucket Management |
|||
|
|||
The SeaweedFS Admin Interface now includes comprehensive S3 bucket management capabilities. |
|||
|
|||
## Features |
|||
|
|||
### Bucket Overview |
|||
- **Dashboard View**: List all S3 buckets with summary statistics |
|||
- **Bucket Statistics**: Total buckets, storage usage, object counts |
|||
- **Status Monitoring**: Real-time bucket status and health indicators |
|||
|
|||
### Bucket Operations |
|||
- **Create Buckets**: Create new S3 buckets |
|||
- **Delete Buckets**: Remove buckets and all their contents (with confirmation) |
|||
- **View Details**: Browse bucket contents and object listings |
|||
- **Export Data**: Export bucket lists to CSV format |
|||
|
|||
### Bucket Information |
|||
Each bucket displays: |
|||
- **Name**: Bucket identifier |
|||
- **Created Date**: When the bucket was created |
|||
- **Object Count**: Number of objects stored |
|||
- **Total Size**: Storage space used (formatted in KB/MB/GB/TB) |
|||
- **Region**: Configured AWS region |
|||
- **Status**: Current operational status |
|||
|
|||
## Usage |
|||
|
|||
### Accessing S3 Bucket Management |
|||
|
|||
1. Start the admin server: |
|||
```bash |
|||
weed admin -port=23646 -masters=localhost:9333 -filer=localhost:8888 |
|||
``` |
|||
|
|||
2. Open your browser to: `http://localhost:23646` |
|||
|
|||
3. Click the "S3 Buckets" button in the dashboard toolbar |
|||
|
|||
4. Or navigate directly to: `http://localhost:23646/s3/buckets` |
|||
|
|||
### Creating a New Bucket |
|||
|
|||
1. Click the "Create Bucket" button |
|||
2. Enter a valid bucket name (3-63 characters, lowercase letters, numbers, dots, hyphens) |
|||
3. Select a region (defaults to us-east-1) |
|||
4. Click "Create Bucket" |
|||
|
|||
### Deleting a Bucket |
|||
|
|||
1. Click the trash icon next to the bucket name |
|||
2. Confirm the deletion in the modal dialog |
|||
3. **Warning**: This permanently deletes the bucket and all its contents |
|||
|
|||
### Viewing Bucket Details |
|||
|
|||
1. Click on a bucket name to view detailed information |
|||
2. See all objects within the bucket |
|||
3. View object metadata (size, last modified, etc.) |
|||
|
|||
## API Endpoints |
|||
|
|||
The S3 bucket management feature exposes REST API endpoints: |
|||
|
|||
### List Buckets |
|||
``` |
|||
GET /api/s3/buckets |
|||
``` |
|||
Returns JSON array of all buckets with metadata. |
|||
|
|||
### Create Bucket |
|||
``` |
|||
POST /api/s3/buckets |
|||
Content-Type: application/json |
|||
|
|||
{ |
|||
"name": "my-bucket-name", |
|||
"region": "us-east-1" |
|||
} |
|||
``` |
|||
|
|||
### Delete Bucket |
|||
``` |
|||
DELETE /api/s3/buckets/{bucket-name} |
|||
``` |
|||
Permanently deletes the bucket and all contents. |
|||
|
|||
### Get Bucket Details |
|||
``` |
|||
GET /api/s3/buckets/{bucket-name} |
|||
``` |
|||
Returns detailed bucket information including object listings. |
|||
|
|||
## Technical Implementation |
|||
|
|||
### Backend Integration |
|||
- **Filer Integration**: Uses SeaweedFS filer for bucket storage at `/buckets/` |
|||
- **Streaming API**: Efficiently handles large bucket listings |
|||
- **Error Handling**: Comprehensive error reporting and recovery |
|||
|
|||
### Frontend Features |
|||
- **Bootstrap UI**: Modern, responsive web interface |
|||
- **Real-time Updates**: Automatic refresh after operations |
|||
- **Form Validation**: Client-side bucket name validation |
|||
- **Modal Dialogs**: User-friendly create/delete workflows |
|||
|
|||
### Security Considerations |
|||
- **Confirmation Dialogs**: Prevent accidental deletions |
|||
- **Input Validation**: Prevent invalid bucket names |
|||
- **Error Messages**: Clear feedback for failed operations |
|||
|
|||
## Bucket Naming Rules |
|||
|
|||
S3 bucket names must follow these rules: |
|||
- 3-63 characters in length |
|||
- Contain only lowercase letters, numbers, dots (.), and hyphens (-) |
|||
- Start and end with a lowercase letter or number |
|||
- Cannot contain spaces or special characters |
|||
- Cannot be formatted as an IP address |
|||
|
|||
## Storage Structure |
|||
|
|||
Buckets are stored in the SeaweedFS filer at: |
|||
``` |
|||
/buckets/{bucket-name}/ |
|||
``` |
|||
|
|||
Each bucket directory contains: |
|||
- Object files with their original names |
|||
- Nested directories for object key prefixes |
|||
- Metadata preserved from S3 operations |
|||
|
|||
## Performance Notes |
|||
|
|||
- **Lazy Loading**: Bucket sizes and object counts are calculated on-demand |
|||
- **Streaming**: Large bucket listings use streaming responses |
|||
- **Caching**: Cluster topology data is cached for performance |
|||
- **Pagination**: Large object lists are handled efficiently |
|||
|
|||
## Troubleshooting |
|||
|
|||
### Common Issues |
|||
|
|||
1. **Bucket Creation Fails** |
|||
- Check bucket name follows S3 naming rules |
|||
- Ensure filer is accessible and running |
|||
- Verify sufficient storage space |
|||
|
|||
2. **Bucket Deletion Fails** |
|||
- Ensure bucket exists and is accessible |
|||
- Check for permission issues |
|||
- Verify filer connectivity |
|||
|
|||
3. **Bucket List Empty** |
|||
- Verify filer has `/buckets/` directory |
|||
- Check filer connectivity |
|||
- Ensure buckets were created through S3 API |
|||
|
|||
### Debug Steps |
|||
|
|||
1. Check admin server logs for error messages |
|||
2. Verify filer is running and accessible |
|||
3. Test filer connectivity: `curl http://localhost:8888/` |
|||
4. Check browser console for JavaScript errors |
|||
|
|||
## Future Enhancements |
|||
|
|||
- **Bucket Policies**: Manage access control policies |
|||
- **Lifecycle Rules**: Configure object lifecycle management |
|||
- **Versioning**: Enable/disable bucket versioning |
|||
- **Replication**: Configure cross-region replication |
|||
- **Metrics**: Detailed usage and performance metrics |
|||
- **Notifications**: Bucket event notifications |
|||
- **Search**: Search and filter bucket contents |
@ -0,0 +1,247 @@ |
|||
package main |
|||
|
|||
import ( |
|||
"context" |
|||
"crypto/tls" |
|||
"embed" |
|||
"flag" |
|||
"fmt" |
|||
"io/fs" |
|||
"log" |
|||
"net/http" |
|||
"os" |
|||
"os/signal" |
|||
"syscall" |
|||
"time" |
|||
|
|||
"github.com/gin-contrib/sessions" |
|||
"github.com/gin-contrib/sessions/cookie" |
|||
"github.com/gin-gonic/gin" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
//go:embed static/* view/*
|
|||
var adminFS embed.FS |
|||
|
|||
func main() { |
|||
var ( |
|||
port = flag.Int("port", 23646, "Port to run the admin server on") |
|||
host = flag.String("host", "localhost", "Host to bind the admin server to") |
|||
sessionKey = flag.String("sessionKey", "", "Session encryption key (32 bytes, random if not provided)") |
|||
tlsCert = flag.String("tlsCert", "", "Path to TLS certificate file") |
|||
tlsKey = flag.String("tlsKey", "", "Path to TLS key file") |
|||
master = flag.String("master", "localhost:9333", "SeaweedFS master server address") |
|||
authRequired = flag.Bool("auth", false, "Enable authentication") |
|||
username = flag.String("username", "admin", "Admin username (only used if auth is enabled)") |
|||
password = flag.String("password", "", "Admin password (only used if auth is enabled)") |
|||
help = flag.Bool("help", false, "Show help") |
|||
) |
|||
|
|||
flag.Parse() |
|||
|
|||
if *help { |
|||
fmt.Println("SeaweedFS Admin Server") |
|||
fmt.Println() |
|||
flag.PrintDefaults() |
|||
return |
|||
} |
|||
|
|||
// Set Gin mode
|
|||
gin.SetMode(gin.ReleaseMode) |
|||
|
|||
// Create router
|
|||
r := gin.New() |
|||
r.Use(gin.Logger(), gin.Recovery()) |
|||
|
|||
// Session store
|
|||
var sessionKeyBytes []byte |
|||
if *sessionKey != "" { |
|||
sessionKeyBytes = []byte(*sessionKey) |
|||
} else { |
|||
// Generate a random session key
|
|||
sessionKeyBytes = make([]byte, 32) |
|||
for i := range sessionKeyBytes { |
|||
sessionKeyBytes[i] = byte(time.Now().UnixNano() & 0xff) |
|||
} |
|||
} |
|||
store := cookie.NewStore(sessionKeyBytes) |
|||
r.Use(sessions.Sessions("admin-session", store)) |
|||
|
|||
// Static files
|
|||
staticFS, err := fs.Sub(adminFS, "static") |
|||
if err != nil { |
|||
log.Fatal("Failed to create static filesystem:", err) |
|||
} |
|||
r.StaticFS("/static", http.FS(staticFS)) |
|||
|
|||
// Templates
|
|||
viewFS, err := fs.Sub(adminFS, "view") |
|||
if err != nil { |
|||
log.Fatal("Failed to create view filesystem:", err) |
|||
} |
|||
|
|||
// Create admin server
|
|||
adminServer := dash.NewAdminServer(*master, http.FS(viewFS)) |
|||
|
|||
// Setup routes
|
|||
setupRoutes(r, adminServer, *authRequired, *username, *password) |
|||
|
|||
// Server configuration
|
|||
addr := fmt.Sprintf("%s:%d", *host, *port) |
|||
server := &http.Server{ |
|||
Addr: addr, |
|||
Handler: r, |
|||
} |
|||
|
|||
// TLS configuration
|
|||
if *tlsCert != "" && *tlsKey != "" { |
|||
server.TLSConfig = &tls.Config{ |
|||
MinVersion: tls.VersionTLS12, |
|||
} |
|||
} |
|||
|
|||
// Start server
|
|||
go func() { |
|||
log.Printf("Starting SeaweedFS Admin Server on %s", addr) |
|||
|
|||
var err error |
|||
if *tlsCert != "" && *tlsKey != "" { |
|||
log.Printf("Using TLS with cert: %s, key: %s", *tlsCert, *tlsKey) |
|||
err = server.ListenAndServeTLS(*tlsCert, *tlsKey) |
|||
} else { |
|||
err = server.ListenAndServe() |
|||
} |
|||
|
|||
if err != nil && err != http.ErrServerClosed { |
|||
log.Fatal("Failed to start server:", err) |
|||
} |
|||
}() |
|||
|
|||
// Wait for interrupt signal to gracefully shutdown the server
|
|||
quit := make(chan os.Signal, 1) |
|||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) |
|||
<-quit |
|||
log.Println("Shutting down admin server...") |
|||
|
|||
// Give outstanding requests 30 seconds to complete
|
|||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
|||
defer cancel() |
|||
|
|||
if err := server.Shutdown(ctx); err != nil { |
|||
log.Fatal("Admin server forced to shutdown:", err) |
|||
} |
|||
|
|||
log.Println("Admin server exited") |
|||
} |
|||
|
|||
func setupRoutes(r *gin.Engine, adminServer *dash.AdminServer, authRequired bool, username, password string) { |
|||
// Health check (no auth required)
|
|||
r.GET("/health", func(c *gin.Context) { |
|||
c.JSON(200, gin.H{"status": "ok"}) |
|||
}) |
|||
|
|||
if authRequired { |
|||
// Auth routes
|
|||
auth := r.Group("/") |
|||
auth.GET("/login", adminServer.ShowLogin) |
|||
auth.POST("/login", adminServer.HandleLogin(username, password)) |
|||
auth.POST("/logout", adminServer.HandleLogout) |
|||
|
|||
// Protected routes
|
|||
protected := r.Group("/") |
|||
protected.Use(dash.RequireAuth()) |
|||
|
|||
// Admin routes
|
|||
protected.GET("/", adminServer.ShowAdmin) |
|||
protected.GET("/admin", adminServer.ShowAdmin) |
|||
protected.GET("/overview", adminServer.ShowAdmin) |
|||
|
|||
// Cluster management
|
|||
cluster := protected.Group("/cluster") |
|||
{ |
|||
cluster.GET("/topology", adminServer.GetClusterTopologyHandler) |
|||
cluster.GET("/masters", adminServer.GetMasters) |
|||
cluster.GET("/volumes", adminServer.GetVolumeServers) |
|||
cluster.POST("/volumes/assign", adminServer.AssignVolume) |
|||
} |
|||
|
|||
// Volume management
|
|||
volumes := protected.Group("/volumes") |
|||
{ |
|||
volumes.GET("/", adminServer.ListVolumes) |
|||
volumes.POST("/create", adminServer.CreateVolume) |
|||
volumes.DELETE("/:id", adminServer.DeleteVolume) |
|||
volumes.POST("/:id/replicate", adminServer.ReplicateVolume) |
|||
} |
|||
|
|||
// File browser
|
|||
files := protected.Group("/filer") |
|||
{ |
|||
files.GET("/*path", adminServer.BrowseFiles) |
|||
files.POST("/upload", adminServer.UploadFile) |
|||
files.DELETE("/*path", adminServer.DeleteFile) |
|||
} |
|||
|
|||
// Metrics
|
|||
metrics := protected.Group("/metrics") |
|||
{ |
|||
metrics.GET("/", adminServer.ShowMetrics) |
|||
metrics.GET("/data", adminServer.GetMetricsData) |
|||
} |
|||
|
|||
// Maintenance
|
|||
maintenance := protected.Group("/maintenance") |
|||
{ |
|||
maintenance.POST("/gc", adminServer.TriggerGC) |
|||
maintenance.POST("/compact", adminServer.CompactVolumes) |
|||
maintenance.GET("/status", adminServer.GetMaintenanceStatus) |
|||
} |
|||
} else { |
|||
// No auth required - all routes are public
|
|||
r.GET("/", adminServer.ShowAdmin) |
|||
r.GET("/admin", adminServer.ShowAdmin) |
|||
r.GET("/overview", adminServer.ShowAdmin) |
|||
|
|||
// Cluster management
|
|||
cluster := r.Group("/cluster") |
|||
{ |
|||
cluster.GET("/topology", adminServer.GetClusterTopologyHandler) |
|||
cluster.GET("/masters", adminServer.GetMasters) |
|||
cluster.GET("/volumes", adminServer.GetVolumeServers) |
|||
cluster.POST("/volumes/assign", adminServer.AssignVolume) |
|||
} |
|||
|
|||
// Volume management
|
|||
volumes := r.Group("/volumes") |
|||
{ |
|||
volumes.GET("/", adminServer.ListVolumes) |
|||
volumes.POST("/create", adminServer.CreateVolume) |
|||
volumes.DELETE("/:id", adminServer.DeleteVolume) |
|||
volumes.POST("/:id/replicate", adminServer.ReplicateVolume) |
|||
} |
|||
|
|||
// File browser
|
|||
files := r.Group("/filer") |
|||
{ |
|||
files.GET("/*path", adminServer.BrowseFiles) |
|||
files.POST("/upload", adminServer.UploadFile) |
|||
files.DELETE("/*path", adminServer.DeleteFile) |
|||
} |
|||
|
|||
// Metrics
|
|||
metrics := r.Group("/metrics") |
|||
{ |
|||
metrics.GET("/", adminServer.ShowMetrics) |
|||
metrics.GET("/data", adminServer.GetMetricsData) |
|||
} |
|||
|
|||
// Maintenance
|
|||
maintenance := r.Group("/maintenance") |
|||
{ |
|||
maintenance.POST("/gc", adminServer.TriggerGC) |
|||
maintenance.POST("/compact", adminServer.CompactVolumes) |
|||
maintenance.GET("/status", adminServer.GetMaintenanceStatus) |
|||
} |
|||
} |
|||
} |
1146
weed/admin/dash/admin_server.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,350 @@ |
|||
package dash |
|||
|
|||
import ( |
|||
"context" |
|||
"path/filepath" |
|||
"sort" |
|||
"strings" |
|||
"time" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
|||
) |
|||
|
|||
// FileEntry represents a file or directory entry in the file browser
|
|||
type FileEntry struct { |
|||
Name string `json:"name"` |
|||
FullPath string `json:"full_path"` |
|||
IsDirectory bool `json:"is_directory"` |
|||
Size int64 `json:"size"` |
|||
ModTime time.Time `json:"mod_time"` |
|||
Mode string `json:"mode"` |
|||
Uid uint32 `json:"uid"` |
|||
Gid uint32 `json:"gid"` |
|||
Mime string `json:"mime"` |
|||
Replication string `json:"replication"` |
|||
Collection string `json:"collection"` |
|||
TtlSec int32 `json:"ttl_sec"` |
|||
} |
|||
|
|||
// BreadcrumbItem represents a single breadcrumb in the navigation
|
|||
type BreadcrumbItem struct { |
|||
Name string `json:"name"` |
|||
Path string `json:"path"` |
|||
} |
|||
|
|||
// FileBrowserData contains all data needed for the file browser view
|
|||
type FileBrowserData struct { |
|||
Username string `json:"username"` |
|||
CurrentPath string `json:"current_path"` |
|||
ParentPath string `json:"parent_path"` |
|||
Breadcrumbs []BreadcrumbItem `json:"breadcrumbs"` |
|||
Entries []FileEntry `json:"entries"` |
|||
TotalEntries int `json:"total_entries"` |
|||
TotalSize int64 `json:"total_size"` |
|||
LastUpdated time.Time `json:"last_updated"` |
|||
IsBucketPath bool `json:"is_bucket_path"` |
|||
BucketName string `json:"bucket_name"` |
|||
} |
|||
|
|||
// GetFileBrowser retrieves file browser data for a given path
|
|||
func (s *AdminServer) GetFileBrowser(path string) (*FileBrowserData, error) { |
|||
if path == "" { |
|||
path = "/" |
|||
} |
|||
|
|||
var entries []FileEntry |
|||
var totalSize int64 |
|||
|
|||
// Get directory listing from filer
|
|||
err := s.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { |
|||
stream, err := client.ListEntries(context.Background(), &filer_pb.ListEntriesRequest{ |
|||
Directory: path, |
|||
Prefix: "", |
|||
Limit: 1000, |
|||
InclusiveStartFrom: false, |
|||
}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
for { |
|||
resp, err := stream.Recv() |
|||
if err != nil { |
|||
if err.Error() == "EOF" { |
|||
break |
|||
} |
|||
return err |
|||
} |
|||
|
|||
entry := resp.Entry |
|||
if entry == nil { |
|||
continue |
|||
} |
|||
|
|||
fullPath := path |
|||
if !strings.HasSuffix(fullPath, "/") { |
|||
fullPath += "/" |
|||
} |
|||
fullPath += entry.Name |
|||
|
|||
var modTime time.Time |
|||
if entry.Attributes != nil && entry.Attributes.Mtime > 0 { |
|||
modTime = time.Unix(entry.Attributes.Mtime, 0) |
|||
} |
|||
|
|||
var mode string |
|||
var uid, gid uint32 |
|||
var size int64 |
|||
var replication, collection string |
|||
var ttlSec int32 |
|||
|
|||
if entry.Attributes != nil { |
|||
mode = formatFileMode(entry.Attributes.FileMode) |
|||
uid = entry.Attributes.Uid |
|||
gid = entry.Attributes.Gid |
|||
size = int64(entry.Attributes.FileSize) |
|||
ttlSec = entry.Attributes.TtlSec |
|||
} |
|||
|
|||
// Get replication and collection from entry extended attributes or chunks
|
|||
if entry.Extended != nil { |
|||
if repl, ok := entry.Extended["replication"]; ok { |
|||
replication = string(repl) |
|||
} |
|||
if coll, ok := entry.Extended["collection"]; ok { |
|||
collection = string(coll) |
|||
} |
|||
} |
|||
|
|||
// Determine MIME type based on file extension
|
|||
mime := "application/octet-stream" |
|||
if entry.IsDirectory { |
|||
mime = "inode/directory" |
|||
} else { |
|||
ext := strings.ToLower(filepath.Ext(entry.Name)) |
|||
switch ext { |
|||
case ".txt", ".log": |
|||
mime = "text/plain" |
|||
case ".html", ".htm": |
|||
mime = "text/html" |
|||
case ".css": |
|||
mime = "text/css" |
|||
case ".js": |
|||
mime = "application/javascript" |
|||
case ".json": |
|||
mime = "application/json" |
|||
case ".xml": |
|||
mime = "application/xml" |
|||
case ".pdf": |
|||
mime = "application/pdf" |
|||
case ".jpg", ".jpeg": |
|||
mime = "image/jpeg" |
|||
case ".png": |
|||
mime = "image/png" |
|||
case ".gif": |
|||
mime = "image/gif" |
|||
case ".svg": |
|||
mime = "image/svg+xml" |
|||
case ".mp4": |
|||
mime = "video/mp4" |
|||
case ".mp3": |
|||
mime = "audio/mpeg" |
|||
case ".zip": |
|||
mime = "application/zip" |
|||
case ".tar": |
|||
mime = "application/x-tar" |
|||
case ".gz": |
|||
mime = "application/gzip" |
|||
} |
|||
} |
|||
|
|||
fileEntry := FileEntry{ |
|||
Name: entry.Name, |
|||
FullPath: fullPath, |
|||
IsDirectory: entry.IsDirectory, |
|||
Size: size, |
|||
ModTime: modTime, |
|||
Mode: mode, |
|||
Uid: uid, |
|||
Gid: gid, |
|||
Mime: mime, |
|||
Replication: replication, |
|||
Collection: collection, |
|||
TtlSec: ttlSec, |
|||
} |
|||
|
|||
entries = append(entries, fileEntry) |
|||
if !entry.IsDirectory { |
|||
totalSize += size |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
}) |
|||
|
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
// Sort entries: directories first, then files, both alphabetically
|
|||
sort.Slice(entries, func(i, j int) bool { |
|||
if entries[i].IsDirectory != entries[j].IsDirectory { |
|||
return entries[i].IsDirectory |
|||
} |
|||
return strings.ToLower(entries[i].Name) < strings.ToLower(entries[j].Name) |
|||
}) |
|||
|
|||
// Generate breadcrumbs
|
|||
breadcrumbs := s.generateBreadcrumbs(path) |
|||
|
|||
// Calculate parent path
|
|||
parentPath := "/" |
|||
if path != "/" { |
|||
parentPath = filepath.Dir(path) |
|||
if parentPath == "." { |
|||
parentPath = "/" |
|||
} |
|||
} |
|||
|
|||
// Check if this is a bucket path
|
|||
isBucketPath := false |
|||
bucketName := "" |
|||
if strings.HasPrefix(path, "/buckets/") { |
|||
isBucketPath = true |
|||
pathParts := strings.Split(strings.Trim(path, "/"), "/") |
|||
if len(pathParts) >= 2 { |
|||
bucketName = pathParts[1] |
|||
} |
|||
} |
|||
|
|||
return &FileBrowserData{ |
|||
CurrentPath: path, |
|||
ParentPath: parentPath, |
|||
Breadcrumbs: breadcrumbs, |
|||
Entries: entries, |
|||
TotalEntries: len(entries), |
|||
TotalSize: totalSize, |
|||
LastUpdated: time.Now(), |
|||
IsBucketPath: isBucketPath, |
|||
BucketName: bucketName, |
|||
}, nil |
|||
} |
|||
|
|||
// generateBreadcrumbs creates breadcrumb navigation for the current path
|
|||
func (s *AdminServer) generateBreadcrumbs(path string) []BreadcrumbItem { |
|||
var breadcrumbs []BreadcrumbItem |
|||
|
|||
// Always start with root
|
|||
breadcrumbs = append(breadcrumbs, BreadcrumbItem{ |
|||
Name: "Root", |
|||
Path: "/", |
|||
}) |
|||
|
|||
if path == "/" { |
|||
return breadcrumbs |
|||
} |
|||
|
|||
// Split path and build breadcrumbs
|
|||
parts := strings.Split(strings.Trim(path, "/"), "/") |
|||
currentPath := "" |
|||
|
|||
for _, part := range parts { |
|||
if part == "" { |
|||
continue |
|||
} |
|||
currentPath += "/" + part |
|||
|
|||
// Special handling for bucket paths
|
|||
displayName := part |
|||
if len(breadcrumbs) == 1 && part == "buckets" { |
|||
displayName = "S3 Buckets" |
|||
} else if len(breadcrumbs) == 2 && strings.HasPrefix(path, "/buckets/") { |
|||
displayName = "📦 " + part // Add bucket icon to bucket name
|
|||
} |
|||
|
|||
breadcrumbs = append(breadcrumbs, BreadcrumbItem{ |
|||
Name: displayName, |
|||
Path: currentPath, |
|||
}) |
|||
} |
|||
|
|||
return breadcrumbs |
|||
} |
|||
|
|||
// formatFileMode converts file mode to Unix-style string representation (e.g., "drwxr-xr-x")
|
|||
func formatFileMode(mode uint32) string { |
|||
var result []byte = make([]byte, 10) |
|||
|
|||
// File type
|
|||
switch mode & 0170000 { // S_IFMT mask
|
|||
case 0040000: // S_IFDIR
|
|||
result[0] = 'd' |
|||
case 0100000: // S_IFREG
|
|||
result[0] = '-' |
|||
case 0120000: // S_IFLNK
|
|||
result[0] = 'l' |
|||
case 0020000: // S_IFCHR
|
|||
result[0] = 'c' |
|||
case 0060000: // S_IFBLK
|
|||
result[0] = 'b' |
|||
case 0010000: // S_IFIFO
|
|||
result[0] = 'p' |
|||
case 0140000: // S_IFSOCK
|
|||
result[0] = 's' |
|||
default: |
|||
result[0] = '-' // S_IFREG is default
|
|||
} |
|||
|
|||
// Owner permissions
|
|||
if mode&0400 != 0 { // S_IRUSR
|
|||
result[1] = 'r' |
|||
} else { |
|||
result[1] = '-' |
|||
} |
|||
if mode&0200 != 0 { // S_IWUSR
|
|||
result[2] = 'w' |
|||
} else { |
|||
result[2] = '-' |
|||
} |
|||
if mode&0100 != 0 { // S_IXUSR
|
|||
result[3] = 'x' |
|||
} else { |
|||
result[3] = '-' |
|||
} |
|||
|
|||
// Group permissions
|
|||
if mode&0040 != 0 { // S_IRGRP
|
|||
result[4] = 'r' |
|||
} else { |
|||
result[4] = '-' |
|||
} |
|||
if mode&0020 != 0 { // S_IWGRP
|
|||
result[5] = 'w' |
|||
} else { |
|||
result[5] = '-' |
|||
} |
|||
if mode&0010 != 0 { // S_IXGRP
|
|||
result[6] = 'x' |
|||
} else { |
|||
result[6] = '-' |
|||
} |
|||
|
|||
// Other permissions
|
|||
if mode&0004 != 0 { // S_IROTH
|
|||
result[7] = 'r' |
|||
} else { |
|||
result[7] = '-' |
|||
} |
|||
if mode&0002 != 0 { // S_IWOTH
|
|||
result[8] = 'w' |
|||
} else { |
|||
result[8] = '-' |
|||
} |
|||
if mode&0001 != 0 { // S_IXOTH
|
|||
result[9] = 'x' |
|||
} else { |
|||
result[9] = '-' |
|||
} |
|||
|
|||
return string(result) |
|||
} |
@ -0,0 +1,373 @@ |
|||
package dash |
|||
|
|||
import ( |
|||
"context" |
|||
"net/http" |
|||
"time" |
|||
|
|||
"github.com/gin-gonic/gin" |
|||
"github.com/seaweedfs/seaweedfs/weed/cluster" |
|||
"github.com/seaweedfs/seaweedfs/weed/glog" |
|||
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb" |
|||
) |
|||
|
|||
type AdminData struct { |
|||
Username string `json:"username"` |
|||
ClusterStatus string `json:"cluster_status"` |
|||
TotalVolumes int `json:"total_volumes"` |
|||
TotalFiles int64 `json:"total_files"` |
|||
TotalSize int64 `json:"total_size"` |
|||
MasterNodes []MasterNode `json:"master_nodes"` |
|||
VolumeServers []VolumeServer `json:"volume_servers"` |
|||
FilerNodes []FilerNode `json:"filer_nodes"` |
|||
DataCenters []DataCenter `json:"datacenters"` |
|||
LastUpdated time.Time `json:"last_updated"` |
|||
SystemHealth string `json:"system_health"` |
|||
} |
|||
|
|||
// S3 Bucket management data structures for templates
|
|||
type S3BucketsData struct { |
|||
Username string `json:"username"` |
|||
Buckets []S3Bucket `json:"buckets"` |
|||
TotalBuckets int `json:"total_buckets"` |
|||
TotalSize int64 `json:"total_size"` |
|||
LastUpdated time.Time `json:"last_updated"` |
|||
} |
|||
|
|||
type CreateBucketRequest struct { |
|||
Name string `json:"name" binding:"required"` |
|||
Region string `json:"region"` |
|||
} |
|||
|
|||
// Object Store Users management structures
|
|||
type ObjectStoreUser struct { |
|||
Username string `json:"username"` |
|||
Email string `json:"email"` |
|||
AccessKey string `json:"access_key"` |
|||
SecretKey string `json:"secret_key"` |
|||
Status string `json:"status"` |
|||
CreatedAt time.Time `json:"created_at"` |
|||
LastLogin time.Time `json:"last_login"` |
|||
Permissions []string `json:"permissions"` |
|||
} |
|||
|
|||
type ObjectStoreUsersData struct { |
|||
Username string `json:"username"` |
|||
Users []ObjectStoreUser `json:"users"` |
|||
TotalUsers int `json:"total_users"` |
|||
LastUpdated time.Time `json:"last_updated"` |
|||
} |
|||
|
|||
type FilerNode struct { |
|||
Address string `json:"address"` |
|||
DataCenter string `json:"datacenter"` |
|||
Rack string `json:"rack"` |
|||
Status string `json:"status"` |
|||
LastUpdated time.Time `json:"last_updated"` |
|||
} |
|||
|
|||
// GetAdminData retrieves admin data as a struct (for reuse by both JSON and HTML handlers)
|
|||
func (s *AdminServer) GetAdminData(username string) (AdminData, error) { |
|||
if username == "" { |
|||
username = "admin" |
|||
} |
|||
|
|||
// Get cluster topology
|
|||
topology, err := s.GetClusterTopology() |
|||
if err != nil { |
|||
glog.Errorf("Failed to get cluster topology: %v", err) |
|||
return AdminData{}, err |
|||
} |
|||
|
|||
// Get master nodes status
|
|||
masterNodes := s.getMasterNodesStatus() |
|||
|
|||
// Get filer nodes status
|
|||
filerNodes := s.getFilerNodesStatus() |
|||
|
|||
// Prepare admin data
|
|||
adminData := AdminData{ |
|||
Username: username, |
|||
ClusterStatus: s.determineClusterStatus(topology, masterNodes), |
|||
TotalVolumes: topology.TotalVolumes, |
|||
TotalFiles: topology.TotalFiles, |
|||
TotalSize: topology.TotalSize, |
|||
MasterNodes: masterNodes, |
|||
VolumeServers: topology.VolumeServers, |
|||
FilerNodes: filerNodes, |
|||
DataCenters: topology.DataCenters, |
|||
LastUpdated: topology.UpdatedAt, |
|||
SystemHealth: s.determineSystemHealth(topology, masterNodes), |
|||
} |
|||
|
|||
return adminData, nil |
|||
} |
|||
|
|||
// ShowAdmin displays the main admin page (now uses GetAdminData)
|
|||
func (s *AdminServer) ShowAdmin(c *gin.Context) { |
|||
username := c.GetString("username") |
|||
|
|||
adminData, err := s.GetAdminData(username) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get admin data: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
// Return JSON for API calls
|
|||
c.JSON(http.StatusOK, adminData) |
|||
} |
|||
|
|||
// ShowOverview displays cluster overview
|
|||
func (s *AdminServer) ShowOverview(c *gin.Context) { |
|||
topology, err := s.GetClusterTopology() |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |
|||
return |
|||
} |
|||
|
|||
c.JSON(http.StatusOK, topology) |
|||
} |
|||
|
|||
// S3 Bucket Management Handlers
|
|||
|
|||
// ShowS3Buckets displays the S3 buckets management page
|
|||
func (s *AdminServer) ShowS3Buckets(c *gin.Context) { |
|||
username := c.GetString("username") |
|||
|
|||
buckets, err := s.GetS3Buckets() |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get S3 buckets: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
// Calculate totals
|
|||
var totalSize int64 |
|||
for _, bucket := range buckets { |
|||
totalSize += bucket.Size |
|||
} |
|||
|
|||
data := S3BucketsData{ |
|||
Username: username, |
|||
Buckets: buckets, |
|||
TotalBuckets: len(buckets), |
|||
TotalSize: totalSize, |
|||
LastUpdated: time.Now(), |
|||
} |
|||
|
|||
c.JSON(http.StatusOK, data) |
|||
} |
|||
|
|||
// ShowBucketDetails displays detailed information about a specific bucket
|
|||
func (s *AdminServer) ShowBucketDetails(c *gin.Context) { |
|||
bucketName := c.Param("bucket") |
|||
if bucketName == "" { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) |
|||
return |
|||
} |
|||
|
|||
details, err := s.GetBucketDetails(bucketName) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bucket details: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
c.JSON(http.StatusOK, details) |
|||
} |
|||
|
|||
// CreateBucket creates a new S3 bucket
|
|||
func (s *AdminServer) CreateBucket(c *gin.Context) { |
|||
var req CreateBucketRequest |
|||
if err := c.ShouldBindJSON(&req); err != nil { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
// Validate bucket name (basic validation)
|
|||
if len(req.Name) < 3 || len(req.Name) > 63 { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name must be between 3 and 63 characters"}) |
|||
return |
|||
} |
|||
|
|||
err := s.CreateS3Bucket(req.Name) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create bucket: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
c.JSON(http.StatusCreated, gin.H{ |
|||
"message": "Bucket created successfully", |
|||
"bucket": req.Name, |
|||
}) |
|||
} |
|||
|
|||
// DeleteBucket deletes an S3 bucket
|
|||
func (s *AdminServer) DeleteBucket(c *gin.Context) { |
|||
bucketName := c.Param("bucket") |
|||
if bucketName == "" { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Bucket name is required"}) |
|||
return |
|||
} |
|||
|
|||
err := s.DeleteS3Bucket(bucketName) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete bucket: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
c.JSON(http.StatusOK, gin.H{ |
|||
"message": "Bucket deleted successfully", |
|||
"bucket": bucketName, |
|||
}) |
|||
} |
|||
|
|||
// ListBucketsAPI returns buckets as JSON API
|
|||
func (s *AdminServer) ListBucketsAPI(c *gin.Context) { |
|||
buckets, err := s.GetS3Buckets() |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |
|||
return |
|||
} |
|||
|
|||
c.JSON(http.StatusOK, gin.H{ |
|||
"buckets": buckets, |
|||
"count": len(buckets), |
|||
}) |
|||
} |
|||
|
|||
// getMasterNodesStatus checks status of all master nodes
|
|||
func (s *AdminServer) getMasterNodesStatus() []MasterNode { |
|||
var masterNodes []MasterNode |
|||
|
|||
// Since we have a single master address, create one entry
|
|||
var isLeader bool = true // Assume leader since it's the only master we know about
|
|||
var status string |
|||
|
|||
// Try to get leader info from this master
|
|||
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error { |
|||
_, err := client.GetMasterConfiguration(context.Background(), &master_pb.GetMasterConfigurationRequest{}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
// For now, assume this master is the leader since we can connect to it
|
|||
isLeader = true |
|||
return nil |
|||
}) |
|||
|
|||
if err != nil { |
|||
status = "unreachable" |
|||
isLeader = false |
|||
} else { |
|||
status = "active" |
|||
} |
|||
|
|||
masterNodes = append(masterNodes, MasterNode{ |
|||
Address: s.masterAddress, |
|||
IsLeader: isLeader, |
|||
Status: status, |
|||
}) |
|||
|
|||
return masterNodes |
|||
} |
|||
|
|||
// getFilerNodesStatus checks status of all filer nodes using master's ListClusterNodes
|
|||
func (s *AdminServer) getFilerNodesStatus() []FilerNode { |
|||
var filerNodes []FilerNode |
|||
|
|||
// Get filer nodes from master using ListClusterNodes
|
|||
err := s.WithMasterClient(func(client master_pb.SeaweedClient) error { |
|||
resp, err := client.ListClusterNodes(context.Background(), &master_pb.ListClusterNodesRequest{ |
|||
ClientType: cluster.FilerType, |
|||
}) |
|||
if err != nil { |
|||
return err |
|||
} |
|||
|
|||
// Process each filer node
|
|||
for _, node := range resp.ClusterNodes { |
|||
filerNodes = append(filerNodes, FilerNode{ |
|||
Address: node.Address, |
|||
DataCenter: node.DataCenter, |
|||
Rack: node.Rack, |
|||
Status: "active", // If it's in the cluster list, it's considered active
|
|||
LastUpdated: time.Now(), |
|||
}) |
|||
} |
|||
|
|||
return nil |
|||
}) |
|||
|
|||
if err != nil { |
|||
glog.Errorf("Failed to get filer nodes from master %s: %v", s.masterAddress, err) |
|||
// Return empty list if we can't get filer info from master
|
|||
return []FilerNode{} |
|||
} |
|||
|
|||
return filerNodes |
|||
} |
|||
|
|||
// determineClusterStatus analyzes cluster health
|
|||
func (s *AdminServer) determineClusterStatus(topology *ClusterTopology, masters []MasterNode) string { |
|||
// Check if we have an active leader
|
|||
hasActiveLeader := false |
|||
for _, master := range masters { |
|||
if master.IsLeader && master.Status == "active" { |
|||
hasActiveLeader = true |
|||
break |
|||
} |
|||
} |
|||
|
|||
if !hasActiveLeader { |
|||
return "critical" |
|||
} |
|||
|
|||
// Check volume server health
|
|||
activeServers := 0 |
|||
for _, vs := range topology.VolumeServers { |
|||
if vs.Status == "active" { |
|||
activeServers++ |
|||
} |
|||
} |
|||
|
|||
if activeServers == 0 { |
|||
return "critical" |
|||
} else if activeServers < len(topology.VolumeServers) { |
|||
return "warning" |
|||
} |
|||
|
|||
return "healthy" |
|||
} |
|||
|
|||
// determineSystemHealth provides overall system health assessment
|
|||
func (s *AdminServer) determineSystemHealth(topology *ClusterTopology, masters []MasterNode) string { |
|||
// Simple health calculation based on active components
|
|||
totalComponents := len(masters) + len(topology.VolumeServers) |
|||
activeComponents := 0 |
|||
|
|||
for _, master := range masters { |
|||
if master.Status == "active" { |
|||
activeComponents++ |
|||
} |
|||
} |
|||
|
|||
for _, vs := range topology.VolumeServers { |
|||
if vs.Status == "active" { |
|||
activeComponents++ |
|||
} |
|||
} |
|||
|
|||
if totalComponents == 0 { |
|||
return "unknown" |
|||
} |
|||
|
|||
healthPercent := float64(activeComponents) / float64(totalComponents) * 100 |
|||
|
|||
if healthPercent >= 95 { |
|||
return "excellent" |
|||
} else if healthPercent >= 80 { |
|||
return "good" |
|||
} else if healthPercent >= 60 { |
|||
return "fair" |
|||
} else { |
|||
return "poor" |
|||
} |
|||
} |
@ -0,0 +1,128 @@ |
|||
package dash |
|||
|
|||
import ( |
|||
"net/http" |
|||
|
|||
"github.com/gin-contrib/sessions" |
|||
"github.com/gin-gonic/gin" |
|||
) |
|||
|
|||
// ShowLogin displays the login page
|
|||
func (s *AdminServer) ShowLogin(c *gin.Context) { |
|||
// If authentication is not required, redirect to admin
|
|||
session := sessions.Default(c) |
|||
if session.Get("authenticated") == true { |
|||
c.Redirect(http.StatusSeeOther, "/admin") |
|||
return |
|||
} |
|||
|
|||
// For now, return a simple login form as JSON
|
|||
c.HTML(http.StatusOK, "login.html", gin.H{ |
|||
"title": "SeaweedFS Admin Login", |
|||
"error": c.Query("error"), |
|||
}) |
|||
} |
|||
|
|||
// HandleLogin handles login form submission
|
|||
func (s *AdminServer) HandleLogin(username, password string) gin.HandlerFunc { |
|||
return func(c *gin.Context) { |
|||
loginUsername := c.PostForm("username") |
|||
loginPassword := c.PostForm("password") |
|||
|
|||
if loginUsername == username && loginPassword == password { |
|||
session := sessions.Default(c) |
|||
session.Set("authenticated", true) |
|||
session.Set("username", loginUsername) |
|||
session.Save() |
|||
|
|||
c.Redirect(http.StatusSeeOther, "/admin") |
|||
return |
|||
} |
|||
|
|||
// Authentication failed
|
|||
c.Redirect(http.StatusSeeOther, "/login?error=Invalid credentials") |
|||
} |
|||
} |
|||
|
|||
// HandleLogout handles user logout
|
|||
func (s *AdminServer) HandleLogout(c *gin.Context) { |
|||
session := sessions.Default(c) |
|||
session.Clear() |
|||
session.Save() |
|||
c.Redirect(http.StatusSeeOther, "/login") |
|||
} |
|||
|
|||
// Additional methods for admin functionality
|
|||
func (s *AdminServer) GetClusterTopologyHandler(c *gin.Context) { |
|||
topology, err := s.GetClusterTopology() |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |
|||
return |
|||
} |
|||
c.JSON(http.StatusOK, topology) |
|||
} |
|||
|
|||
func (s *AdminServer) GetMasters(c *gin.Context) { |
|||
c.JSON(http.StatusOK, gin.H{"masters": []string{s.masterAddress}}) |
|||
} |
|||
|
|||
func (s *AdminServer) GetVolumeServers(c *gin.Context) { |
|||
topology, err := s.GetClusterTopology() |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |
|||
return |
|||
} |
|||
c.JSON(http.StatusOK, gin.H{"volume_servers": topology.VolumeServers}) |
|||
} |
|||
|
|||
func (s *AdminServer) AssignVolume(c *gin.Context) { |
|||
c.JSON(http.StatusNotImplemented, gin.H{"message": "Volume assignment not yet implemented"}) |
|||
} |
|||
|
|||
func (s *AdminServer) ListVolumes(c *gin.Context) { |
|||
c.JSON(http.StatusNotImplemented, gin.H{"message": "Volume listing not yet implemented"}) |
|||
} |
|||
|
|||
func (s *AdminServer) CreateVolume(c *gin.Context) { |
|||
c.JSON(http.StatusNotImplemented, gin.H{"message": "Volume creation not yet implemented"}) |
|||
} |
|||
|
|||
func (s *AdminServer) DeleteVolume(c *gin.Context) { |
|||
c.JSON(http.StatusNotImplemented, gin.H{"message": "Volume deletion not yet implemented"}) |
|||
} |
|||
|
|||
func (s *AdminServer) ReplicateVolume(c *gin.Context) { |
|||
c.JSON(http.StatusNotImplemented, gin.H{"message": "Volume replication not yet implemented"}) |
|||
} |
|||
|
|||
func (s *AdminServer) BrowseFiles(c *gin.Context) { |
|||
c.JSON(http.StatusNotImplemented, gin.H{"message": "File browsing not yet implemented"}) |
|||
} |
|||
|
|||
func (s *AdminServer) UploadFile(c *gin.Context) { |
|||
c.JSON(http.StatusNotImplemented, gin.H{"message": "File upload not yet implemented"}) |
|||
} |
|||
|
|||
func (s *AdminServer) DeleteFile(c *gin.Context) { |
|||
c.JSON(http.StatusNotImplemented, gin.H{"message": "File deletion not yet implemented"}) |
|||
} |
|||
|
|||
func (s *AdminServer) ShowMetrics(c *gin.Context) { |
|||
c.JSON(http.StatusNotImplemented, gin.H{"message": "Metrics display not yet implemented"}) |
|||
} |
|||
|
|||
func (s *AdminServer) GetMetricsData(c *gin.Context) { |
|||
c.JSON(http.StatusNotImplemented, gin.H{"message": "Metrics data not yet implemented"}) |
|||
} |
|||
|
|||
func (s *AdminServer) TriggerGC(c *gin.Context) { |
|||
c.JSON(http.StatusNotImplemented, gin.H{"message": "Garbage collection not yet implemented"}) |
|||
} |
|||
|
|||
func (s *AdminServer) CompactVolumes(c *gin.Context) { |
|||
c.JSON(http.StatusNotImplemented, gin.H{"message": "Volume compaction not yet implemented"}) |
|||
} |
|||
|
|||
func (s *AdminServer) GetMaintenanceStatus(c *gin.Context) { |
|||
c.JSON(http.StatusNotImplemented, gin.H{"message": "Maintenance status not yet implemented"}) |
|||
} |
@ -0,0 +1,27 @@ |
|||
package dash |
|||
|
|||
import ( |
|||
"net/http" |
|||
|
|||
"github.com/gin-contrib/sessions" |
|||
"github.com/gin-gonic/gin" |
|||
) |
|||
|
|||
// RequireAuth checks if user is authenticated
|
|||
func RequireAuth() gin.HandlerFunc { |
|||
return func(c *gin.Context) { |
|||
session := sessions.Default(c) |
|||
authenticated := session.Get("authenticated") |
|||
username := session.Get("username") |
|||
|
|||
if authenticated != true || username == nil { |
|||
c.Redirect(http.StatusTemporaryRedirect, "/login") |
|||
c.Abort() |
|||
return |
|||
} |
|||
|
|||
// Set username in context for use in handlers
|
|||
c.Set("username", username) |
|||
c.Next() |
|||
} |
|||
} |
@ -0,0 +1,45 @@ |
|||
package handlers |
|||
|
|||
import ( |
|||
"net/http" |
|||
|
|||
"github.com/gin-gonic/gin" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout" |
|||
) |
|||
|
|||
// AuthHandlers contains authentication-related HTTP handlers
|
|||
type AuthHandlers struct { |
|||
adminServer *dash.AdminServer |
|||
} |
|||
|
|||
// NewAuthHandlers creates a new instance of AuthHandlers
|
|||
func NewAuthHandlers(adminServer *dash.AdminServer) *AuthHandlers { |
|||
return &AuthHandlers{ |
|||
adminServer: adminServer, |
|||
} |
|||
} |
|||
|
|||
// ShowLogin displays the login page
|
|||
func (a *AuthHandlers) ShowLogin(c *gin.Context) { |
|||
errorMessage := c.Query("error") |
|||
|
|||
// Render login template
|
|||
c.Header("Content-Type", "text/html") |
|||
loginComponent := layout.LoginForm(c, "SeaweedFS Admin", errorMessage) |
|||
err := loginComponent.Render(c.Request.Context(), c.Writer) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render login template: " + err.Error()}) |
|||
return |
|||
} |
|||
} |
|||
|
|||
// HandleLogin handles login form submission
|
|||
func (a *AuthHandlers) HandleLogin(username, password string) gin.HandlerFunc { |
|||
return a.adminServer.HandleLogin(username, password) |
|||
} |
|||
|
|||
// HandleLogout handles user logout
|
|||
func (a *AuthHandlers) HandleLogout(c *gin.Context) { |
|||
a.adminServer.HandleLogout(c) |
|||
} |
@ -0,0 +1,202 @@ |
|||
package handlers |
|||
|
|||
import ( |
|||
"net/http" |
|||
"strconv" |
|||
|
|||
"github.com/gin-gonic/gin" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/view/app" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout" |
|||
) |
|||
|
|||
// ClusterHandlers contains all the HTTP handlers for cluster management
|
|||
type ClusterHandlers struct { |
|||
adminServer *dash.AdminServer |
|||
} |
|||
|
|||
// NewClusterHandlers creates a new instance of ClusterHandlers
|
|||
func NewClusterHandlers(adminServer *dash.AdminServer) *ClusterHandlers { |
|||
return &ClusterHandlers{ |
|||
adminServer: adminServer, |
|||
} |
|||
} |
|||
|
|||
// ShowClusterVolumeServers renders the cluster volume servers page
|
|||
func (h *ClusterHandlers) ShowClusterVolumeServers(c *gin.Context) { |
|||
// Get cluster volume servers data
|
|||
volumeServersData, err := h.adminServer.GetClusterVolumeServers() |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get cluster volume servers: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
// Set username
|
|||
username := c.GetString("username") |
|||
if username == "" { |
|||
username = "admin" |
|||
} |
|||
volumeServersData.Username = username |
|||
|
|||
// Render HTML template
|
|||
c.Header("Content-Type", "text/html") |
|||
volumeServersComponent := app.ClusterVolumeServers(*volumeServersData) |
|||
layoutComponent := layout.Layout(c, volumeServersComponent) |
|||
err = layoutComponent.Render(c.Request.Context(), c.Writer) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) |
|||
return |
|||
} |
|||
} |
|||
|
|||
// ShowClusterVolumes renders the cluster volumes page
|
|||
func (h *ClusterHandlers) ShowClusterVolumes(c *gin.Context) { |
|||
// Get pagination and sorting parameters from query string
|
|||
page := 1 |
|||
if p := c.Query("page"); p != "" { |
|||
if parsed, err := strconv.Atoi(p); err == nil && parsed > 0 { |
|||
page = parsed |
|||
} |
|||
} |
|||
|
|||
pageSize := 100 |
|||
if ps := c.Query("pageSize"); ps != "" { |
|||
if parsed, err := strconv.Atoi(ps); err == nil && parsed > 0 && parsed <= 1000 { |
|||
pageSize = parsed |
|||
} |
|||
} |
|||
|
|||
sortBy := c.DefaultQuery("sortBy", "id") |
|||
sortOrder := c.DefaultQuery("sortOrder", "asc") |
|||
|
|||
// Get cluster volumes data
|
|||
volumesData, err := h.adminServer.GetClusterVolumes(page, pageSize, sortBy, sortOrder) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get cluster volumes: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
// Set username
|
|||
username := c.GetString("username") |
|||
if username == "" { |
|||
username = "admin" |
|||
} |
|||
volumesData.Username = username |
|||
|
|||
// Render HTML template
|
|||
c.Header("Content-Type", "text/html") |
|||
volumesComponent := app.ClusterVolumes(*volumesData) |
|||
layoutComponent := layout.Layout(c, volumesComponent) |
|||
err = layoutComponent.Render(c.Request.Context(), c.Writer) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) |
|||
return |
|||
} |
|||
} |
|||
|
|||
// ShowClusterCollections renders the cluster collections page
|
|||
func (h *ClusterHandlers) ShowClusterCollections(c *gin.Context) { |
|||
// Get cluster collections data
|
|||
collectionsData, err := h.adminServer.GetClusterCollections() |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get cluster collections: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
// Set username
|
|||
username := c.GetString("username") |
|||
if username == "" { |
|||
username = "admin" |
|||
} |
|||
collectionsData.Username = username |
|||
|
|||
// Render HTML template
|
|||
c.Header("Content-Type", "text/html") |
|||
collectionsComponent := app.ClusterCollections(*collectionsData) |
|||
layoutComponent := layout.Layout(c, collectionsComponent) |
|||
err = layoutComponent.Render(c.Request.Context(), c.Writer) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) |
|||
return |
|||
} |
|||
} |
|||
|
|||
// ShowClusterMasters renders the cluster masters page
|
|||
func (h *ClusterHandlers) ShowClusterMasters(c *gin.Context) { |
|||
// Get cluster masters data
|
|||
mastersData, err := h.adminServer.GetClusterMasters() |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get cluster masters: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
// Set username
|
|||
username := c.GetString("username") |
|||
if username == "" { |
|||
username = "admin" |
|||
} |
|||
mastersData.Username = username |
|||
|
|||
// Render HTML template
|
|||
c.Header("Content-Type", "text/html") |
|||
mastersComponent := app.ClusterMasters(*mastersData) |
|||
layoutComponent := layout.Layout(c, mastersComponent) |
|||
err = layoutComponent.Render(c.Request.Context(), c.Writer) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) |
|||
return |
|||
} |
|||
} |
|||
|
|||
// ShowClusterFilers renders the cluster filers page
|
|||
func (h *ClusterHandlers) ShowClusterFilers(c *gin.Context) { |
|||
// Get cluster filers data
|
|||
filersData, err := h.adminServer.GetClusterFilers() |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get cluster filers: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
// Set username
|
|||
username := c.GetString("username") |
|||
if username == "" { |
|||
username = "admin" |
|||
} |
|||
filersData.Username = username |
|||
|
|||
// Render HTML template
|
|||
c.Header("Content-Type", "text/html") |
|||
filersComponent := app.ClusterFilers(*filersData) |
|||
layoutComponent := layout.Layout(c, filersComponent) |
|||
err = layoutComponent.Render(c.Request.Context(), c.Writer) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) |
|||
return |
|||
} |
|||
} |
|||
|
|||
// GetClusterTopology returns the cluster topology as JSON
|
|||
func (h *ClusterHandlers) GetClusterTopology(c *gin.Context) { |
|||
topology, err := h.adminServer.GetClusterTopology() |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |
|||
return |
|||
} |
|||
c.JSON(http.StatusOK, topology) |
|||
} |
|||
|
|||
// GetMasters returns master node information
|
|||
func (h *ClusterHandlers) GetMasters(c *gin.Context) { |
|||
// Simple master info
|
|||
c.JSON(http.StatusOK, gin.H{"masters": []gin.H{{"address": "localhost:9333", "status": "active"}}}) |
|||
} |
|||
|
|||
// GetVolumeServers returns volume server information
|
|||
func (h *ClusterHandlers) GetVolumeServers(c *gin.Context) { |
|||
topology, err := h.adminServer.GetClusterTopology() |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) |
|||
return |
|||
} |
|||
c.JSON(http.StatusOK, gin.H{"volume_servers": topology.VolumeServers}) |
|||
} |
@ -0,0 +1,447 @@ |
|||
package handlers |
|||
|
|||
import ( |
|||
"bytes" |
|||
"context" |
|||
"fmt" |
|||
"io" |
|||
"mime/multipart" |
|||
"net" |
|||
"net/http" |
|||
"path/filepath" |
|||
"strconv" |
|||
"strings" |
|||
"time" |
|||
|
|||
"github.com/gin-gonic/gin" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/view/app" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout" |
|||
"github.com/seaweedfs/seaweedfs/weed/glog" |
|||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
|||
) |
|||
|
|||
type FileBrowserHandlers struct { |
|||
adminServer *dash.AdminServer |
|||
} |
|||
|
|||
func NewFileBrowserHandlers(adminServer *dash.AdminServer) *FileBrowserHandlers { |
|||
return &FileBrowserHandlers{ |
|||
adminServer: adminServer, |
|||
} |
|||
} |
|||
|
|||
// ShowFileBrowser renders the file browser page
|
|||
func (h *FileBrowserHandlers) ShowFileBrowser(c *gin.Context) { |
|||
// Get path from query parameter, default to root
|
|||
path := c.DefaultQuery("path", "/") |
|||
|
|||
// Get file browser data
|
|||
browserData, err := h.adminServer.GetFileBrowser(path) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get file browser data: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
// Set username
|
|||
username := c.GetString("username") |
|||
if username == "" { |
|||
username = "admin" |
|||
} |
|||
browserData.Username = username |
|||
|
|||
// Render HTML template
|
|||
c.Header("Content-Type", "text/html") |
|||
browserComponent := app.FileBrowser(*browserData) |
|||
layoutComponent := layout.Layout(c, browserComponent) |
|||
err = layoutComponent.Render(c.Request.Context(), c.Writer) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) |
|||
return |
|||
} |
|||
} |
|||
|
|||
// DeleteFile handles file deletion API requests
|
|||
func (h *FileBrowserHandlers) DeleteFile(c *gin.Context) { |
|||
var request struct { |
|||
Path string `json:"path" binding:"required"` |
|||
} |
|||
|
|||
if err := c.ShouldBindJSON(&request); err != nil { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
// Delete file via filer
|
|||
err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { |
|||
_, err := client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{ |
|||
Directory: filepath.Dir(request.Path), |
|||
Name: filepath.Base(request.Path), |
|||
IsDeleteData: true, |
|||
IsRecursive: true, |
|||
IgnoreRecursiveError: false, |
|||
}) |
|||
return err |
|||
}) |
|||
|
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete file: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
c.JSON(http.StatusOK, gin.H{"message": "File deleted successfully"}) |
|||
} |
|||
|
|||
// DeleteMultipleFiles handles multiple file deletion API requests
|
|||
func (h *FileBrowserHandlers) DeleteMultipleFiles(c *gin.Context) { |
|||
var request struct { |
|||
Paths []string `json:"paths" binding:"required"` |
|||
} |
|||
|
|||
if err := c.ShouldBindJSON(&request); err != nil { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
if len(request.Paths) == 0 { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "No paths provided"}) |
|||
return |
|||
} |
|||
|
|||
var deletedCount int |
|||
var failedCount int |
|||
var errors []string |
|||
|
|||
// Delete each file/folder
|
|||
for _, path := range request.Paths { |
|||
err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { |
|||
_, err := client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{ |
|||
Directory: filepath.Dir(path), |
|||
Name: filepath.Base(path), |
|||
IsDeleteData: true, |
|||
IsRecursive: true, |
|||
IgnoreRecursiveError: false, |
|||
}) |
|||
return err |
|||
}) |
|||
|
|||
if err != nil { |
|||
failedCount++ |
|||
errors = append(errors, fmt.Sprintf("%s: %v", path, err)) |
|||
} else { |
|||
deletedCount++ |
|||
} |
|||
} |
|||
|
|||
// Prepare response
|
|||
response := map[string]interface{}{ |
|||
"deleted": deletedCount, |
|||
"failed": failedCount, |
|||
"total": len(request.Paths), |
|||
} |
|||
|
|||
if len(errors) > 0 { |
|||
response["errors"] = errors |
|||
} |
|||
|
|||
if deletedCount > 0 { |
|||
if failedCount == 0 { |
|||
response["message"] = fmt.Sprintf("Successfully deleted %d item(s)", deletedCount) |
|||
} else { |
|||
response["message"] = fmt.Sprintf("Deleted %d item(s), failed to delete %d item(s)", deletedCount, failedCount) |
|||
} |
|||
c.JSON(http.StatusOK, response) |
|||
} else { |
|||
response["message"] = "Failed to delete all selected items" |
|||
c.JSON(http.StatusInternalServerError, response) |
|||
} |
|||
} |
|||
|
|||
// CreateFolder handles folder creation requests
|
|||
func (h *FileBrowserHandlers) CreateFolder(c *gin.Context) { |
|||
var request struct { |
|||
Path string `json:"path" binding:"required"` |
|||
FolderName string `json:"folder_name" binding:"required"` |
|||
} |
|||
|
|||
if err := c.ShouldBindJSON(&request); err != nil { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
// Clean and validate folder name
|
|||
folderName := strings.TrimSpace(request.FolderName) |
|||
if folderName == "" || strings.Contains(folderName, "/") || strings.Contains(folderName, "\\") { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid folder name"}) |
|||
return |
|||
} |
|||
|
|||
// Create full path for new folder
|
|||
fullPath := filepath.Join(request.Path, folderName) |
|||
if !strings.HasPrefix(fullPath, "/") { |
|||
fullPath = "/" + fullPath |
|||
} |
|||
|
|||
// Create folder via filer
|
|||
err := h.adminServer.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error { |
|||
_, err := client.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{ |
|||
Directory: filepath.Dir(fullPath), |
|||
Entry: &filer_pb.Entry{ |
|||
Name: filepath.Base(fullPath), |
|||
IsDirectory: true, |
|||
Attributes: &filer_pb.FuseAttributes{ |
|||
FileMode: uint32(0755 | (1 << 31)), // Directory mode
|
|||
Uid: uint32(1000), |
|||
Gid: uint32(1000), |
|||
Crtime: time.Now().Unix(), |
|||
Mtime: time.Now().Unix(), |
|||
TtlSec: 0, |
|||
}, |
|||
}, |
|||
}) |
|||
return err |
|||
}) |
|||
|
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create folder: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
c.JSON(http.StatusOK, gin.H{"message": "Folder created successfully"}) |
|||
} |
|||
|
|||
// UploadFile handles file upload requests
|
|||
func (h *FileBrowserHandlers) UploadFile(c *gin.Context) { |
|||
// Get the current path
|
|||
currentPath := c.PostForm("path") |
|||
if currentPath == "" { |
|||
currentPath = "/" |
|||
} |
|||
|
|||
// Parse multipart form
|
|||
err := c.Request.ParseMultipartForm(100 << 20) // 100MB max memory
|
|||
if err != nil { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to parse multipart form: " + err.Error()}) |
|||
return |
|||
} |
|||
|
|||
// Get uploaded files (supports multiple files)
|
|||
files := c.Request.MultipartForm.File["files"] |
|||
if len(files) == 0 { |
|||
c.JSON(http.StatusBadRequest, gin.H{"error": "No files uploaded"}) |
|||
return |
|||
} |
|||
|
|||
var uploadResults []map[string]interface{} |
|||
var failedUploads []string |
|||
|
|||
// Process each uploaded file
|
|||
for _, fileHeader := range files { |
|||
// Validate file name
|
|||
fileName := fileHeader.Filename |
|||
if fileName == "" { |
|||
failedUploads = append(failedUploads, "invalid filename") |
|||
continue |
|||
} |
|||
|
|||
// Create full path for the file
|
|||
fullPath := filepath.Join(currentPath, fileName) |
|||
if !strings.HasPrefix(fullPath, "/") { |
|||
fullPath = "/" + fullPath |
|||
} |
|||
|
|||
// Open the file
|
|||
file, err := fileHeader.Open() |
|||
if err != nil { |
|||
failedUploads = append(failedUploads, fmt.Sprintf("%s: %v", fileName, err)) |
|||
continue |
|||
} |
|||
|
|||
// Upload file to filer
|
|||
err = h.uploadFileToFiler(fullPath, fileHeader) |
|||
file.Close() |
|||
|
|||
if err != nil { |
|||
failedUploads = append(failedUploads, fmt.Sprintf("%s: %v", fileName, err)) |
|||
} else { |
|||
uploadResults = append(uploadResults, map[string]interface{}{ |
|||
"name": fileName, |
|||
"size": fileHeader.Size, |
|||
"path": fullPath, |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// Prepare response
|
|||
response := map[string]interface{}{ |
|||
"uploaded": len(uploadResults), |
|||
"failed": len(failedUploads), |
|||
"files": uploadResults, |
|||
} |
|||
|
|||
if len(failedUploads) > 0 { |
|||
response["errors"] = failedUploads |
|||
} |
|||
|
|||
if len(uploadResults) > 0 { |
|||
if len(failedUploads) == 0 { |
|||
response["message"] = fmt.Sprintf("Successfully uploaded %d file(s)", len(uploadResults)) |
|||
} else { |
|||
response["message"] = fmt.Sprintf("Uploaded %d file(s), %d failed", len(uploadResults), len(failedUploads)) |
|||
} |
|||
c.JSON(http.StatusOK, response) |
|||
} else { |
|||
response["message"] = "All file uploads failed" |
|||
c.JSON(http.StatusInternalServerError, response) |
|||
} |
|||
} |
|||
|
|||
// uploadFileToFiler uploads a file directly to the filer using multipart form data
|
|||
func (h *FileBrowserHandlers) uploadFileToFiler(filePath string, fileHeader *multipart.FileHeader) error { |
|||
// Get filer address from admin server
|
|||
filerAddress := h.adminServer.GetFilerAddress() |
|||
if filerAddress == "" { |
|||
return fmt.Errorf("filer address not configured") |
|||
} |
|||
|
|||
// Validate and sanitize the filer address
|
|||
if err := h.validateFilerAddress(filerAddress); err != nil { |
|||
return fmt.Errorf("invalid filer address: %v", err) |
|||
} |
|||
|
|||
// Validate and sanitize the file path
|
|||
cleanFilePath, err := h.validateAndCleanFilePath(filePath) |
|||
if err != nil { |
|||
return fmt.Errorf("invalid file path: %v", err) |
|||
} |
|||
|
|||
// Open the file
|
|||
file, err := fileHeader.Open() |
|||
if err != nil { |
|||
return fmt.Errorf("failed to open file: %v", err) |
|||
} |
|||
defer file.Close() |
|||
|
|||
// Create multipart form data
|
|||
var body bytes.Buffer |
|||
writer := multipart.NewWriter(&body) |
|||
|
|||
// Create form file field
|
|||
part, err := writer.CreateFormFile("file", fileHeader.Filename) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to create form file: %v", err) |
|||
} |
|||
|
|||
// Copy file content to form
|
|||
_, err = io.Copy(part, file) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to copy file content: %v", err) |
|||
} |
|||
|
|||
// Close the writer to finalize the form
|
|||
err = writer.Close() |
|||
if err != nil { |
|||
return fmt.Errorf("failed to close multipart writer: %v", err) |
|||
} |
|||
|
|||
// Create the upload URL with validated components
|
|||
uploadURL := fmt.Sprintf("http://%s%s", filerAddress, cleanFilePath) |
|||
|
|||
// Create HTTP request
|
|||
req, err := http.NewRequest("POST", uploadURL, &body) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to create request: %v", err) |
|||
} |
|||
|
|||
// Set content type with boundary
|
|||
req.Header.Set("Content-Type", writer.FormDataContentType()) |
|||
|
|||
// Send request
|
|||
client := &http.Client{Timeout: 60 * time.Second} // Increased timeout for larger files
|
|||
resp, err := client.Do(req) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to upload file: %v", err) |
|||
} |
|||
defer resp.Body.Close() |
|||
|
|||
// Check response
|
|||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { |
|||
responseBody, _ := io.ReadAll(resp.Body) |
|||
return fmt.Errorf("upload failed with status %d: %s", resp.StatusCode, string(responseBody)) |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// validateFilerAddress validates that the filer address is safe to use
|
|||
func (h *FileBrowserHandlers) validateFilerAddress(address string) error { |
|||
if address == "" { |
|||
return fmt.Errorf("filer address cannot be empty") |
|||
} |
|||
|
|||
// Parse the address to validate it's a proper host:port format
|
|||
host, port, err := net.SplitHostPort(address) |
|||
if err != nil { |
|||
return fmt.Errorf("invalid address format: %v", err) |
|||
} |
|||
|
|||
// Validate host is not empty
|
|||
if host == "" { |
|||
return fmt.Errorf("host cannot be empty") |
|||
} |
|||
|
|||
// Validate port is numeric and in valid range
|
|||
if port == "" { |
|||
return fmt.Errorf("port cannot be empty") |
|||
} |
|||
|
|||
portNum, err := strconv.Atoi(port) |
|||
if err != nil { |
|||
return fmt.Errorf("invalid port number: %v", err) |
|||
} |
|||
|
|||
if portNum < 1 || portNum > 65535 { |
|||
return fmt.Errorf("port number must be between 1 and 65535") |
|||
} |
|||
|
|||
// Additional security: prevent private network access unless explicitly allowed
|
|||
// This helps prevent SSRF attacks to internal services
|
|||
ip := net.ParseIP(host) |
|||
if ip != nil { |
|||
// Check for localhost, private networks, and other dangerous addresses
|
|||
if ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() { |
|||
// Only allow if it's the configured filer (trusted)
|
|||
// In production, you might want to be more restrictive
|
|||
glog.V(2).Infof("Allowing access to private/local address: %s (configured filer)", address) |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// validateAndCleanFilePath validates and cleans the file path to prevent path traversal
|
|||
func (h *FileBrowserHandlers) validateAndCleanFilePath(filePath string) (string, error) { |
|||
if filePath == "" { |
|||
return "", fmt.Errorf("file path cannot be empty") |
|||
} |
|||
|
|||
// Clean the path to remove any .. or . components
|
|||
cleanPath := filepath.Clean(filePath) |
|||
|
|||
// Ensure the path starts with /
|
|||
if !strings.HasPrefix(cleanPath, "/") { |
|||
cleanPath = "/" + cleanPath |
|||
} |
|||
|
|||
// Prevent path traversal attacks
|
|||
if strings.Contains(cleanPath, "..") { |
|||
return "", fmt.Errorf("path traversal not allowed") |
|||
} |
|||
|
|||
// Additional validation: ensure path doesn't contain dangerous characters
|
|||
if strings.ContainsAny(cleanPath, "\x00\r\n") { |
|||
return "", fmt.Errorf("path contains invalid characters") |
|||
} |
|||
|
|||
return cleanPath, nil |
|||
} |
@ -0,0 +1,320 @@ |
|||
package handlers |
|||
|
|||
import ( |
|||
"net/http" |
|||
"time" |
|||
|
|||
"github.com/gin-gonic/gin" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/view/app" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout" |
|||
) |
|||
|
|||
// AdminHandlers contains all the HTTP handlers for the admin interface
|
|||
type AdminHandlers struct { |
|||
adminServer *dash.AdminServer |
|||
authHandlers *AuthHandlers |
|||
clusterHandlers *ClusterHandlers |
|||
fileBrowserHandlers *FileBrowserHandlers |
|||
} |
|||
|
|||
// NewAdminHandlers creates a new instance of AdminHandlers
|
|||
func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers { |
|||
authHandlers := NewAuthHandlers(adminServer) |
|||
clusterHandlers := NewClusterHandlers(adminServer) |
|||
fileBrowserHandlers := NewFileBrowserHandlers(adminServer) |
|||
return &AdminHandlers{ |
|||
adminServer: adminServer, |
|||
authHandlers: authHandlers, |
|||
clusterHandlers: clusterHandlers, |
|||
fileBrowserHandlers: fileBrowserHandlers, |
|||
} |
|||
} |
|||
|
|||
// SetupRoutes configures all the routes for the admin interface
|
|||
func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, password string) { |
|||
// Health check (no auth required)
|
|||
r.GET("/health", h.HealthCheck) |
|||
|
|||
if authRequired { |
|||
// Authentication routes (no auth required)
|
|||
r.GET("/login", h.authHandlers.ShowLogin) |
|||
r.POST("/login", h.authHandlers.HandleLogin(username, password)) |
|||
r.GET("/logout", h.authHandlers.HandleLogout) |
|||
|
|||
// Protected routes group
|
|||
protected := r.Group("/") |
|||
protected.Use(dash.RequireAuth()) |
|||
|
|||
// Main admin interface routes
|
|||
protected.GET("/", h.ShowDashboard) |
|||
protected.GET("/admin", h.ShowDashboard) |
|||
|
|||
// Object Store management routes
|
|||
protected.GET("/object-store/buckets", h.ShowS3Buckets) |
|||
protected.GET("/object-store/buckets/:bucket", h.ShowBucketDetails) |
|||
protected.GET("/object-store/users", h.ShowObjectStoreUsers) |
|||
|
|||
// File browser routes
|
|||
protected.GET("/files", h.fileBrowserHandlers.ShowFileBrowser) |
|||
|
|||
// Cluster management routes
|
|||
protected.GET("/cluster/masters", h.clusterHandlers.ShowClusterMasters) |
|||
protected.GET("/cluster/filers", h.clusterHandlers.ShowClusterFilers) |
|||
protected.GET("/cluster/volume-servers", h.clusterHandlers.ShowClusterVolumeServers) |
|||
protected.GET("/cluster/volumes", h.clusterHandlers.ShowClusterVolumes) |
|||
protected.GET("/cluster/collections", h.clusterHandlers.ShowClusterCollections) |
|||
|
|||
// API routes for AJAX calls
|
|||
api := protected.Group("/api") |
|||
{ |
|||
api.GET("/cluster/topology", h.clusterHandlers.GetClusterTopology) |
|||
api.GET("/cluster/masters", h.clusterHandlers.GetMasters) |
|||
api.GET("/cluster/volumes", h.clusterHandlers.GetVolumeServers) |
|||
api.GET("/admin", h.adminServer.ShowAdmin) // JSON API for admin data
|
|||
|
|||
// S3 API routes
|
|||
s3Api := api.Group("/s3") |
|||
{ |
|||
s3Api.GET("/buckets", h.adminServer.ListBucketsAPI) |
|||
s3Api.POST("/buckets", h.adminServer.CreateBucket) |
|||
s3Api.DELETE("/buckets/:bucket", h.adminServer.DeleteBucket) |
|||
s3Api.GET("/buckets/:bucket", h.adminServer.ShowBucketDetails) |
|||
} |
|||
|
|||
// File management API routes
|
|||
filesApi := api.Group("/files") |
|||
{ |
|||
filesApi.DELETE("/delete", h.fileBrowserHandlers.DeleteFile) |
|||
filesApi.DELETE("/delete-multiple", h.fileBrowserHandlers.DeleteMultipleFiles) |
|||
filesApi.POST("/create-folder", h.fileBrowserHandlers.CreateFolder) |
|||
filesApi.POST("/upload", h.fileBrowserHandlers.UploadFile) |
|||
} |
|||
} |
|||
} else { |
|||
// No authentication required - all routes are public
|
|||
r.GET("/", h.ShowDashboard) |
|||
r.GET("/admin", h.ShowDashboard) |
|||
|
|||
// Object Store management routes
|
|||
r.GET("/object-store/buckets", h.ShowS3Buckets) |
|||
r.GET("/object-store/buckets/:bucket", h.ShowBucketDetails) |
|||
r.GET("/object-store/users", h.ShowObjectStoreUsers) |
|||
|
|||
// File browser routes
|
|||
r.GET("/files", h.fileBrowserHandlers.ShowFileBrowser) |
|||
|
|||
// Cluster management routes
|
|||
r.GET("/cluster/masters", h.clusterHandlers.ShowClusterMasters) |
|||
r.GET("/cluster/filers", h.clusterHandlers.ShowClusterFilers) |
|||
r.GET("/cluster/volume-servers", h.clusterHandlers.ShowClusterVolumeServers) |
|||
r.GET("/cluster/volumes", h.clusterHandlers.ShowClusterVolumes) |
|||
r.GET("/cluster/collections", h.clusterHandlers.ShowClusterCollections) |
|||
|
|||
// API routes for AJAX calls
|
|||
api := r.Group("/api") |
|||
{ |
|||
api.GET("/cluster/topology", h.clusterHandlers.GetClusterTopology) |
|||
api.GET("/cluster/masters", h.clusterHandlers.GetMasters) |
|||
api.GET("/cluster/volumes", h.clusterHandlers.GetVolumeServers) |
|||
api.GET("/admin", h.adminServer.ShowAdmin) // JSON API for admin data
|
|||
|
|||
// S3 API routes
|
|||
s3Api := api.Group("/s3") |
|||
{ |
|||
s3Api.GET("/buckets", h.adminServer.ListBucketsAPI) |
|||
s3Api.POST("/buckets", h.adminServer.CreateBucket) |
|||
s3Api.DELETE("/buckets/:bucket", h.adminServer.DeleteBucket) |
|||
s3Api.GET("/buckets/:bucket", h.adminServer.ShowBucketDetails) |
|||
} |
|||
|
|||
// File management API routes
|
|||
filesApi := api.Group("/files") |
|||
{ |
|||
filesApi.DELETE("/delete", h.fileBrowserHandlers.DeleteFile) |
|||
filesApi.DELETE("/delete-multiple", h.fileBrowserHandlers.DeleteMultipleFiles) |
|||
filesApi.POST("/create-folder", h.fileBrowserHandlers.CreateFolder) |
|||
filesApi.POST("/upload", h.fileBrowserHandlers.UploadFile) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
// HealthCheck returns the health status of the admin interface
|
|||
func (h *AdminHandlers) HealthCheck(c *gin.Context) { |
|||
c.JSON(200, gin.H{"status": "ok"}) |
|||
} |
|||
|
|||
// ShowDashboard renders the main admin dashboard
|
|||
func (h *AdminHandlers) ShowDashboard(c *gin.Context) { |
|||
// Get admin data from the server
|
|||
adminData := h.getAdminData(c) |
|||
|
|||
// Render HTML template
|
|||
c.Header("Content-Type", "text/html") |
|||
adminComponent := app.Admin(adminData) |
|||
layoutComponent := layout.Layout(c, adminComponent) |
|||
err := layoutComponent.Render(c.Request.Context(), c.Writer) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) |
|||
return |
|||
} |
|||
} |
|||
|
|||
// ShowS3Buckets renders the S3 buckets management page
|
|||
func (h *AdminHandlers) ShowS3Buckets(c *gin.Context) { |
|||
// Get S3 buckets data from the server
|
|||
s3Data := h.getS3BucketsData(c) |
|||
|
|||
// Render HTML template
|
|||
c.Header("Content-Type", "text/html") |
|||
s3Component := app.S3Buckets(s3Data) |
|||
layoutComponent := layout.Layout(c, s3Component) |
|||
err := layoutComponent.Render(c.Request.Context(), c.Writer) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) |
|||
return |
|||
} |
|||
} |
|||
|
|||
// ShowBucketDetails returns detailed information about a specific bucket
|
|||
func (h *AdminHandlers) ShowBucketDetails(c *gin.Context) { |
|||
bucketName := c.Param("bucket") |
|||
details, err := h.adminServer.GetBucketDetails(bucketName) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get bucket details: " + err.Error()}) |
|||
return |
|||
} |
|||
c.JSON(http.StatusOK, details) |
|||
} |
|||
|
|||
// ShowObjectStoreUsers renders the object store users management page
|
|||
func (h *AdminHandlers) ShowObjectStoreUsers(c *gin.Context) { |
|||
// Get object store users data from the server
|
|||
usersData := h.getObjectStoreUsersData(c) |
|||
|
|||
// Render HTML template
|
|||
c.Header("Content-Type", "text/html") |
|||
usersComponent := app.ObjectStoreUsers(usersData) |
|||
layoutComponent := layout.Layout(c, usersComponent) |
|||
err := layoutComponent.Render(c.Request.Context(), c.Writer) |
|||
if err != nil { |
|||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to render template: " + err.Error()}) |
|||
return |
|||
} |
|||
} |
|||
|
|||
// getS3BucketsData retrieves S3 buckets data from the server
|
|||
func (h *AdminHandlers) getS3BucketsData(c *gin.Context) dash.S3BucketsData { |
|||
username := c.GetString("username") |
|||
if username == "" { |
|||
username = "admin" |
|||
} |
|||
|
|||
// Get S3 buckets
|
|||
buckets, err := h.adminServer.GetS3Buckets() |
|||
if err != nil { |
|||
// Return empty data on error
|
|||
return dash.S3BucketsData{ |
|||
Username: username, |
|||
Buckets: []dash.S3Bucket{}, |
|||
TotalBuckets: 0, |
|||
TotalSize: 0, |
|||
LastUpdated: time.Now(), |
|||
} |
|||
} |
|||
|
|||
// Calculate totals
|
|||
var totalSize int64 |
|||
for _, bucket := range buckets { |
|||
totalSize += bucket.Size |
|||
} |
|||
|
|||
return dash.S3BucketsData{ |
|||
Username: username, |
|||
Buckets: buckets, |
|||
TotalBuckets: len(buckets), |
|||
TotalSize: totalSize, |
|||
LastUpdated: time.Now(), |
|||
} |
|||
} |
|||
|
|||
// getObjectStoreUsersData retrieves object store users data from the server
|
|||
func (h *AdminHandlers) getObjectStoreUsersData(c *gin.Context) dash.ObjectStoreUsersData { |
|||
username := c.GetString("username") |
|||
if username == "" { |
|||
username = "admin" |
|||
} |
|||
|
|||
// Get object store users
|
|||
users, err := h.adminServer.GetObjectStoreUsers() |
|||
if err != nil { |
|||
// Return empty data on error
|
|||
return dash.ObjectStoreUsersData{ |
|||
Username: username, |
|||
Users: []dash.ObjectStoreUser{}, |
|||
TotalUsers: 0, |
|||
LastUpdated: time.Now(), |
|||
} |
|||
} |
|||
|
|||
return dash.ObjectStoreUsersData{ |
|||
Username: username, |
|||
Users: users, |
|||
TotalUsers: len(users), |
|||
LastUpdated: time.Now(), |
|||
} |
|||
} |
|||
|
|||
// getAdminData retrieves admin data from the server (now uses consolidated method)
|
|||
func (h *AdminHandlers) getAdminData(c *gin.Context) dash.AdminData { |
|||
username := c.GetString("username") |
|||
|
|||
// Use the consolidated GetAdminData method from AdminServer
|
|||
adminData, err := h.adminServer.GetAdminData(username) |
|||
if err != nil { |
|||
// Return default data when services are not available
|
|||
if username == "" { |
|||
username = "admin" |
|||
} |
|||
|
|||
masterNodes := []dash.MasterNode{ |
|||
{ |
|||
Address: "localhost:9333", |
|||
IsLeader: true, |
|||
Status: "unreachable", |
|||
}, |
|||
} |
|||
|
|||
return dash.AdminData{ |
|||
Username: username, |
|||
ClusterStatus: "warning", |
|||
TotalVolumes: 0, |
|||
TotalFiles: 0, |
|||
TotalSize: 0, |
|||
MasterNodes: masterNodes, |
|||
VolumeServers: []dash.VolumeServer{}, |
|||
FilerNodes: []dash.FilerNode{}, |
|||
DataCenters: []dash.DataCenter{}, |
|||
LastUpdated: time.Now(), |
|||
SystemHealth: "poor", |
|||
} |
|||
} |
|||
|
|||
return adminData |
|||
} |
|||
|
|||
// Helper functions
|
|||
func (h *AdminHandlers) determineClusterStatus(topology *dash.ClusterTopology, masters []dash.MasterNode) string { |
|||
if len(topology.VolumeServers) == 0 { |
|||
return "warning" |
|||
} |
|||
return "healthy" |
|||
} |
|||
|
|||
func (h *AdminHandlers) determineSystemHealth(topology *dash.ClusterTopology, masters []dash.MasterNode) string { |
|||
if len(topology.VolumeServers) > 0 && len(masters) > 0 { |
|||
return "good" |
|||
} |
|||
return "fair" |
|||
} |
@ -0,0 +1,217 @@ |
|||
/* SeaweedFS Dashboard Custom Styles */ |
|||
|
|||
/* Sidebar Styles */ |
|||
.sidebar { |
|||
position: fixed; |
|||
top: 56px; |
|||
bottom: 0; |
|||
left: 0; |
|||
z-index: 100; |
|||
padding: 48px 0 0; |
|||
box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); |
|||
} |
|||
|
|||
.sidebar-heading { |
|||
font-size: .75rem; |
|||
text-transform: uppercase; |
|||
} |
|||
|
|||
.sidebar .nav-link { |
|||
font-weight: 500; |
|||
color: #333; |
|||
} |
|||
|
|||
.sidebar .nav-link:hover { |
|||
color: #007bff; |
|||
} |
|||
|
|||
.sidebar .nav-link.active { |
|||
color: #007bff; |
|||
} |
|||
|
|||
.sidebar .nav-link:hover .feather, |
|||
.sidebar .nav-link.active .feather { |
|||
color: inherit; |
|||
} |
|||
|
|||
/* Main content area */ |
|||
main { |
|||
margin-left: 240px; |
|||
} |
|||
|
|||
@media (max-width: 767.98px) { |
|||
.sidebar { |
|||
top: 5rem; |
|||
} |
|||
main { |
|||
margin-left: 0; |
|||
} |
|||
} |
|||
|
|||
/* Custom card styles */ |
|||
.border-left-primary { |
|||
border-left: 0.25rem solid #4e73df !important; |
|||
} |
|||
|
|||
.border-left-success { |
|||
border-left: 0.25rem solid #1cc88a !important; |
|||
} |
|||
|
|||
.border-left-info { |
|||
border-left: 0.25rem solid #36b9cc !important; |
|||
} |
|||
|
|||
.border-left-warning { |
|||
border-left: 0.25rem solid #f6c23e !important; |
|||
} |
|||
|
|||
.border-left-danger { |
|||
border-left: 0.25rem solid #e74a3b !important; |
|||
} |
|||
|
|||
/* Status badges */ |
|||
.badge { |
|||
font-size: 0.875em; |
|||
} |
|||
|
|||
/* Progress bars */ |
|||
.progress { |
|||
background-color: #f8f9fc; |
|||
border: 1px solid #e3e6f0; |
|||
} |
|||
|
|||
.progress-bar { |
|||
font-size: 0.75rem; |
|||
font-weight: 700; |
|||
color: #fff; |
|||
text-align: center; |
|||
} |
|||
|
|||
/* Tables */ |
|||
.table { |
|||
color: #5a5c69; |
|||
} |
|||
|
|||
.table thead th { |
|||
vertical-align: bottom; |
|||
border-bottom: 1px solid #e3e6f0; |
|||
font-weight: 700; |
|||
color: #5a5c69; |
|||
background-color: #f8f9fc; |
|||
} |
|||
|
|||
.table-bordered { |
|||
border: 1px solid #e3e6f0; |
|||
} |
|||
|
|||
.table-bordered th, |
|||
.table-bordered td { |
|||
border: 1px solid #e3e6f0; |
|||
} |
|||
|
|||
/* Cards */ |
|||
.card { |
|||
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15) !important; |
|||
border: 1px solid #e3e6f0; |
|||
} |
|||
|
|||
.card-header { |
|||
background-color: #f8f9fc; |
|||
border-bottom: 1px solid #e3e6f0; |
|||
} |
|||
|
|||
/* Buttons */ |
|||
.btn-primary { |
|||
background-color: #4e73df; |
|||
border-color: #4e73df; |
|||
} |
|||
|
|||
.btn-primary:hover { |
|||
background-color: #2e59d9; |
|||
border-color: #2653d4; |
|||
} |
|||
|
|||
/* Text utilities */ |
|||
.text-gray-800 { |
|||
color: #5a5c69 !important; |
|||
} |
|||
|
|||
.text-gray-300 { |
|||
color: #dddfeb !important; |
|||
} |
|||
|
|||
/* Animation for HTMX updates */ |
|||
.htmx-indicator { |
|||
opacity: 0; |
|||
transition: opacity 500ms ease-in; |
|||
} |
|||
|
|||
.htmx-request .htmx-indicator { |
|||
opacity: 1; |
|||
} |
|||
|
|||
.htmx-request.htmx-indicator { |
|||
opacity: 1; |
|||
} |
|||
|
|||
/* Loading spinner */ |
|||
.spinner-border-sm { |
|||
width: 1rem; |
|||
height: 1rem; |
|||
} |
|||
|
|||
/* Custom utilities */ |
|||
.bg-gradient-primary { |
|||
background: linear-gradient(180deg, #4e73df 10%, #224abe 100%); |
|||
} |
|||
|
|||
.shadow { |
|||
box-shadow: 0 0.15rem 1.75rem 0 rgba(58, 59, 69, 0.15) !important; |
|||
} |
|||
|
|||
/* Collapsible menu styles */ |
|||
.nav-link[data-bs-toggle="collapse"] { |
|||
position: relative; |
|||
} |
|||
|
|||
.nav-link[data-bs-toggle="collapse"] .fa-chevron-down { |
|||
transition: transform 0.2s ease; |
|||
} |
|||
|
|||
.nav-link[data-bs-toggle="collapse"][aria-expanded="true"] .fa-chevron-down { |
|||
transform: rotate(180deg); |
|||
} |
|||
|
|||
.nav-link[data-bs-toggle="collapse"]:not(.collapsed) { |
|||
color: #007bff; |
|||
} |
|||
|
|||
.nav-link[data-bs-toggle="collapse"]:not(.collapsed) .fa-chevron-down { |
|||
color: #007bff; |
|||
} |
|||
|
|||
/* Submenu styles */ |
|||
.nav .nav { |
|||
border-left: 1px solid #e3e6f0; |
|||
margin-left: 0.5rem; |
|||
} |
|||
|
|||
.nav .nav .nav-link { |
|||
font-size: 0.875rem; |
|||
padding-left: 1rem; |
|||
} |
|||
|
|||
.nav .nav .nav-link:hover { |
|||
background-color: #f8f9fc; |
|||
} |
|||
|
|||
/* Responsive adjustments */ |
|||
@media (max-width: 576px) { |
|||
.card-body { |
|||
padding: 1rem; |
|||
} |
|||
|
|||
.h5 { |
|||
font-size: 1rem; |
|||
} |
|||
} |
1576
weed/admin/static/js/admin.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,351 @@ |
|||
package app |
|||
|
|||
import ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
templ Admin(data dash.AdminData) { |
|||
<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-tachometer-alt me-2"></i>Dashboard |
|||
</h1> |
|||
<div class="btn-toolbar mb-2 mb-md-0"> |
|||
<div class="btn-group me-2"> |
|||
<a href="/s3/buckets" class="btn btn-sm btn-primary"> |
|||
<i class="fas fa-cube me-1"></i>S3 Buckets |
|||
</a> |
|||
|
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div id="dashboard-content"> |
|||
<!-- Status 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"> |
|||
Cluster Status |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
<span class={fmt.Sprintf("badge bg-%s", getStatusColor(data.ClusterStatus))}> |
|||
{data.ClusterStatus} |
|||
</span> |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-heartbeat 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"> |
|||
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-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 Files |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{formatNumber(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-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> |
|||
|
|||
<!-- Master Nodes Status --> |
|||
<div class="row mb-4"> |
|||
<div class="col-lg-6"> |
|||
<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-server me-2"></i>Master Nodes |
|||
</h6> |
|||
</div> |
|||
<div class="card-body"> |
|||
<div class="table-responsive"> |
|||
<table class="table table-bordered" width="100%" cellspacing="0"> |
|||
<thead> |
|||
<tr> |
|||
<th>Address</th> |
|||
<th>Role</th> |
|||
<th>Status</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
for _, master := range data.MasterNodes { |
|||
<tr> |
|||
<td>{master.Address}</td> |
|||
<td> |
|||
if master.IsLeader { |
|||
<span class="badge bg-primary">Leader</span> |
|||
} else { |
|||
<span class="badge bg-secondary">Follower</span> |
|||
} |
|||
</td> |
|||
<td> |
|||
<span class={fmt.Sprintf("badge bg-%s", getStatusColor(master.Status))}> |
|||
{master.Status} |
|||
</span> |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- System Health --> |
|||
<div class="col-lg-6"> |
|||
<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-chart-pie me-2"></i>System Health |
|||
</h6> |
|||
</div> |
|||
<div class="card-body text-center"> |
|||
<div class="mb-3"> |
|||
<h3 class={fmt.Sprintf("text-%s", getHealthColor(data.SystemHealth))}> |
|||
{data.SystemHealth} |
|||
</h3> |
|||
</div> |
|||
<div class="row"> |
|||
<div class="col-4"> |
|||
<div class="card bg-light"> |
|||
<div class="card-body"> |
|||
<h5>{fmt.Sprintf("%d", len(data.MasterNodes))}</h5> |
|||
<small class="text-muted">Masters</small> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-4"> |
|||
<div class="card bg-light"> |
|||
<div class="card-body"> |
|||
<h5>{fmt.Sprintf("%d", len(data.VolumeServers))}</h5> |
|||
<small class="text-muted">Volume Servers</small> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="col-4"> |
|||
<div class="card bg-light"> |
|||
<div class="card-body"> |
|||
<h5>{fmt.Sprintf("%d", len(data.FilerNodes))}</h5> |
|||
<small class="text-muted">Filers</small> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Volume Servers --> |
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<div class="card shadow mb-4"> |
|||
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between"> |
|||
<h6 class="m-0 font-weight-bold text-primary"> |
|||
<i class="fas fa-database me-2"></i>Volume Servers |
|||
</h6> |
|||
<div class="dropdown no-arrow"> |
|||
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"> |
|||
<i class="fas fa-ellipsis-v fa-sm fa-fw text-gray-400"></i> |
|||
</a> |
|||
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in"> |
|||
<div class="dropdown-header">Actions:</div> |
|||
<a class="dropdown-item" href="/volumes">View Details</a> |
|||
<a class="dropdown-item" href="/cluster">Topology View</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="card-body"> |
|||
<div class="table-responsive"> |
|||
<table class="table table-hover" width="100%" cellspacing="0"> |
|||
<thead> |
|||
<tr> |
|||
<th>ID</th> |
|||
<th>Address</th> |
|||
<th>Data Center</th> |
|||
<th>Rack</th> |
|||
<th>Volumes</th> |
|||
<th>Capacity</th> |
|||
<th>Status</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
for _, vs := range data.VolumeServers { |
|||
<tr> |
|||
<td>{vs.ID}</td> |
|||
<td> |
|||
<a href={templ.SafeURL(fmt.Sprintf("http://%s", vs.PublicURL))} target="_blank"> |
|||
{vs.Address} |
|||
<i class="fas fa-external-link-alt ms-1 text-muted"></i> |
|||
</a> |
|||
</td> |
|||
<td>{vs.DataCenter}</td> |
|||
<td>{vs.Rack}</td> |
|||
<td> |
|||
<div class="progress" style="height: 20px;"> |
|||
<div class="progress-bar" role="progressbar" |
|||
style={fmt.Sprintf("width: %d%%", calculatePercent(vs.Volumes, vs.MaxVolumes))}> |
|||
{fmt.Sprintf("%d/%d", vs.Volumes, vs.MaxVolumes)} |
|||
</div> |
|||
</div> |
|||
</td> |
|||
<td>{formatBytes(vs.DiskUsage)} / {formatBytes(vs.DiskCapacity)}</td> |
|||
<td> |
|||
<span class={fmt.Sprintf("badge bg-%s", getStatusColor(vs.Status))}> |
|||
{vs.Status} |
|||
</span> |
|||
</td> |
|||
</tr> |
|||
} |
|||
if len(data.VolumeServers) == 0 { |
|||
<tr> |
|||
<td colspan="7" class="text-center text-muted py-4"> |
|||
<i class="fas fa-info-circle me-2"></i> |
|||
No volume servers found |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Filer Nodes --> |
|||
<div class="row mb-4"> |
|||
<div class="col-12"> |
|||
<div class="card shadow mb-4"> |
|||
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between"> |
|||
<h6 class="m-0 font-weight-bold text-primary"> |
|||
<i class="fas fa-folder me-2"></i>Filer Nodes |
|||
</h6> |
|||
<div class="dropdown no-arrow"> |
|||
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"> |
|||
<i class="fas fa-ellipsis-v fa-sm fa-fw text-gray-400"></i> |
|||
</a> |
|||
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in"> |
|||
<div class="dropdown-header">Actions:</div> |
|||
<a class="dropdown-item" href="/filer">File Browser</a> |
|||
<a class="dropdown-item" href="/cluster">Topology View</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="card-body"> |
|||
<div class="table-responsive"> |
|||
<table class="table table-hover" width="100%" cellspacing="0"> |
|||
<thead> |
|||
<tr> |
|||
<th>Address</th> |
|||
<th>Data Center</th> |
|||
<th>Rack</th> |
|||
<th>Status</th> |
|||
<th>Last Updated</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
for _, filer := range data.FilerNodes { |
|||
<tr> |
|||
<td> |
|||
<a href={templ.SafeURL(fmt.Sprintf("http://%s", filer.Address))} target="_blank"> |
|||
{filer.Address} |
|||
<i class="fas fa-external-link-alt ms-1 text-muted"></i> |
|||
</a> |
|||
</td> |
|||
<td>{filer.DataCenter}</td> |
|||
<td>{filer.Rack}</td> |
|||
<td> |
|||
<span class={fmt.Sprintf("badge bg-%s", getStatusColor(filer.Status))}> |
|||
{filer.Status} |
|||
</span> |
|||
</td> |
|||
<td>{filer.LastUpdated.Format("2006-01-02 15:04:05")}</td> |
|||
</tr> |
|||
} |
|||
if len(data.FilerNodes) == 0 { |
|||
<tr> |
|||
<td colspan="5" class="text-center text-muted py-4"> |
|||
<i class="fas fa-info-circle me-2"></i> |
|||
No filer nodes found |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</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> |
|||
} |
@ -0,0 +1,555 @@ |
|||
// Code generated by templ - DO NOT EDIT.
|
|||
|
|||
// templ: version: v0.3.833
|
|||
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 ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
func Admin(data dash.AdminData) 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) |
|||
templ_7745c5c3_Var1 := templ.GetChildren(ctx) |
|||
if templ_7745c5c3_Var1 == nil { |
|||
templ_7745c5c3_Var1 = templ.NopComponent |
|||
} |
|||
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=\"fas fa-tachometer-alt me-2\"></i>Dashboard</h1><div class=\"btn-toolbar mb-2 mb-md-0\"><div class=\"btn-group me-2\"><a href=\"/s3/buckets\" class=\"btn btn-sm btn-primary\"><i class=\"fas fa-cube me-1\"></i>S3 Buckets</a></div></div></div><div id=\"dashboard-content\"><!-- Status 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\">Cluster Status</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var2 = []any{fmt.Sprintf("badge bg-%s", getStatusColor(data.ClusterStatus))} |
|||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<span class=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var3 string |
|||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var2).String()) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 1, Col: 0} |
|||
} |
|||
_, 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, 3, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var4 string |
|||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.ClusterStatus) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 36, Col: 59} |
|||
} |
|||
_, 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, 4, "</span></div></div><div class=\"col-auto\"><i class=\"fas fa-heartbeat 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\">Total Volumes</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var5 string |
|||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalVolumes)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 57, Col: 73} |
|||
} |
|||
_, 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, 5, "</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-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 Files</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var6 string |
|||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(formatNumber(data.TotalFiles)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 77, Col: 66} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</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-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\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var7 string |
|||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(data.TotalSize)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 97, Col: 64} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></div><div class=\"col-auto\"><i class=\"fas fa-hdd fa-2x text-gray-300\"></i></div></div></div></div></div></div><!-- Master Nodes Status --><div class=\"row mb-4\"><div class=\"col-lg-6\"><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-server me-2\"></i>Master Nodes</h6></div><div class=\"card-body\"><div class=\"table-responsive\"><table class=\"table table-bordered\" width=\"100%\" cellspacing=\"0\"><thead><tr><th>Address</th><th>Role</th><th>Status</th></tr></thead> <tbody>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for _, master := range data.MasterNodes { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<tr><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var8 string |
|||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(master.Address) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 131, Col: 63} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if master.IsLeader { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span class=\"badge bg-primary\">Leader</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span class=\"badge bg-secondary\">Follower</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var9 = []any{fmt.Sprintf("badge bg-%s", getStatusColor(master.Status))} |
|||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var9...) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span class=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var10 string |
|||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var9).String()) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 1, Col: 0} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var11 string |
|||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(master.Status) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 141, Col: 66} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</span></td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</tbody></table></div></div></div></div><!-- System Health --><div class=\"col-lg-6\"><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-chart-pie me-2\"></i>System Health</h6></div><div class=\"card-body text-center\"><div class=\"mb-3\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var12 = []any{fmt.Sprintf("text-%s", getHealthColor(data.SystemHealth))} |
|||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var12...) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<h3 class=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var13 string |
|||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var12).String()) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 1, Col: 0} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var14 string |
|||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(data.SystemHealth) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 164, Col: 50} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</h3></div><div class=\"row\"><div class=\"col-4\"><div class=\"card bg-light\"><div class=\"card-body\"><h5>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var15 string |
|||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(data.MasterNodes))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 171, Col: 85} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</h5><small class=\"text-muted\">Masters</small></div></div></div><div class=\"col-4\"><div class=\"card bg-light\"><div class=\"card-body\"><h5>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var16 string |
|||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(data.VolumeServers))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 179, Col: 87} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</h5><small class=\"text-muted\">Volume Servers</small></div></div></div><div class=\"col-4\"><div class=\"card bg-light\"><div class=\"card-body\"><h5>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var17 string |
|||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(data.FilerNodes))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 187, Col: 84} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</h5><small class=\"text-muted\">Filers</small></div></div></div></div></div></div></div></div><!-- Volume Servers --><div class=\"row\"><div class=\"col-12\"><div class=\"card shadow mb-4\"><div class=\"card-header py-3 d-flex flex-row align-items-center justify-content-between\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-database me-2\"></i>Volume Servers</h6><div class=\"dropdown no-arrow\"><a class=\"dropdown-toggle\" href=\"#\" role=\"button\" data-bs-toggle=\"dropdown\"><i class=\"fas fa-ellipsis-v fa-sm fa-fw text-gray-400\"></i></a><div class=\"dropdown-menu dropdown-menu-right shadow animated--fade-in\"><div class=\"dropdown-header\">Actions:</div><a class=\"dropdown-item\" href=\"/volumes\">View Details</a> <a class=\"dropdown-item\" href=\"/cluster\">Topology View</a></div></div></div><div class=\"card-body\"><div class=\"table-responsive\"><table class=\"table table-hover\" width=\"100%\" cellspacing=\"0\"><thead><tr><th>ID</th><th>Address</th><th>Data Center</th><th>Rack</th><th>Volumes</th><th>Capacity</th><th>Status</th></tr></thead> <tbody>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for _, vs := range data.VolumeServers { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<tr><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var18 string |
|||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(vs.ID) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 234, Col: 54} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</td><td><a href=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var19 templ.SafeURL = templ.SafeURL(fmt.Sprintf("http://%s", vs.PublicURL)) |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var19))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" target=\"_blank\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var20 string |
|||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(vs.Address) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 237, Col: 63} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " <i class=\"fas fa-external-link-alt ms-1 text-muted\"></i></a></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var21 string |
|||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(vs.DataCenter) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 241, Col: 62} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var22 string |
|||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(vs.Rack) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 242, Col: 56} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</td><td><div class=\"progress\" style=\"height: 20px;\"><div class=\"progress-bar\" role=\"progressbar\" style=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var23 string |
|||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("width: %d%%", calculatePercent(vs.Volumes, vs.MaxVolumes))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 246, Col: 135} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var24 string |
|||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", vs.Volumes, vs.MaxVolumes)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 247, Col: 104} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div></div></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var25 string |
|||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(vs.DiskUsage)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 251, Col: 74} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " / ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var26 string |
|||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(vs.DiskCapacity)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 251, Col: 107} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var27 = []any{fmt.Sprintf("badge bg-%s", getStatusColor(vs.Status))} |
|||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var27...) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<span class=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var28 string |
|||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var27).String()) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 1, Col: 0} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var29 string |
|||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(vs.Status) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 254, Col: 62} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span></td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
if len(data.VolumeServers) == 0 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<tr><td colspan=\"7\" class=\"text-center text-muted py-4\"><i class=\"fas fa-info-circle me-2\"></i> No volume servers found</td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</tbody></table></div></div></div></div></div><!-- Filer Nodes --><div class=\"row mb-4\"><div class=\"col-12\"><div class=\"card shadow mb-4\"><div class=\"card-header py-3 d-flex flex-row align-items-center justify-content-between\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-folder me-2\"></i>Filer Nodes</h6><div class=\"dropdown no-arrow\"><a class=\"dropdown-toggle\" href=\"#\" role=\"button\" data-bs-toggle=\"dropdown\"><i class=\"fas fa-ellipsis-v fa-sm fa-fw text-gray-400\"></i></a><div class=\"dropdown-menu dropdown-menu-right shadow animated--fade-in\"><div class=\"dropdown-header\">Actions:</div><a class=\"dropdown-item\" href=\"/filer\">File Browser</a> <a class=\"dropdown-item\" href=\"/cluster\">Topology View</a></div></div></div><div class=\"card-body\"><div class=\"table-responsive\"><table class=\"table table-hover\" width=\"100%\" cellspacing=\"0\"><thead><tr><th>Address</th><th>Data Center</th><th>Rack</th><th>Status</th><th>Last Updated</th></tr></thead> <tbody>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for _, filer := range data.FilerNodes { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<tr><td><a href=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var30 templ.SafeURL = templ.SafeURL(fmt.Sprintf("http://%s", filer.Address)) |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var30))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" target=\"_blank\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var31 string |
|||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(filer.Address) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 311, Col: 66} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " <i class=\"fas fa-external-link-alt ms-1 text-muted\"></i></a></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var32 string |
|||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(filer.DataCenter) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 315, Col: 65} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var33 string |
|||
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(filer.Rack) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 316, Col: 59} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var34 = []any{fmt.Sprintf("badge bg-%s", getStatusColor(filer.Status))} |
|||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var34...) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<span class=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var35 string |
|||
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var34).String()) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 1, Col: 0} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var36 string |
|||
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(filer.Status) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 319, Col: 65} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "</span></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var37 string |
|||
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(filer.LastUpdated.Format("2006-01-02 15:04:05")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 322, Col: 96} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
if len(data.FilerNodes) == 0 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "<tr><td colspan=\"5\" class=\"text-center text-muted py-4\"><i class=\"fas fa-info-circle me-2\"></i> No filer nodes found</td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "</tbody></table></div></div></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: ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var38 string |
|||
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/admin.templ`, Line: 346, Col: 81} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "</small></div></div></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
var _ = templruntime.GeneratedTemplate |
@ -0,0 +1,360 @@ |
|||
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-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 Collections |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{fmt.Sprintf("%d", countActiveCollections(data.Collections))} |
|||
</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"> |
|||
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> |
|||
|
|||
<!-- Second Row of Summary Cards --> |
|||
<div class="row mb-4"> |
|||
<div class="col-xl-6 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 class="col-xl-6 col-md-6 mb-4"> |
|||
<div class="card border-left-dark 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-dark text-uppercase mb-1"> |
|||
Data Centers |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{fmt.Sprintf("%d", countUniqueCollectionDataCenters(data.Collections))} |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-building 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>Data Center</th> |
|||
<th>Replication</th> |
|||
<th>Volumes</th> |
|||
<th>Files</th> |
|||
<th>Size</th> |
|||
<th>TTL</th> |
|||
<th>Disk Type</th> |
|||
<th>Status</th> |
|||
<th>Actions</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
for _, collection := range data.Collections { |
|||
<tr> |
|||
<td> |
|||
<strong>{collection.Name}</strong> |
|||
</td> |
|||
<td> |
|||
<span class="badge bg-light text-dark">{collection.DataCenter}</span> |
|||
</td> |
|||
<td> |
|||
<span class="badge bg-info">{collection.Replication}</span> |
|||
</td> |
|||
<td> |
|||
<div class="d-flex align-items-center"> |
|||
<i class="fas fa-database me-2 text-muted"></i> |
|||
{fmt.Sprintf("%d", collection.VolumeCount)} |
|||
</div> |
|||
</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> |
|||
if collection.TTL != "" { |
|||
<span class="badge bg-warning text-dark">{collection.TTL}</span> |
|||
} else { |
|||
<span class="text-muted">None</span> |
|||
} |
|||
</td> |
|||
<td> |
|||
<span class="badge bg-secondary">{collection.DiskType}</span> |
|||
</td> |
|||
<td> |
|||
<span class={fmt.Sprintf("badge bg-%s", getStatusColor(collection.Status))}> |
|||
{collection.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="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 countActiveCollections(collections []dash.CollectionInfo) int { |
|||
count := 0 |
|||
for _, collection := range collections { |
|||
if collection.Status == "active" { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
|||
|
|||
func countUniqueCollectionDataCenters(collections []dash.CollectionInfo) int { |
|||
dcMap := make(map[string]bool) |
|||
for _, collection := range collections { |
|||
dcMap[collection.DataCenter] = true |
|||
} |
|||
return len(dcMap) |
|||
} |
@ -0,0 +1,346 @@ |
|||
// Code generated by templ - DO NOT EDIT.
|
|||
|
|||
// templ: version: v0.3.833
|
|||
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 ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
func ClusterCollections(data dash.ClusterCollectionsData) 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) |
|||
templ_7745c5c3_Var1 := templ.GetChildren(ctx) |
|||
if templ_7745c5c3_Var1 == nil { |
|||
templ_7745c5c3_Var1 = templ.NopComponent |
|||
} |
|||
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=\"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\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var2 string |
|||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalCollections)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 37, Col: 77} |
|||
} |
|||
_, 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, 2, "</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-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 Collections</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var3 string |
|||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", countActiveCollections(data.Collections))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 57, Col: 96} |
|||
} |
|||
_, 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, 3, "</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\">Total Volumes</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var4 string |
|||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalVolumes)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 77, Col: 73} |
|||
} |
|||
_, 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, 4, "</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\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var5 string |
|||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalFiles)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 97, Col: 71} |
|||
} |
|||
_, 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, 5, "</div></div><div class=\"col-auto\"><i class=\"fas fa-file fa-2x text-gray-300\"></i></div></div></div></div></div></div><!-- Second Row of Summary Cards --><div class=\"row mb-4\"><div class=\"col-xl-6 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\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var6 string |
|||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(data.TotalSize)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 120, Col: 64} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</div></div><div class=\"col-auto\"><i class=\"fas fa-hdd fa-2x text-gray-300\"></i></div></div></div></div></div><div class=\"col-xl-6 col-md-6 mb-4\"><div class=\"card border-left-dark 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-dark text-uppercase mb-1\">Data Centers</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var7 string |
|||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", countUniqueCollectionDataCenters(data.Collections))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 140, Col: 106} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></div><div class=\"col-auto\"><i class=\"fas fa-building 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 templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if len(data.Collections) > 0 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"table-responsive\"><table class=\"table table-hover\" id=\"collectionsTable\"><thead><tr><th>Collection Name</th><th>Data Center</th><th>Replication</th><th>Volumes</th><th>Files</th><th>Size</th><th>TTL</th><th>Disk Type</th><th>Status</th><th>Actions</th></tr></thead> <tbody>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for _, collection := range data.Collections { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<tr><td><strong>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var8 string |
|||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 181, Col: 68} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</strong></td><td><span class=\"badge bg-light text-dark\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var9 string |
|||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(collection.DataCenter) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 184, Col: 105} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span></td><td><span class=\"badge bg-info\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var10 string |
|||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Replication) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 187, Col: 95} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</span></td><td><div class=\"d-flex align-items-center\"><i class=\"fas fa-database me-2 text-muted\"></i> ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var11 string |
|||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", collection.VolumeCount)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 192, Col: 90} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</div></td><td><div class=\"d-flex align-items-center\"><i class=\"fas fa-file me-2 text-muted\"></i> ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var12 string |
|||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", collection.FileCount)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 198, Col: 88} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div></td><td><div class=\"d-flex align-items-center\"><i class=\"fas fa-hdd me-2 text-muted\"></i> ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var13 string |
|||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(collection.TotalSize)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 204, Col: 82} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if collection.TTL != "" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<span class=\"badge bg-warning text-dark\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var14 string |
|||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(collection.TTL) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 209, Col: 104} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<span class=\"text-muted\">None</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</td><td><span class=\"badge bg-secondary\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var15 string |
|||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(collection.DiskType) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 215, Col: 97} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</span></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var16 = []any{fmt.Sprintf("badge bg-%s", getStatusColor(collection.Status))} |
|||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<span class=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var17 string |
|||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var16).String()) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 1, Col: 0} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var18 string |
|||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Status) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 219, Col: 66} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</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=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var19 string |
|||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 234, Col: 93} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" onclick=\"confirmDeleteCollection(this)\"><i class=\"fas fa-trash\"></i></button></div></td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</tbody></table></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<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>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</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: ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var20 string |
|||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_collections.templ`, Line: 263, Col: 81} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</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>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
func countActiveCollections(collections []dash.CollectionInfo) int { |
|||
count := 0 |
|||
for _, collection := range collections { |
|||
if collection.Status == "active" { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
|||
|
|||
func countUniqueCollectionDataCenters(collections []dash.CollectionInfo) int { |
|||
dcMap := make(map[string]bool) |
|||
for _, collection := range collections { |
|||
dcMap[collection.DataCenter] = true |
|||
} |
|||
return len(dcMap) |
|||
} |
|||
|
|||
var _ = templruntime.GeneratedTemplate |
@ -0,0 +1,163 @@ |
|||
package app |
|||
|
|||
import ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
templ ClusterFilers(data dash.ClusterFilersData) { |
|||
<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-folder-open me-2"></i>Filers |
|||
</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="exportFilers()"> |
|||
<i class="fas fa-download me-1"></i>Export |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div id="filers-content"> |
|||
<!-- Summary Cards --> |
|||
<div class="row mb-4"> |
|||
<div class="col-xl-6 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 Filers |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{ fmt.Sprintf("%d", data.TotalFilers) } |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-folder-open fa-2x text-gray-300"></i> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div class="col-xl-6 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 Filers |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{ fmt.Sprintf("%d", countActiveFilers(data.Filers)) } |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-check-circle fa-2x text-gray-300"></i> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Filers 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-folder-open me-2"></i>Filers |
|||
</h6> |
|||
</div> |
|||
<div class="card-body"> |
|||
if len(data.Filers) > 0 { |
|||
<div class="table-responsive"> |
|||
<table class="table table-hover" id="filersTable"> |
|||
<thead> |
|||
<tr> |
|||
<th>Address</th> |
|||
<th>Version</th> |
|||
<th>Data Center</th> |
|||
<th>Rack</th> |
|||
<th>Created At</th> |
|||
<th>Status</th> |
|||
<th>Actions</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
for _, filer := range data.Filers { |
|||
<tr> |
|||
<td> |
|||
<a href={ templ.SafeURL(fmt.Sprintf("http://%s", filer.Address)) } target="_blank" class="text-decoration-none"> |
|||
{ filer.Address } |
|||
<i class="fas fa-external-link-alt ms-1 text-muted"></i> |
|||
</a> |
|||
</td> |
|||
<td> |
|||
<span class="badge bg-light text-dark">{ filer.Version }</span> |
|||
</td> |
|||
<td> |
|||
<span class="badge bg-light text-dark">{ filer.DataCenter }</span> |
|||
</td> |
|||
<td> |
|||
<span class="badge bg-light text-dark">{ filer.Rack }</span> |
|||
</td> |
|||
<td> |
|||
if !filer.CreatedAt.IsZero() { |
|||
{ filer.CreatedAt.Format("2006-01-02 15:04:05") } |
|||
} else { |
|||
<span class="text-muted">N/A</span> |
|||
} |
|||
</td> |
|||
<td> |
|||
<span class={ fmt.Sprintf("badge bg-%s", getStatusColor(filer.Status)) }> |
|||
{ filer.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="File Browser" onclick={ templ.ComponentScript{Call: fmt.Sprintf("window.open('http://%s', '_blank')", filer.Address)} }> |
|||
<i class="fas fa-folder-open"></i> |
|||
</button> |
|||
</div> |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
} else { |
|||
<div class="text-center py-5"> |
|||
<i class="fas fa-folder-open fa-3x text-muted mb-3"></i> |
|||
<h5 class="text-muted">No Filers Found</h5> |
|||
<p class="text-muted">No filer servers 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> |
|||
} |
|||
|
|||
func countActiveFilers(filers []dash.FilerInfo) int { |
|||
count := 0 |
|||
for _, filer := range filers { |
|||
if filer.Status == "active" { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
@ -0,0 +1,252 @@ |
|||
// Code generated by templ - DO NOT EDIT.
|
|||
|
|||
// templ: version: v0.3.833
|
|||
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 ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
func ClusterFilers(data dash.ClusterFilersData) 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) |
|||
templ_7745c5c3_Var1 := templ.GetChildren(ctx) |
|||
if templ_7745c5c3_Var1 == nil { |
|||
templ_7745c5c3_Var1 = templ.NopComponent |
|||
} |
|||
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=\"fas fa-folder-open me-2\"></i>Filers</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=\"exportFilers()\"><i class=\"fas fa-download me-1\"></i>Export</button></div></div></div><div id=\"filers-content\"><!-- Summary Cards --><div class=\"row mb-4\"><div class=\"col-xl-6 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 Filers</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var2 string |
|||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalFilers)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_filers.templ`, Line: 34, Col: 46} |
|||
} |
|||
_, 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, 2, "</div></div><div class=\"col-auto\"><i class=\"fas fa-folder-open fa-2x text-gray-300\"></i></div></div></div></div></div><div class=\"col-xl-6 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 Filers</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var3 string |
|||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", countActiveFilers(data.Filers))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_filers.templ`, Line: 54, Col: 60} |
|||
} |
|||
_, 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, 3, "</div></div><div class=\"col-auto\"><i class=\"fas fa-check-circle fa-2x text-gray-300\"></i></div></div></div></div></div></div><!-- Filers 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-folder-open me-2\"></i>Filers</h6></div><div class=\"card-body\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if len(data.Filers) > 0 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"table-responsive\"><table class=\"table table-hover\" id=\"filersTable\"><thead><tr><th>Address</th><th>Version</th><th>Data Center</th><th>Rack</th><th>Created At</th><th>Status</th><th>Actions</th></tr></thead> <tbody>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for _, filer := range data.Filers { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<tr><td><a href=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var4 templ.SafeURL = templ.SafeURL(fmt.Sprintf("http://%s", filer.Address)) |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" target=\"_blank\" class=\"text-decoration-none\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var5 string |
|||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(filer.Address) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_filers.templ`, Line: 93, Col: 27} |
|||
} |
|||
_, 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, 7, " <i class=\"fas fa-external-link-alt ms-1 text-muted\"></i></a></td><td><span class=\"badge bg-light text-dark\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var6 string |
|||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(filer.Version) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_filers.templ`, Line: 98, Col: 65} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span></td><td><span class=\"badge bg-light text-dark\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var7 string |
|||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(filer.DataCenter) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_filers.templ`, Line: 101, Col: 68} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</span></td><td><span class=\"badge bg-light text-dark\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var8 string |
|||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(filer.Rack) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_filers.templ`, Line: 104, Col: 62} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</span></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if !filer.CreatedAt.IsZero() { |
|||
var templ_7745c5c3_Var9 string |
|||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(filer.CreatedAt.Format("2006-01-02 15:04:05")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_filers.templ`, Line: 108, Col: 59} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span class=\"text-muted\">N/A</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var10 = []any{fmt.Sprintf("badge bg-%s", getStatusColor(filer.Status))} |
|||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span class=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var11 string |
|||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var10).String()) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_filers.templ`, Line: 1, Col: 0} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var12 string |
|||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(filer.Status) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_filers.templ`, Line: 115, Col: 26} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</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> ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("window.open('http://%s', '_blank')", filer.Address)}) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<button type=\"button\" class=\"btn btn-outline-secondary btn-sm\" title=\"File Browser\" onclick=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var13 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("window.open('http://%s', '_blank')", filer.Address)} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13.Call) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\"><i class=\"fas fa-folder-open\"></i></button></div></td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</tbody></table></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"text-center py-5\"><i class=\"fas fa-folder-open fa-3x text-muted mb-3\"></i><h5 class=\"text-muted\">No Filers Found</h5><p class=\"text-muted\">No filer servers are currently available in the cluster.</p></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</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: ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var14 string |
|||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_filers.templ`, Line: 148, Col: 67} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</small></div></div></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
func countActiveFilers(filers []dash.FilerInfo) int { |
|||
count := 0 |
|||
for _, filer := range filers { |
|||
if filer.Status == "active" { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
|||
|
|||
var _ = templruntime.GeneratedTemplate |
@ -0,0 +1,209 @@ |
|||
package app |
|||
|
|||
import ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
templ ClusterMasters(data dash.ClusterMastersData) { |
|||
<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-crown me-2"></i>Masters |
|||
</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="exportMasters()"> |
|||
<i class="fas fa-download me-1"></i>Export |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div id="masters-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 Masters |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{ fmt.Sprintf("%d", data.TotalMasters) } |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-crown 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 Masters |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{ fmt.Sprintf("%d", countActiveMasters(data.Masters)) } |
|||
</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"> |
|||
Leaders |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{ fmt.Sprintf("%d", data.LeaderCount) } |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-star 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"> |
|||
Cluster Health |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
if data.LeaderCount > 0 { |
|||
Healthy |
|||
} else { |
|||
Warning |
|||
} |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-heartbeat fa-2x text-gray-300"></i> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Masters 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-crown me-2"></i>Masters |
|||
</h6> |
|||
</div> |
|||
<div class="card-body"> |
|||
if len(data.Masters) > 0 { |
|||
<div class="table-responsive"> |
|||
<table class="table table-hover" id="mastersTable"> |
|||
<thead> |
|||
<tr> |
|||
<th>Address</th> |
|||
<th>Role</th> |
|||
<th>Suffrage</th> |
|||
<th>Status</th> |
|||
<th>Actions</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
for _, master := range data.Masters { |
|||
<tr> |
|||
<td> |
|||
<a href={ templ.SafeURL(fmt.Sprintf("http://%s", master.Address)) } target="_blank" class="text-decoration-none"> |
|||
{ master.Address } |
|||
<i class="fas fa-external-link-alt ms-1 text-muted"></i> |
|||
</a> |
|||
</td> |
|||
<td> |
|||
if master.IsLeader { |
|||
<span class="badge bg-warning text-dark"> |
|||
<i class="fas fa-star me-1"></i>Leader |
|||
</span> |
|||
} else { |
|||
<span class="badge bg-secondary"> |
|||
<i class="fas fa-circle me-1"></i>Follower |
|||
</span> |
|||
} |
|||
</td> |
|||
<td> |
|||
if master.Suffrage != "" { |
|||
<span class="badge bg-info text-dark"> |
|||
{ master.Suffrage } |
|||
</span> |
|||
} else { |
|||
<span class="text-muted">-</span> |
|||
} |
|||
</td> |
|||
<td> |
|||
<span class={ fmt.Sprintf("badge bg-%s", getStatusColor(master.Status)) }> |
|||
{ master.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="Manage"> |
|||
<i class="fas fa-cog"></i> |
|||
</button> |
|||
</div> |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
} else { |
|||
<div class="text-center py-5"> |
|||
<i class="fas fa-crown fa-3x text-muted mb-3"></i> |
|||
<h5 class="text-muted">No Masters Found</h5> |
|||
<p class="text-muted">No master servers 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> |
|||
} |
|||
|
|||
func countActiveMasters(masters []dash.MasterInfo) int { |
|||
count := 0 |
|||
for _, master := range masters { |
|||
if master.Status == "active" { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
@ -0,0 +1,247 @@ |
|||
// Code generated by templ - DO NOT EDIT.
|
|||
|
|||
// templ: version: v0.3.833
|
|||
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 ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
func ClusterMasters(data dash.ClusterMastersData) 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) |
|||
templ_7745c5c3_Var1 := templ.GetChildren(ctx) |
|||
if templ_7745c5c3_Var1 == nil { |
|||
templ_7745c5c3_Var1 = templ.NopComponent |
|||
} |
|||
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=\"fas fa-crown me-2\"></i>Masters</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=\"exportMasters()\"><i class=\"fas fa-download me-1\"></i>Export</button></div></div></div><div id=\"masters-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 Masters</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var2 string |
|||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalMasters)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_masters.templ`, Line: 34, Col: 47} |
|||
} |
|||
_, 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, 2, "</div></div><div class=\"col-auto\"><i class=\"fas fa-crown 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 Masters</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var3 string |
|||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", countActiveMasters(data.Masters))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_masters.templ`, Line: 54, Col: 62} |
|||
} |
|||
_, 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, 3, "</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\">Leaders</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var4 string |
|||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.LeaderCount)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_masters.templ`, Line: 74, Col: 46} |
|||
} |
|||
_, 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, 4, "</div></div><div class=\"col-auto\"><i class=\"fas fa-star 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\">Cluster Health</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.LeaderCount > 0 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "Healthy") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "Warning") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div></div><div class=\"col-auto\"><i class=\"fas fa-heartbeat fa-2x text-gray-300\"></i></div></div></div></div></div></div><!-- Masters 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-crown me-2\"></i>Masters</h6></div><div class=\"card-body\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if len(data.Masters) > 0 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"table-responsive\"><table class=\"table table-hover\" id=\"mastersTable\"><thead><tr><th>Address</th><th>Role</th><th>Suffrage</th><th>Status</th><th>Actions</th></tr></thead> <tbody>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for _, master := range data.Masters { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<tr><td><a href=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var5 templ.SafeURL = templ.SafeURL(fmt.Sprintf("http://%s", master.Address)) |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var5))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" target=\"_blank\" class=\"text-decoration-none\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var6 string |
|||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(master.Address) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_masters.templ`, Line: 135, Col: 28} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " <i class=\"fas fa-external-link-alt ms-1 text-muted\"></i></a></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if master.IsLeader { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span class=\"badge bg-warning text-dark\"><i class=\"fas fa-star me-1\"></i>Leader</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span class=\"badge bg-secondary\"><i class=\"fas fa-circle me-1\"></i>Follower</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if master.Suffrage != "" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<span class=\"badge bg-info text-dark\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var7 string |
|||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(master.Suffrage) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_masters.templ`, Line: 153, Col: 30} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span class=\"text-muted\">-</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var8 = []any{fmt.Sprintf("badge bg-%s", getStatusColor(master.Status))} |
|||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span class=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var9 string |
|||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String()) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_masters.templ`, Line: 1, Col: 0} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var10 string |
|||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(master.Status) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_masters.templ`, Line: 161, Col: 27} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</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=\"Manage\"><i class=\"fas fa-cog\"></i></button></div></td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</tbody></table></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<div class=\"text-center py-5\"><i class=\"fas fa-crown fa-3x text-muted mb-3\"></i><h5 class=\"text-muted\">No Masters Found</h5><p class=\"text-muted\">No master servers are currently available in the cluster.</p></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</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: ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var11 string |
|||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_masters.templ`, Line: 194, Col: 67} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</small></div></div></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
func countActiveMasters(masters []dash.MasterInfo) int { |
|||
count := 0 |
|||
for _, master := range masters { |
|||
if master.Status == "active" { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
|||
|
|||
var _ = templruntime.GeneratedTemplate |
@ -0,0 +1,221 @@ |
|||
package app |
|||
|
|||
import ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
templ ClusterVolumeServers(data dash.ClusterVolumeServersData) { |
|||
<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-server me-2"></i>Volume Servers |
|||
</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="exportVolumeServers()"> |
|||
<i class="fas fa-download me-1"></i>Export |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div id="hosts-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 Volume Servers |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{fmt.Sprintf("%d", data.TotalVolumeServers)} |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-server 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 Volume Servers |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{fmt.Sprintf("%d", countActiveVolumeServers(data.VolumeServers))} |
|||
</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"> |
|||
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 Capacity |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{formatBytes(data.TotalCapacity)} |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-hdd fa-2x text-gray-300"></i> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Hosts 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-server me-2"></i>Volume Servers |
|||
</h6> |
|||
</div> |
|||
<div class="card-body"> |
|||
if len(data.VolumeServers) > 0 { |
|||
<div class="table-responsive"> |
|||
<table class="table table-hover" id="hostsTable"> |
|||
<thead> |
|||
<tr> |
|||
<th>Server ID</th> |
|||
<th>Address</th> |
|||
<th>Data Center</th> |
|||
<th>Rack</th> |
|||
<th>Volumes</th> |
|||
<th>Capacity</th> |
|||
<th>Usage</th> |
|||
<th>Status</th> |
|||
<th>Actions</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
for _, host := range data.VolumeServers { |
|||
<tr> |
|||
<td> |
|||
<code>{host.ID}</code> |
|||
</td> |
|||
<td> |
|||
<a href={templ.SafeURL(fmt.Sprintf("http://%s", host.PublicURL))} target="_blank" class="text-decoration-none"> |
|||
{host.Address} |
|||
<i class="fas fa-external-link-alt ms-1 text-muted"></i> |
|||
</a> |
|||
</td> |
|||
<td> |
|||
<span class="badge bg-light text-dark">{host.DataCenter}</span> |
|||
</td> |
|||
<td> |
|||
<span class="badge bg-light text-dark">{host.Rack}</span> |
|||
</td> |
|||
<td> |
|||
<div class="d-flex align-items-center"> |
|||
<div class="progress me-2" style="width: 60px; height: 16px;"> |
|||
<div class="progress-bar" role="progressbar" |
|||
style={fmt.Sprintf("width: %d%%", calculatePercent(host.Volumes, host.MaxVolumes))}> |
|||
</div> |
|||
</div> |
|||
<small>{fmt.Sprintf("%d/%d", host.Volumes, host.MaxVolumes)}</small> |
|||
</div> |
|||
</td> |
|||
<td>{formatBytes(host.DiskCapacity)}</td> |
|||
<td> |
|||
<div class="d-flex align-items-center"> |
|||
<div class="progress me-2" style="width: 60px; height: 16px;"> |
|||
<div class="progress-bar" role="progressbar" |
|||
style={fmt.Sprintf("width: %d%%", calculatePercent(int(host.DiskUsage), int(host.DiskCapacity)))}> |
|||
</div> |
|||
</div> |
|||
<small>{formatBytes(host.DiskUsage)}</small> |
|||
</div> |
|||
</td> |
|||
<td> |
|||
<span class={fmt.Sprintf("badge bg-%s", getStatusColor(host.Status))}> |
|||
{host.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="Manage"> |
|||
<i class="fas fa-cog"></i> |
|||
</button> |
|||
</div> |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
} else { |
|||
<div class="text-center py-5"> |
|||
<i class="fas fa-server fa-3x text-muted mb-3"></i> |
|||
<h5 class="text-muted">No Volume Servers Found</h5> |
|||
<p class="text-muted">No volume servers 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> |
|||
} |
|||
|
|||
func countActiveVolumeServers(volumeServers []dash.VolumeServer) int { |
|||
count := 0 |
|||
for _, server := range volumeServers { |
|||
if server.Status == "active" { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
@ -0,0 +1,306 @@ |
|||
// Code generated by templ - DO NOT EDIT.
|
|||
|
|||
// templ: version: v0.3.833
|
|||
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 ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
func ClusterVolumeServers(data dash.ClusterVolumeServersData) 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) |
|||
templ_7745c5c3_Var1 := templ.GetChildren(ctx) |
|||
if templ_7745c5c3_Var1 == nil { |
|||
templ_7745c5c3_Var1 = templ.NopComponent |
|||
} |
|||
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=\"fas fa-server me-2\"></i>Volume Servers</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=\"exportVolumeServers()\"><i class=\"fas fa-download me-1\"></i>Export</button></div></div></div><div id=\"hosts-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 Volume Servers</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var2 string |
|||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalVolumeServers)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 34, Col: 79} |
|||
} |
|||
_, 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, 2, "</div></div><div class=\"col-auto\"><i class=\"fas fa-server 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 Volume Servers</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var3 string |
|||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", countActiveVolumeServers(data.VolumeServers))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 54, Col: 100} |
|||
} |
|||
_, 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, 3, "</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\">Total Volumes</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var4 string |
|||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalVolumes)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 74, Col: 73} |
|||
} |
|||
_, 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, 4, "</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 Capacity</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var5 string |
|||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(data.TotalCapacity)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 94, Col: 68} |
|||
} |
|||
_, 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, 5, "</div></div><div class=\"col-auto\"><i class=\"fas fa-hdd fa-2x text-gray-300\"></i></div></div></div></div></div></div><!-- Hosts 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-server me-2\"></i>Volume Servers</h6></div><div class=\"card-body\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if len(data.VolumeServers) > 0 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"table-responsive\"><table class=\"table table-hover\" id=\"hostsTable\"><thead><tr><th>Server ID</th><th>Address</th><th>Data Center</th><th>Rack</th><th>Volumes</th><th>Capacity</th><th>Usage</th><th>Status</th><th>Actions</th></tr></thead> <tbody>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for _, host := range data.VolumeServers { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<tr><td><code>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var6 string |
|||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(host.ID) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 134, Col: 58} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</code></td><td><a href=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var7 templ.SafeURL = templ.SafeURL(fmt.Sprintf("http://%s", host.PublicURL)) |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var7))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" target=\"_blank\" class=\"text-decoration-none\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var8 string |
|||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(host.Address) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 138, Col: 61} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, " <i class=\"fas fa-external-link-alt ms-1 text-muted\"></i></a></td><td><span class=\"badge bg-light text-dark\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var9 string |
|||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(host.DataCenter) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 143, Col: 99} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span></td><td><span class=\"badge bg-light text-dark\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var10 string |
|||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(host.Rack) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 146, Col: 93} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</span></td><td><div class=\"d-flex align-items-center\"><div class=\"progress me-2\" style=\"width: 60px; height: 16px;\"><div class=\"progress-bar\" role=\"progressbar\" style=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var11 string |
|||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("width: %d%%", calculatePercent(host.Volumes, host.MaxVolumes))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 152, Col: 139} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\"></div></div><small>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var12 string |
|||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", host.Volumes, host.MaxVolumes)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 155, Col: 107} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</small></div></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var13 string |
|||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(host.DiskCapacity)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 158, Col: 75} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</td><td><div class=\"d-flex align-items-center\"><div class=\"progress me-2\" style=\"width: 60px; height: 16px;\"><div class=\"progress-bar\" role=\"progressbar\" style=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var14 string |
|||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("width: %d%%", calculatePercent(int(host.DiskUsage), int(host.DiskCapacity)))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 163, Col: 153} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\"></div></div><small>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var15 string |
|||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(host.DiskUsage)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 166, Col: 83} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</small></div></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var16 = []any{fmt.Sprintf("badge bg-%s", getStatusColor(host.Status))} |
|||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var16...) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<span class=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var17 string |
|||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var16).String()) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 1, Col: 0} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var18 string |
|||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(host.Status) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 171, Col: 60} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</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=\"Manage\"><i class=\"fas fa-cog\"></i></button></div></td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</tbody></table></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<div class=\"text-center py-5\"><i class=\"fas fa-server fa-3x text-muted mb-3\"></i><h5 class=\"text-muted\">No Volume Servers Found</h5><p class=\"text-muted\">No volume servers are currently available in the cluster.</p></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</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: ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var19 string |
|||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volume_servers.templ`, Line: 206, Col: 81} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</small></div></div></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
func countActiveVolumeServers(volumeServers []dash.VolumeServer) int { |
|||
count := 0 |
|||
for _, server := range volumeServers { |
|||
if server.Status == "active" { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
|||
|
|||
var _ = templruntime.GeneratedTemplate |
@ -0,0 +1,414 @@ |
|||
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 |
|||
} |
@ -0,0 +1,661 @@ |
|||
// Code generated by templ - DO NOT EDIT.
|
|||
|
|||
// templ: version: v0.3.833
|
|||
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 ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
func ClusterVolumes(data dash.ClusterVolumesData) 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) |
|||
templ_7745c5c3_Var1 := templ.GetChildren(ctx) |
|||
if templ_7745c5c3_Var1 == nil { |
|||
templ_7745c5c3_Var1 = templ.NopComponent |
|||
} |
|||
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=\"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 templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.PageSize == 50 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " selected=\"selected\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, ">50 per page</option> <option value=\"100\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.PageSize == 100 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " selected=\"selected\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, ">100 per page</option> <option value=\"200\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.PageSize == 200 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " selected=\"selected\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, ">200 per page</option> <option value=\"500\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.PageSize == 500 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, " selected=\"selected\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, ">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\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var2 string |
|||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalVolumes)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 40, Col: 73} |
|||
} |
|||
_, 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, 10, "</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\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var3 string |
|||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", countActiveVolumes(data.Volumes))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 60, Col: 88} |
|||
} |
|||
_, 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, 11, "</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\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var4 string |
|||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", countUniqueDataCenters(data.Volumes))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 80, Col: 92} |
|||
} |
|||
_, 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, 12, "</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\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var5 string |
|||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(data.TotalSize)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 100, Col: 64} |
|||
} |
|||
_, 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, 13, "</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 templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if len(data.Volumes) > 0 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<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") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = getSortIcon("id", data.SortBy, data.SortOrder).Render(ctx, templ_7745c5c3_Buffer) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</a></th><th><a href=\"#\" onclick=\"sortTable('server')\" class=\"text-decoration-none text-dark\">Server") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = getSortIcon("server", data.SortBy, data.SortOrder).Render(ctx, templ_7745c5c3_Buffer) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</a></th><th><a href=\"#\" onclick=\"sortTable('datacenter')\" class=\"text-decoration-none text-dark\">Data Center") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = getSortIcon("datacenter", data.SortBy, data.SortOrder).Render(ctx, templ_7745c5c3_Buffer) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</a></th><th><a href=\"#\" onclick=\"sortTable('rack')\" class=\"text-decoration-none text-dark\">Rack") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = getSortIcon("rack", data.SortBy, data.SortOrder).Render(ctx, templ_7745c5c3_Buffer) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</a></th><th><a href=\"#\" onclick=\"sortTable('collection')\" class=\"text-decoration-none text-dark\">Collection") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = getSortIcon("collection", data.SortBy, data.SortOrder).Render(ctx, templ_7745c5c3_Buffer) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</a></th><th><a href=\"#\" onclick=\"sortTable('size')\" class=\"text-decoration-none text-dark\">Size") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = getSortIcon("size", data.SortBy, data.SortOrder).Render(ctx, templ_7745c5c3_Buffer) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</a></th><th><a href=\"#\" onclick=\"sortTable('filecount')\" class=\"text-decoration-none text-dark\">File Count") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = getSortIcon("filecount", data.SortBy, data.SortOrder).Render(ctx, templ_7745c5c3_Buffer) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</a></th><th><a href=\"#\" onclick=\"sortTable('replication')\" class=\"text-decoration-none text-dark\">Replication") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = getSortIcon("replication", data.SortBy, data.SortOrder).Render(ctx, templ_7745c5c3_Buffer) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</a></th><th><a href=\"#\" onclick=\"sortTable('status')\" class=\"text-decoration-none text-dark\">Status") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = getSortIcon("status", data.SortBy, data.SortOrder).Render(ctx, templ_7745c5c3_Buffer) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</a></th><th>Actions</th></tr></thead> <tbody>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for _, volume := range data.Volumes { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<tr><td><code>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var6 string |
|||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", volume.ID)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 186, Col: 79} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</code></td><td><a href=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var7 templ.SafeURL = templ.SafeURL(fmt.Sprintf("http://%s", volume.Server)) |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var7))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" target=\"_blank\" class=\"text-decoration-none\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var8 string |
|||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(volume.Server) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 190, Col: 62} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, " <i class=\"fas fa-external-link-alt ms-1 text-muted\"></i></a></td><td><span class=\"badge bg-light text-dark\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var9 string |
|||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(volume.DataCenter) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 195, Col: 101} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</span></td><td><span class=\"badge bg-light text-dark\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var10 string |
|||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(volume.Rack) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 198, Col: 95} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "</span></td><td><span class=\"badge bg-secondary\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var11 string |
|||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(volume.Collection) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 201, Col: 95} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</span></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var12 string |
|||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(volume.Size)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 203, Col: 69} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var13 string |
|||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", volume.FileCount)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 204, Col: 80} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</td><td><span class=\"badge bg-info\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var14 string |
|||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(volume.Replication) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 206, Col: 91} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</span></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var15 = []any{fmt.Sprintf("badge bg-%s", getStatusColor(volume.Status))} |
|||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<span class=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var16 string |
|||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var15).String()) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 1, Col: 0} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var17 string |
|||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(volume.Status) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 210, Col: 62} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</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>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</tbody></table></div><!-- Volume Summary --> <div class=\"d-flex justify-content-between align-items-center mt-3\"><div><small class=\"text-muted\">Showing ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var18 string |
|||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", (data.CurrentPage-1)*data.PageSize+1)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 239, Col: 98} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " to ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var19 string |
|||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", minInt(data.CurrentPage*data.PageSize, data.TotalVolumes))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 239, Col: 180} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, " of ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var20 string |
|||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalVolumes)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 239, Col: 222} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " volumes</small></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.TotalPages > 1 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<div><small class=\"text-muted\">Page ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var21 string |
|||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.CurrentPage)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 245, Col: 77} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " of ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var22 string |
|||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalPages)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 245, Col: 117} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</small></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "</div><!-- Pagination Controls --> ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.TotalPages > 1 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<div class=\"d-flex justify-content-center mt-3\"><nav aria-label=\"Volumes pagination\"><ul class=\"pagination pagination-sm mb-0\"><!-- Previous Button -->") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.CurrentPage > 1 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<li class=\"page-item\"><a class=\"page-link pagination-link\" href=\"#\" data-page=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var23 string |
|||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.CurrentPage-1)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 259, Col: 138} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\"><i class=\"fas fa-chevron-left\"></i></a></li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<li class=\"page-item disabled\"><span class=\"page-link\"><i class=\"fas fa-chevron-left\"></i></span></li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<!-- Page Numbers -->") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for i := maxInt(1, data.CurrentPage-2); i <= minInt(data.TotalPages, data.CurrentPage+2); i++ { |
|||
if i == data.CurrentPage { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "<li class=\"page-item active\"><span class=\"page-link\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var24 string |
|||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 275, Col: 93} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "</span></li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<li class=\"page-item\"><a class=\"page-link pagination-link\" href=\"#\" data-page=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var25 string |
|||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 279, Col: 125} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var26 string |
|||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 279, Col: 148} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</a></li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<!-- Next Button -->") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.CurrentPage < data.TotalPages { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<li class=\"page-item\"><a class=\"page-link pagination-link\" href=\"#\" data-page=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var27 string |
|||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.CurrentPage+1)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 287, Col: 138} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "\"><i class=\"fas fa-chevron-right\"></i></a></li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "<li class=\"page-item disabled\"><span class=\"page-link\"><i class=\"fas fa-chevron-right\"></i></span></li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "</ul></nav></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<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>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "</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: ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var28 string |
|||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_volumes.templ`, Line: 317, Col: 81} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</small></div></div></div><!-- JavaScript for pagination and sorting --><script>\n // Initialize pagination links when page loads\n document.addEventListener('DOMContentLoaded', function() {\n // Add click handlers to pagination links\n document.querySelectorAll('.pagination-link').forEach(link => {\n link.addEventListener('click', function(e) {\n e.preventDefault();\n const page = this.getAttribute('data-page');\n goToPage(page);\n });\n });\n });\n \n function goToPage(page) {\n const url = new URL(window.location);\n url.searchParams.set('page', page);\n window.location.href = url.toString();\n }\n \n function changePageSize() {\n const pageSize = document.getElementById('pageSizeSelect').value;\n const url = new URL(window.location);\n url.searchParams.set('pageSize', pageSize);\n url.searchParams.set('page', '1'); // Reset to first page\n window.location.href = url.toString();\n }\n \n function sortTable(column) {\n const url = new URL(window.location);\n const currentSort = url.searchParams.get('sortBy');\n const currentOrder = url.searchParams.get('sortOrder') || 'asc';\n \n let newOrder = 'asc';\n if (currentSort === column && currentOrder === 'asc') {\n newOrder = 'desc';\n }\n \n url.searchParams.set('sortBy', column);\n url.searchParams.set('sortOrder', newOrder);\n url.searchParams.set('page', '1'); // Reset to first page\n window.location.href = url.toString();\n }\n \n function exportVolumes() {\n // TODO: Implement volume export functionality\n alert('Export functionality to be implemented');\n }\n </script>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
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) |
|||
} |
|||
|
|||
func getSortIcon(column, currentSort, currentOrder string) 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) |
|||
templ_7745c5c3_Var29 := templ.GetChildren(ctx) |
|||
if templ_7745c5c3_Var29 == nil { |
|||
templ_7745c5c3_Var29 = templ.NopComponent |
|||
} |
|||
ctx = templ.ClearChildren(ctx) |
|||
if column != currentSort { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "<i class=\"fas fa-sort text-muted ms-1\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else if currentOrder == "asc" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<i class=\"fas fa-sort-up text-primary ms-1\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<i class=\"fas fa-sort-down text-primary ms-1\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
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 |
|||
} |
|||
|
|||
var _ = templruntime.GeneratedTemplate |
@ -0,0 +1,438 @@ |
|||
package app |
|||
|
|||
import ( |
|||
"fmt" |
|||
"path/filepath" |
|||
"strings" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
templ FileBrowser(data dash.FileBrowserData) { |
|||
<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"> |
|||
if data.IsBucketPath && data.BucketName != "" { |
|||
<i class="fas fa-cube me-2"></i>S3 Bucket: {data.BucketName} |
|||
} else { |
|||
<i class="fas fa-folder-open me-2"></i>File Browser |
|||
} |
|||
</h1> |
|||
<div class="btn-toolbar mb-2 mb-md-0"> |
|||
<div class="btn-group me-2"> |
|||
if data.IsBucketPath && data.BucketName != "" { |
|||
<a href="/object-store/buckets" class="btn btn-sm btn-outline-secondary"> |
|||
<i class="fas fa-arrow-left me-1"></i>Back to Buckets |
|||
</a> |
|||
} |
|||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="createFolder()"> |
|||
<i class="fas fa-folder-plus me-1"></i>New Folder |
|||
</button> |
|||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="uploadFile()"> |
|||
<i class="fas fa-upload me-1"></i>Upload |
|||
</button> |
|||
<button type="button" class="btn btn-sm btn-outline-danger" id="deleteSelectedBtn" onclick="confirmDeleteSelected()" style="display: none;"> |
|||
<i class="fas fa-trash me-1"></i>Delete Selected |
|||
</button> |
|||
<button type="button" class="btn btn-sm btn-outline-info" onclick="exportFileList()"> |
|||
<i class="fas fa-download me-1"></i>Export |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Breadcrumb Navigation --> |
|||
<nav aria-label="breadcrumb" class="mb-3"> |
|||
<ol class="breadcrumb"> |
|||
for i, crumb := range data.Breadcrumbs { |
|||
if i == len(data.Breadcrumbs)-1 { |
|||
<li class="breadcrumb-item active" aria-current="page"> |
|||
<i class="fas fa-folder me-1"></i>{ crumb.Name } |
|||
</li> |
|||
} else { |
|||
<li class="breadcrumb-item"> |
|||
<a href={ templ.SafeURL(fmt.Sprintf("/files?path=%s", crumb.Path)) } class="text-decoration-none"> |
|||
if crumb.Name == "Root" { |
|||
<i class="fas fa-home me-1"></i> |
|||
} else { |
|||
<i class="fas fa-folder me-1"></i> |
|||
} |
|||
{ crumb.Name } |
|||
</a> |
|||
</li> |
|||
} |
|||
} |
|||
</ol> |
|||
</nav> |
|||
|
|||
<!-- 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 Entries |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{ fmt.Sprintf("%d", data.TotalEntries) } |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-list 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"> |
|||
Directories |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{ fmt.Sprintf("%d", countDirectories(data.Entries)) } |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-folder 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"> |
|||
Files |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{ fmt.Sprintf("%d", countFiles(data.Entries)) } |
|||
</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-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> |
|||
|
|||
<!-- File Listing --> |
|||
<div class="card shadow mb-4"> |
|||
<div class="card-header py-3 d-flex justify-content-between align-items-center"> |
|||
<h6 class="m-0 font-weight-bold text-primary"> |
|||
<i class="fas fa-folder-open me-2"></i> |
|||
if data.CurrentPath == "/" { |
|||
Root Directory |
|||
} else if data.CurrentPath == "/buckets" { |
|||
S3 Buckets Directory |
|||
<a href="/object-store/buckets" class="btn btn-sm btn-outline-primary ms-2"> |
|||
<i class="fas fa-cube me-1"></i>Manage Buckets |
|||
</a> |
|||
} else { |
|||
{ filepath.Base(data.CurrentPath) } |
|||
} |
|||
</h6> |
|||
if data.ParentPath != data.CurrentPath { |
|||
<a href={ templ.SafeURL(fmt.Sprintf("/files?path=%s", data.ParentPath)) } class="btn btn-sm btn-outline-secondary"> |
|||
<i class="fas fa-arrow-up me-1"></i>Up |
|||
</a> |
|||
} |
|||
</div> |
|||
<div class="card-body"> |
|||
if len(data.Entries) > 0 { |
|||
<div class="table-responsive"> |
|||
<table class="table table-hover" id="fileTable"> |
|||
<thead> |
|||
<tr> |
|||
<th width="40px"> |
|||
<input type="checkbox" id="selectAll" onchange="toggleSelectAll()"> |
|||
</th> |
|||
<th>Name</th> |
|||
<th>Size</th> |
|||
<th>Type</th> |
|||
<th>Modified</th> |
|||
<th>Permissions</th> |
|||
<th>Actions</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
for _, entry := range data.Entries { |
|||
<tr> |
|||
<td> |
|||
<input type="checkbox" class="file-checkbox" value={ entry.FullPath }> |
|||
</td> |
|||
<td> |
|||
<div class="d-flex align-items-center"> |
|||
if entry.IsDirectory { |
|||
<i class="fas fa-folder text-warning me-2"></i> |
|||
<a href={ templ.SafeURL(fmt.Sprintf("/files?path=%s", entry.FullPath)) } class="text-decoration-none"> |
|||
{ entry.Name } |
|||
</a> |
|||
} else { |
|||
<i class={ fmt.Sprintf("fas %s text-muted me-2", getFileIcon(entry.Mime)) }></i> |
|||
<span>{ entry.Name }</span> |
|||
} |
|||
</div> |
|||
</td> |
|||
<td> |
|||
if entry.IsDirectory { |
|||
<span class="text-muted">—</span> |
|||
} else { |
|||
{ formatBytes(entry.Size) } |
|||
} |
|||
</td> |
|||
<td> |
|||
<span class="badge bg-light text-dark"> |
|||
if entry.IsDirectory { |
|||
Directory |
|||
} else { |
|||
{ getMimeDisplayName(entry.Mime) } |
|||
} |
|||
</span> |
|||
</td> |
|||
<td> |
|||
if !entry.ModTime.IsZero() { |
|||
{ entry.ModTime.Format("2006-01-02 15:04") } |
|||
} else { |
|||
<span class="text-muted">—</span> |
|||
} |
|||
</td> |
|||
<td> |
|||
<code class="small">{ entry.Mode }</code> |
|||
</td> |
|||
<td> |
|||
<div class="btn-group btn-group-sm"> |
|||
if !entry.IsDirectory { |
|||
<button type="button" class="btn btn-outline-primary btn-sm" title="Download" onclick={ templ.ComponentScript{Call: fmt.Sprintf("downloadFile('%s')", entry.FullPath)} }> |
|||
<i class="fas fa-download"></i> |
|||
</button> |
|||
<button type="button" class="btn btn-outline-info btn-sm" title="View" onclick={ templ.ComponentScript{Call: fmt.Sprintf("viewFile('%s')", entry.FullPath)} }> |
|||
<i class="fas fa-eye"></i> |
|||
</button> |
|||
} |
|||
<button type="button" class="btn btn-outline-secondary btn-sm" title="Properties" onclick={ templ.ComponentScript{Call: fmt.Sprintf("showProperties('%s')", entry.FullPath)} }> |
|||
<i class="fas fa-info"></i> |
|||
</button> |
|||
<button type="button" class="btn btn-outline-danger btn-sm" title="Delete" onclick={ templ.ComponentScript{Call: fmt.Sprintf("confirmDelete('%s')", entry.FullPath)} }> |
|||
<i class="fas fa-trash"></i> |
|||
</button> |
|||
</div> |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
} else { |
|||
<div class="text-center py-5"> |
|||
<i class="fas fa-folder-open fa-3x text-muted mb-3"></i> |
|||
<h5 class="text-muted">Empty Directory</h5> |
|||
<p class="text-muted">This directory contains no files or subdirectories.</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> |
|||
|
|||
<!-- Create Folder Modal --> |
|||
<div class="modal fade" id="createFolderModal" tabindex="-1" aria-labelledby="createFolderModalLabel" aria-hidden="true"> |
|||
<div class="modal-dialog"> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h5 class="modal-title" id="createFolderModalLabel"> |
|||
<i class="fas fa-folder-plus me-2"></i>Create New Folder |
|||
</h5> |
|||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
|||
</div> |
|||
<div class="modal-body"> |
|||
<form id="createFolderForm"> |
|||
<div class="mb-3"> |
|||
<label for="folderName" class="form-label">Folder Name</label> |
|||
<input type="text" class="form-control" id="folderName" name="folderName" required |
|||
placeholder="Enter folder name" maxlength="255"> |
|||
<div class="form-text"> |
|||
Folder names cannot contain / or \ characters. |
|||
</div> |
|||
</div> |
|||
<input type="hidden" id="currentPath" name="currentPath" value={ data.CurrentPath }> |
|||
</form> |
|||
</div> |
|||
<div class="modal-footer"> |
|||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> |
|||
<button type="button" class="btn btn-primary" onclick="submitCreateFolder()"> |
|||
<i class="fas fa-folder-plus me-1"></i>Create Folder |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Upload File Modal --> |
|||
<div class="modal fade" id="uploadFileModal" tabindex="-1" aria-labelledby="uploadFileModalLabel" aria-hidden="true"> |
|||
<div class="modal-dialog modal-lg"> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h5 class="modal-title" id="uploadFileModalLabel"> |
|||
<i class="fas fa-upload me-2"></i>Upload Files |
|||
</h5> |
|||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
|||
</div> |
|||
<div class="modal-body"> |
|||
<form id="uploadFileForm" enctype="multipart/form-data"> |
|||
<div class="mb-3"> |
|||
<label for="fileInput" class="form-label">Select Files</label> |
|||
<input type="file" class="form-control" id="fileInput" name="files" multiple required> |
|||
<div class="form-text"> |
|||
Choose one or more files to upload to the current directory. You can select multiple files by holding Ctrl (Cmd on Mac) while clicking. |
|||
</div> |
|||
</div> |
|||
<input type="hidden" id="uploadPath" name="path" value={ data.CurrentPath }> |
|||
|
|||
<!-- File List Preview --> |
|||
<div id="fileListPreview" class="mb-3" style="display: none;"> |
|||
<label class="form-label">Selected Files:</label> |
|||
<div id="selectedFilesList" class="border rounded p-2 bg-light"> |
|||
<!-- Files will be listed here --> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Upload Progress --> |
|||
<div class="mb-3" id="uploadProgress" style="display: none;"> |
|||
<label class="form-label">Upload Progress:</label> |
|||
<div class="progress mb-2"> |
|||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div> |
|||
</div> |
|||
<div id="uploadStatus" class="small text-muted"> |
|||
Preparing upload... |
|||
</div> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
<div class="modal-footer"> |
|||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> |
|||
<button type="button" class="btn btn-primary" onclick="submitUploadFile()"> |
|||
<i class="fas fa-upload me-1"></i>Upload Files |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
} |
|||
|
|||
func countDirectories(entries []dash.FileEntry) int { |
|||
count := 0 |
|||
for _, entry := range entries { |
|||
if entry.IsDirectory { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
|||
|
|||
func countFiles(entries []dash.FileEntry) int { |
|||
count := 0 |
|||
for _, entry := range entries { |
|||
if !entry.IsDirectory { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
|||
|
|||
func getFileIcon(mime string) string { |
|||
switch { |
|||
case strings.HasPrefix(mime, "image/"): |
|||
return "fa-image" |
|||
case strings.HasPrefix(mime, "video/"): |
|||
return "fa-video" |
|||
case strings.HasPrefix(mime, "audio/"): |
|||
return "fa-music" |
|||
case strings.HasPrefix(mime, "text/"): |
|||
return "fa-file-text" |
|||
case mime == "application/pdf": |
|||
return "fa-file-pdf" |
|||
case mime == "application/zip" || strings.Contains(mime, "archive"): |
|||
return "fa-file-archive" |
|||
case mime == "application/json": |
|||
return "fa-file-code" |
|||
case strings.Contains(mime, "script") || strings.Contains(mime, "javascript"): |
|||
return "fa-file-code" |
|||
default: |
|||
return "fa-file" |
|||
} |
|||
} |
|||
|
|||
func getMimeDisplayName(mime string) string { |
|||
switch mime { |
|||
case "text/plain": |
|||
return "Text" |
|||
case "text/html": |
|||
return "HTML" |
|||
case "application/json": |
|||
return "JSON" |
|||
case "application/pdf": |
|||
return "PDF" |
|||
case "image/jpeg": |
|||
return "JPEG" |
|||
case "image/png": |
|||
return "PNG" |
|||
case "image/gif": |
|||
return "GIF" |
|||
case "video/mp4": |
|||
return "MP4" |
|||
case "audio/mpeg": |
|||
return "MP3" |
|||
case "application/zip": |
|||
return "ZIP" |
|||
default: |
|||
if strings.HasPrefix(mime, "image/") { |
|||
return "Image" |
|||
} else if strings.HasPrefix(mime, "video/") { |
|||
return "Video" |
|||
} else if strings.HasPrefix(mime, "audio/") { |
|||
return "Audio" |
|||
} else if strings.HasPrefix(mime, "text/") { |
|||
return "Text" |
|||
} |
|||
return "File" |
|||
} |
|||
} |
@ -0,0 +1,607 @@ |
|||
// Code generated by templ - DO NOT EDIT.
|
|||
|
|||
// templ: version: v0.3.833
|
|||
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 ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
"path/filepath" |
|||
"strings" |
|||
) |
|||
|
|||
func FileBrowser(data dash.FileBrowserData) 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) |
|||
templ_7745c5c3_Var1 := templ.GetChildren(ctx) |
|||
if templ_7745c5c3_Var1 == nil { |
|||
templ_7745c5c3_Var1 = templ.NopComponent |
|||
} |
|||
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\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.IsBucketPath && data.BucketName != "" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<i class=\"fas fa-cube me-2\"></i>S3 Bucket: ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var2 string |
|||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.BucketName) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 14, Col: 63} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<i class=\"fas fa-folder-open me-2\"></i>File Browser") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</h1><div class=\"btn-toolbar mb-2 mb-md-0\"><div class=\"btn-group me-2\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.IsBucketPath && data.BucketName != "" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<a href=\"/object-store/buckets\" class=\"btn btn-sm btn-outline-secondary\"><i class=\"fas fa-arrow-left me-1\"></i>Back to Buckets</a> ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<button type=\"button\" class=\"btn btn-sm btn-outline-primary\" onclick=\"createFolder()\"><i class=\"fas fa-folder-plus me-1\"></i>New Folder</button> <button type=\"button\" class=\"btn btn-sm btn-outline-secondary\" onclick=\"uploadFile()\"><i class=\"fas fa-upload me-1\"></i>Upload</button> <button type=\"button\" class=\"btn btn-sm btn-outline-danger\" id=\"deleteSelectedBtn\" onclick=\"confirmDeleteSelected()\" style=\"display: none;\"><i class=\"fas fa-trash me-1\"></i>Delete Selected</button> <button type=\"button\" class=\"btn btn-sm btn-outline-info\" onclick=\"exportFileList()\"><i class=\"fas fa-download me-1\"></i>Export</button></div></div></div><!-- Breadcrumb Navigation --><nav aria-label=\"breadcrumb\" class=\"mb-3\"><ol class=\"breadcrumb\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for i, crumb := range data.Breadcrumbs { |
|||
if i == len(data.Breadcrumbs)-1 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<li class=\"breadcrumb-item active\" aria-current=\"page\"><i class=\"fas fa-folder me-1\"></i>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var3 string |
|||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(crumb.Name) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 48, Col: 52} |
|||
} |
|||
_, 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, 8, "</li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<li class=\"breadcrumb-item\"><a href=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var4 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files?path=%s", crumb.Path)) |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" class=\"text-decoration-none\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if crumb.Name == "Root" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<i class=\"fas fa-home me-1\"></i> ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<i class=\"fas fa-folder me-1\"></i> ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
var templ_7745c5c3_Var5 string |
|||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(crumb.Name) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 58, Col: 19} |
|||
} |
|||
_, 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, 13, "</a></li>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</ol></nav><!-- 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 Entries</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var6 string |
|||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalEntries)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 77, Col: 46} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div></div><div class=\"col-auto\"><i class=\"fas fa-list 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\">Directories</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var7 string |
|||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", countDirectories(data.Entries))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 97, Col: 59} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</div></div><div class=\"col-auto\"><i class=\"fas fa-folder 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\">Files</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var8 string |
|||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", countFiles(data.Entries))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 117, Col: 53} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</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-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\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var9 string |
|||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(data.TotalSize)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 137, Col: 37} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "</div></div><div class=\"col-auto\"><i class=\"fas fa-hdd fa-2x text-gray-300\"></i></div></div></div></div></div></div><!-- File Listing --><div class=\"card shadow mb-4\"><div class=\"card-header py-3 d-flex justify-content-between align-items-center\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-folder-open me-2\"></i> ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.CurrentPath == "/" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "Root Directory") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else if data.CurrentPath == "/buckets" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "S3 Buckets Directory <a href=\"/object-store/buckets\" class=\"btn btn-sm btn-outline-primary ms-2\"><i class=\"fas fa-cube me-1\"></i>Manage Buckets</a>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
var templ_7745c5c3_Var10 string |
|||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(filepath.Base(data.CurrentPath)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 162, Col: 37} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</h6>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if data.ParentPath != data.CurrentPath { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<a href=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var11 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files?path=%s", data.ParentPath)) |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var11))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" class=\"btn btn-sm btn-outline-secondary\"><i class=\"fas fa-arrow-up me-1\"></i>Up</a>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</div><div class=\"card-body\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if len(data.Entries) > 0 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<div class=\"table-responsive\"><table class=\"table table-hover\" id=\"fileTable\"><thead><tr><th width=\"40px\"><input type=\"checkbox\" id=\"selectAll\" onchange=\"toggleSelectAll()\"></th><th>Name</th><th>Size</th><th>Type</th><th>Modified</th><th>Permissions</th><th>Actions</th></tr></thead> <tbody>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for _, entry := range data.Entries { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<tr><td><input type=\"checkbox\" class=\"file-checkbox\" value=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var12 string |
|||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(entry.FullPath) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 192, Col: 77} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\"></td><td><div class=\"d-flex align-items-center\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if entry.IsDirectory { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<i class=\"fas fa-folder text-warning me-2\"></i> <a href=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var13 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files?path=%s", entry.FullPath)) |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var13))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" class=\"text-decoration-none\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var14 string |
|||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Name) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 199, Col: 25} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</a>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
var templ_7745c5c3_Var15 = []any{fmt.Sprintf("fas %s text-muted me-2", getFileIcon(entry.Mime))} |
|||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var15...) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<i class=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var16 string |
|||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var15).String()) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 1, Col: 0} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "\"></i> <span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var17 string |
|||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Name) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 203, Col: 30} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</div></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if entry.IsDirectory { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<span class=\"text-muted\">—</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
var templ_7745c5c3_Var18 string |
|||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(entry.Size)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 211, Col: 36} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</td><td><span class=\"badge bg-light text-dark\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if entry.IsDirectory { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "Directory") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
var templ_7745c5c3_Var19 string |
|||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(getMimeDisplayName(entry.Mime)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 219, Col: 44} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</span></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if !entry.ModTime.IsZero() { |
|||
var templ_7745c5c3_Var20 string |
|||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(entry.ModTime.Format("2006-01-02 15:04")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 225, Col: 53} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<span class=\"text-muted\">—</span>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</td><td><code class=\"small\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var21 string |
|||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Mode) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 231, Col: 42} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</code></td><td><div class=\"btn-group btn-group-sm\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if !entry.IsDirectory { |
|||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("downloadFile('%s')", entry.FullPath)}) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<button type=\"button\" class=\"btn btn-outline-primary btn-sm\" title=\"Download\" onclick=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var22 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("downloadFile('%s')", entry.FullPath)} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22.Call) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\"><i class=\"fas fa-download\"></i></button> ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("viewFile('%s')", entry.FullPath)}) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<button type=\"button\" class=\"btn btn-outline-info btn-sm\" title=\"View\" onclick=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var23 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("viewFile('%s')", entry.FullPath)} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23.Call) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "\"><i class=\"fas fa-eye\"></i></button> ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("showProperties('%s')", entry.FullPath)}) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<button type=\"button\" class=\"btn btn-outline-secondary btn-sm\" title=\"Properties\" onclick=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var24 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("showProperties('%s')", entry.FullPath)} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24.Call) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\"><i class=\"fas fa-info\"></i></button> ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templ.RenderScriptItems(ctx, templ_7745c5c3_Buffer, templ.ComponentScript{Call: fmt.Sprintf("confirmDelete('%s')", entry.FullPath)}) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "<button type=\"button\" class=\"btn btn-outline-danger btn-sm\" title=\"Delete\" onclick=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var25 templ.ComponentScript = templ.ComponentScript{Call: fmt.Sprintf("confirmDelete('%s')", entry.FullPath)} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25.Call) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "\"><i class=\"fas fa-trash\"></i></button></div></td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "</tbody></table></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} else { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "<div class=\"text-center py-5\"><i class=\"fas fa-folder-open fa-3x text-muted mb-3\"></i><h5 class=\"text-muted\">Empty Directory</h5><p class=\"text-muted\">This directory contains no files or subdirectories.</p></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</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: ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var26 string |
|||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 271, Col: 66} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</small></div></div><!-- Create Folder Modal --><div class=\"modal fade\" id=\"createFolderModal\" tabindex=\"-1\" aria-labelledby=\"createFolderModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"createFolderModalLabel\"><i class=\"fas fa-folder-plus me-2\"></i>Create New Folder</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><form id=\"createFolderForm\"><div class=\"mb-3\"><label for=\"folderName\" class=\"form-label\">Folder Name</label> <input type=\"text\" class=\"form-control\" id=\"folderName\" name=\"folderName\" required placeholder=\"Enter folder name\" maxlength=\"255\"><div class=\"form-text\">Folder names cannot contain / or \\ characters.</div></div><input type=\"hidden\" id=\"currentPath\" name=\"currentPath\" value=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var27 string |
|||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(data.CurrentPath) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 296, Col: 87} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\"></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"submitCreateFolder()\"><i class=\"fas fa-folder-plus me-1\"></i>Create Folder</button></div></div></div></div><!-- Upload File Modal --><div class=\"modal fade\" id=\"uploadFileModal\" tabindex=\"-1\" aria-labelledby=\"uploadFileModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog modal-lg\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"uploadFileModalLabel\"><i class=\"fas fa-upload me-2\"></i>Upload Files</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><form id=\"uploadFileForm\" enctype=\"multipart/form-data\"><div class=\"mb-3\"><label for=\"fileInput\" class=\"form-label\">Select Files</label> <input type=\"file\" class=\"form-control\" id=\"fileInput\" name=\"files\" multiple required><div class=\"form-text\">Choose one or more files to upload to the current directory. You can select multiple files by holding Ctrl (Cmd on Mac) while clicking.</div></div><input type=\"hidden\" id=\"uploadPath\" name=\"path\" value=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var28 string |
|||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(data.CurrentPath) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/file_browser.templ`, Line: 328, Col: 79} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "\"><!-- File List Preview --><div id=\"fileListPreview\" class=\"mb-3\" style=\"display: none;\"><label class=\"form-label\">Selected Files:</label><div id=\"selectedFilesList\" class=\"border rounded p-2 bg-light\"><!-- Files will be listed here --></div></div><!-- Upload Progress --><div class=\"mb-3\" id=\"uploadProgress\" style=\"display: none;\"><label class=\"form-label\">Upload Progress:</label><div class=\"progress mb-2\"><div class=\"progress-bar progress-bar-striped progress-bar-animated\" role=\"progressbar\" style=\"width: 0%\" aria-valuenow=\"0\" aria-valuemin=\"0\" aria-valuemax=\"100\">0%</div></div><div id=\"uploadStatus\" class=\"small text-muted\">Preparing upload...</div></div></form></div><div class=\"modal-footer\"><button type=\"button\" class=\"btn btn-secondary\" data-bs-dismiss=\"modal\">Cancel</button> <button type=\"button\" class=\"btn btn-primary\" onclick=\"submitUploadFile()\"><i class=\"fas fa-upload me-1\"></i>Upload Files</button></div></div></div></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
func countDirectories(entries []dash.FileEntry) int { |
|||
count := 0 |
|||
for _, entry := range entries { |
|||
if entry.IsDirectory { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
|||
|
|||
func countFiles(entries []dash.FileEntry) int { |
|||
count := 0 |
|||
for _, entry := range entries { |
|||
if !entry.IsDirectory { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
|||
|
|||
func getFileIcon(mime string) string { |
|||
switch { |
|||
case strings.HasPrefix(mime, "image/"): |
|||
return "fa-image" |
|||
case strings.HasPrefix(mime, "video/"): |
|||
return "fa-video" |
|||
case strings.HasPrefix(mime, "audio/"): |
|||
return "fa-music" |
|||
case strings.HasPrefix(mime, "text/"): |
|||
return "fa-file-text" |
|||
case mime == "application/pdf": |
|||
return "fa-file-pdf" |
|||
case mime == "application/zip" || strings.Contains(mime, "archive"): |
|||
return "fa-file-archive" |
|||
case mime == "application/json": |
|||
return "fa-file-code" |
|||
case strings.Contains(mime, "script") || strings.Contains(mime, "javascript"): |
|||
return "fa-file-code" |
|||
default: |
|||
return "fa-file" |
|||
} |
|||
} |
|||
|
|||
func getMimeDisplayName(mime string) string { |
|||
switch mime { |
|||
case "text/plain": |
|||
return "Text" |
|||
case "text/html": |
|||
return "HTML" |
|||
case "application/json": |
|||
return "JSON" |
|||
case "application/pdf": |
|||
return "PDF" |
|||
case "image/jpeg": |
|||
return "JPEG" |
|||
case "image/png": |
|||
return "PNG" |
|||
case "image/gif": |
|||
return "GIF" |
|||
case "video/mp4": |
|||
return "MP4" |
|||
case "audio/mpeg": |
|||
return "MP3" |
|||
case "application/zip": |
|||
return "ZIP" |
|||
default: |
|||
if strings.HasPrefix(mime, "image/") { |
|||
return "Image" |
|||
} else if strings.HasPrefix(mime, "video/") { |
|||
return "Video" |
|||
} else if strings.HasPrefix(mime, "audio/") { |
|||
return "Audio" |
|||
} else if strings.HasPrefix(mime, "text/") { |
|||
return "Text" |
|||
} |
|||
return "File" |
|||
} |
|||
} |
|||
|
|||
var _ = templruntime.GeneratedTemplate |
@ -0,0 +1,214 @@ |
|||
package app |
|||
|
|||
import ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
templ ObjectStoreUsers(data dash.ObjectStoreUsersData) { |
|||
<div class="container-fluid"> |
|||
<!-- Page Header --> |
|||
<div class="d-sm-flex align-items-center justify-content-between mb-4"> |
|||
<div> |
|||
<h1 class="h3 mb-0 text-gray-800"> |
|||
<i class="fas fa-users me-2"></i>Object Store Users |
|||
</h1> |
|||
<p class="mb-0 text-muted">Manage S3 API users and their access credentials</p> |
|||
</div> |
|||
<div class="d-flex gap-2"> |
|||
<button type="button" class="btn btn-primary" |
|||
data-bs-toggle="modal" |
|||
data-bs-target="#createUserModal"> |
|||
<i class="fas fa-plus me-1"></i>Create User |
|||
</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 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 Users |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{fmt.Sprintf("%d", data.TotalUsers)} |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-users 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 Users |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{fmt.Sprintf("%d", countActiveUsers(data.Users))} |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-user-check 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"> |
|||
Last Updated |
|||
</div> |
|||
<div class="h6 mb-0 font-weight-bold text-gray-800"> |
|||
{data.LastUpdated.Format("15:04")} |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-clock fa-2x text-gray-300"></i> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Users Table --> |
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<div class="card shadow mb-4"> |
|||
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between"> |
|||
<h6 class="m-0 font-weight-bold text-primary"> |
|||
<i class="fas fa-users me-2"></i>Object Store Users |
|||
</h6> |
|||
<div class="dropdown no-arrow"> |
|||
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"> |
|||
<i class="fas fa-ellipsis-v fa-sm fa-fw text-gray-400"></i> |
|||
</a> |
|||
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in"> |
|||
<div class="dropdown-header">Actions:</div> |
|||
<a class="dropdown-item" href="#" onclick="exportUsers()"> |
|||
<i class="fas fa-download me-2"></i>Export List |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="card-body"> |
|||
<div class="table-responsive"> |
|||
<table class="table table-hover" width="100%" cellspacing="0" id="usersTable"> |
|||
<thead> |
|||
<tr> |
|||
<th>Username</th> |
|||
<th>Email</th> |
|||
<th>Access Key</th> |
|||
<th>Status</th> |
|||
<th>Created</th> |
|||
<th>Last Login</th> |
|||
<th>Actions</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
for _, user := range data.Users { |
|||
<tr> |
|||
<td> |
|||
<div class="d-flex align-items-center"> |
|||
<i class="fas fa-user me-2 text-muted"></i> |
|||
<strong>{user.Username}</strong> |
|||
</div> |
|||
</td> |
|||
<td>{user.Email}</td> |
|||
<td> |
|||
<code class="text-muted">{user.AccessKey}</code> |
|||
</td> |
|||
<td> |
|||
<span class={fmt.Sprintf("badge bg-%s", getUserStatusColor(user.Status))}> |
|||
{user.Status} |
|||
</span> |
|||
</td> |
|||
<td>{user.CreatedAt.Format("2006-01-02")}</td> |
|||
<td>{user.LastLogin.Format("2006-01-02")}</td> |
|||
<td> |
|||
<div class="btn-group btn-group-sm" role="group"> |
|||
<button type="button" |
|||
class="btn btn-outline-primary btn-sm" |
|||
title="Edit User"> |
|||
<i class="fas fa-edit"></i> |
|||
</button> |
|||
<button type="button" |
|||
class="btn btn-outline-danger btn-sm" |
|||
title="Delete User"> |
|||
<i class="fas fa-trash"></i> |
|||
</button> |
|||
</div> |
|||
</td> |
|||
</tr> |
|||
} |
|||
if len(data.Users) == 0 { |
|||
<tr> |
|||
<td colspan="7" class="text-center text-muted py-4"> |
|||
<i class="fas fa-users fa-3x mb-3 text-muted"></i> |
|||
<div> |
|||
<h5>No users found</h5> |
|||
<p>Create your first object store user to get started.</p> |
|||
</div> |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</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> |
|||
} |
|||
|
|||
// Helper functions for template |
|||
func getUserStatusColor(status string) string { |
|||
switch status { |
|||
case "active": |
|||
return "success" |
|||
case "inactive": |
|||
return "warning" |
|||
case "suspended": |
|||
return "danger" |
|||
default: |
|||
return "secondary" |
|||
} |
|||
} |
|||
|
|||
func countActiveUsers(users []dash.ObjectStoreUser) int { |
|||
count := 0 |
|||
for _, user := range users { |
|||
if user.Status == "active" { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
|||
|
@ -0,0 +1,237 @@ |
|||
// Code generated by templ - DO NOT EDIT.
|
|||
|
|||
// templ: version: v0.3.833
|
|||
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 ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
func ObjectStoreUsers(data dash.ObjectStoreUsersData) 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) |
|||
templ_7745c5c3_Var1 := templ.GetChildren(ctx) |
|||
if templ_7745c5c3_Var1 == nil { |
|||
templ_7745c5c3_Var1 = templ.NopComponent |
|||
} |
|||
ctx = templ.ClearChildren(ctx) |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div class=\"container-fluid\"><!-- Page Header --><div class=\"d-sm-flex align-items-center justify-content-between mb-4\"><div><h1 class=\"h3 mb-0 text-gray-800\"><i class=\"fas fa-users me-2\"></i>Object Store Users</h1><p class=\"mb-0 text-muted\">Manage S3 API users and their access credentials</p></div><div class=\"d-flex gap-2\"><button type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#createUserModal\"><i class=\"fas fa-plus me-1\"></i>Create User</button></div></div><!-- 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 Users</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var2 string |
|||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalUsers)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 38, Col: 71} |
|||
} |
|||
_, 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, 2, "</div></div><div class=\"col-auto\"><i class=\"fas fa-users 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 Users</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var3 string |
|||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", countActiveUsers(data.Users))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 58, Col: 84} |
|||
} |
|||
_, 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, 3, "</div></div><div class=\"col-auto\"><i class=\"fas fa-user-check 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\">Last Updated</div><div class=\"h6 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var4 string |
|||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("15:04")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 78, Col: 69} |
|||
} |
|||
_, 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, 4, "</div></div><div class=\"col-auto\"><i class=\"fas fa-clock fa-2x text-gray-300\"></i></div></div></div></div></div></div><!-- Users Table --><div class=\"row\"><div class=\"col-12\"><div class=\"card shadow mb-4\"><div class=\"card-header py-3 d-flex flex-row align-items-center justify-content-between\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-users me-2\"></i>Object Store Users</h6><div class=\"dropdown no-arrow\"><a class=\"dropdown-toggle\" href=\"#\" role=\"button\" data-bs-toggle=\"dropdown\"><i class=\"fas fa-ellipsis-v fa-sm fa-fw text-gray-400\"></i></a><div class=\"dropdown-menu dropdown-menu-right shadow animated--fade-in\"><div class=\"dropdown-header\">Actions:</div><a class=\"dropdown-item\" href=\"#\" onclick=\"exportUsers()\"><i class=\"fas fa-download me-2\"></i>Export List</a></div></div></div><div class=\"card-body\"><div class=\"table-responsive\"><table class=\"table table-hover\" width=\"100%\" cellspacing=\"0\" id=\"usersTable\"><thead><tr><th>Username</th><th>Email</th><th>Access Key</th><th>Status</th><th>Created</th><th>Last Login</th><th>Actions</th></tr></thead> <tbody>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for _, user := range data.Users { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<tr><td><div class=\"d-flex align-items-center\"><i class=\"fas fa-user me-2 text-muted\"></i> <strong>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var5 string |
|||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 130, Col: 74} |
|||
} |
|||
_, 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, 6, "</strong></div></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var6 string |
|||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 133, Col: 59} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</td><td><code class=\"text-muted\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var7 string |
|||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(user.AccessKey) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 135, Col: 88} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</code></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var8 = []any{fmt.Sprintf("badge bg-%s", getUserStatusColor(user.Status))} |
|||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span class=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var9 string |
|||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var8).String()) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 1, Col: 0} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var10 string |
|||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(user.Status) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 139, Col: 64} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</span></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var11 string |
|||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(user.CreatedAt.Format("2006-01-02")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 142, Col: 84} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var12 string |
|||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(user.LastLogin.Format("2006-01-02")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 143, Col: 84} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</td><td><div class=\"btn-group btn-group-sm\" role=\"group\"><button type=\"button\" class=\"btn btn-outline-primary btn-sm\" title=\"Edit User\"><i class=\"fas fa-edit\"></i></button> <button type=\"button\" class=\"btn btn-outline-danger btn-sm\" title=\"Delete User\"><i class=\"fas fa-trash\"></i></button></div></td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
if len(data.Users) == 0 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<tr><td colspan=\"7\" class=\"text-center text-muted py-4\"><i class=\"fas fa-users fa-3x mb-3 text-muted\"></i><div><h5>No users found</h5><p>Create your first object store user to get started.</p></div></td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</tbody></table></div></div></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: ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var13 string |
|||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/object_store_users.templ`, Line: 184, Col: 81} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</small></div></div></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
// Helper functions for template
|
|||
func getUserStatusColor(status string) string { |
|||
switch status { |
|||
case "active": |
|||
return "success" |
|||
case "inactive": |
|||
return "warning" |
|||
case "suspended": |
|||
return "danger" |
|||
default: |
|||
return "secondary" |
|||
} |
|||
} |
|||
|
|||
func countActiveUsers(users []dash.ObjectStoreUser) int { |
|||
count := 0 |
|||
for _, user := range users { |
|||
if user.Status == "active" { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
|||
|
|||
var _ = templruntime.GeneratedTemplate |
@ -0,0 +1,302 @@ |
|||
package app |
|||
|
|||
import ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
templ S3Buckets(data dash.S3BucketsData) { |
|||
<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-cube me-2"></i>S3 Buckets |
|||
</h1> |
|||
<div class="btn-toolbar mb-2 mb-md-0"> |
|||
<div class="btn-group me-2"> |
|||
<button type="button" class="btn btn-sm btn-primary" |
|||
data-bs-toggle="modal" |
|||
data-bs-target="#createBucketModal"> |
|||
<i class="fas fa-plus me-1"></i>Create Bucket |
|||
</button> |
|||
|
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<div id="s3-buckets-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 Buckets |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{fmt.Sprintf("%d", data.TotalBuckets)} |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-cube 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"> |
|||
Total Storage |
|||
</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 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"> |
|||
Active Buckets |
|||
</div> |
|||
<div class="h5 mb-0 font-weight-bold text-gray-800"> |
|||
{fmt.Sprintf("%d", countActiveBuckets(data.Buckets))} |
|||
</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-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"> |
|||
Last Updated |
|||
</div> |
|||
<div class="h6 mb-0 font-weight-bold text-gray-800"> |
|||
{data.LastUpdated.Format("15:04:05")} |
|||
</div> |
|||
</div> |
|||
<div class="col-auto"> |
|||
<i class="fas fa-clock fa-2x text-gray-300"></i> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Buckets Table --> |
|||
<div class="row"> |
|||
<div class="col-12"> |
|||
<div class="card shadow mb-4"> |
|||
<div class="card-header py-3 d-flex flex-row align-items-center justify-content-between"> |
|||
<h6 class="m-0 font-weight-bold text-primary"> |
|||
<i class="fas fa-cube me-2"></i>S3 Buckets |
|||
</h6> |
|||
<div class="dropdown no-arrow"> |
|||
<a class="dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"> |
|||
<i class="fas fa-ellipsis-v fa-sm fa-fw text-gray-400"></i> |
|||
</a> |
|||
<div class="dropdown-menu dropdown-menu-right shadow animated--fade-in"> |
|||
<div class="dropdown-header">Actions:</div> |
|||
<a class="dropdown-item" href="#" onclick="exportBucketList()"> |
|||
<i class="fas fa-download me-2"></i>Export List |
|||
</a> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="card-body"> |
|||
<div class="table-responsive"> |
|||
<table class="table table-hover" width="100%" cellspacing="0" id="bucketsTable"> |
|||
<thead> |
|||
<tr> |
|||
<th>Name</th> |
|||
<th>Created</th> |
|||
<th>Objects</th> |
|||
<th>Size</th> |
|||
<th>Status</th> |
|||
<th>Actions</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
for _, bucket := range data.Buckets { |
|||
<tr> |
|||
<td> |
|||
<a href={templ.SafeURL(fmt.Sprintf("/s3/buckets/%s", bucket.Name))} |
|||
class="text-decoration-none"> |
|||
<i class="fas fa-cube me-2"></i> |
|||
{bucket.Name} |
|||
</a> |
|||
</td> |
|||
<td>{bucket.CreatedAt.Format("2006-01-02 15:04")}</td> |
|||
<td>{fmt.Sprintf("%d", bucket.ObjectCount)}</td> |
|||
<td>{formatBytes(bucket.Size)}</td> |
|||
<td> |
|||
<span class={fmt.Sprintf("badge bg-%s", getBucketStatusColor(bucket.Status))}> |
|||
{bucket.Status} |
|||
</span> |
|||
</td> |
|||
<td> |
|||
<div class="btn-group btn-group-sm" role="group"> |
|||
<a href={templ.SafeURL(fmt.Sprintf("/files?path=/buckets/%s", bucket.Name))} |
|||
class="btn btn-outline-success btn-sm" |
|||
title="Browse Files"> |
|||
<i class="fas fa-folder-open"></i> |
|||
</a> |
|||
<a href={templ.SafeURL(fmt.Sprintf("/s3/buckets/%s", bucket.Name))} |
|||
class="btn btn-outline-primary btn-sm" |
|||
title="View Details"> |
|||
<i class="fas fa-eye"></i> |
|||
</a> |
|||
<button type="button" |
|||
class="btn btn-outline-danger btn-sm delete-bucket-btn" |
|||
data-bucket-name={bucket.Name} |
|||
title="Delete Bucket"> |
|||
<i class="fas fa-trash"></i> |
|||
</button> |
|||
</div> |
|||
</td> |
|||
</tr> |
|||
} |
|||
if len(data.Buckets) == 0 { |
|||
<tr> |
|||
<td colspan="6" class="text-center text-muted py-4"> |
|||
<i class="fas fa-cube fa-3x mb-3 text-muted"></i> |
|||
<div> |
|||
<h5>No S3 buckets found</h5> |
|||
<p>Create your first bucket to get started with S3 storage.</p> |
|||
<button type="button" class="btn btn-primary" |
|||
data-bs-toggle="modal" |
|||
data-bs-target="#createBucketModal"> |
|||
<i class="fas fa-plus me-1"></i>Create Bucket |
|||
</button> |
|||
</div> |
|||
</td> |
|||
</tr> |
|||
} |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</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 Bucket Modal --> |
|||
<div class="modal fade" id="createBucketModal" tabindex="-1" aria-labelledby="createBucketModalLabel" aria-hidden="true"> |
|||
<div class="modal-dialog"> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h5 class="modal-title" id="createBucketModalLabel"> |
|||
<i class="fas fa-plus me-2"></i>Create New S3 Bucket |
|||
</h5> |
|||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
|||
</div> |
|||
<form id="createBucketForm"> |
|||
<div class="modal-body"> |
|||
<div class="mb-3"> |
|||
<label for="bucketName" class="form-label">Bucket Name</label> |
|||
<input type="text" class="form-control" id="bucketName" name="name" |
|||
placeholder="my-bucket-name" required |
|||
pattern="[a-z0-9.-]+" |
|||
title="Bucket name must contain only lowercase letters, numbers, dots, and hyphens"> |
|||
<div class="form-text"> |
|||
Bucket names must be between 3 and 63 characters, contain only lowercase letters, numbers, dots, and hyphens. |
|||
</div> |
|||
</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"> |
|||
<i class="fas fa-plus me-1"></i>Create Bucket |
|||
</button> |
|||
</div> |
|||
</form> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- Delete Confirmation Modal --> |
|||
<div class="modal fade" id="deleteBucketModal" tabindex="-1" aria-labelledby="deleteBucketModalLabel" aria-hidden="true"> |
|||
<div class="modal-dialog"> |
|||
<div class="modal-content"> |
|||
<div class="modal-header"> |
|||
<h5 class="modal-title" id="deleteBucketModalLabel"> |
|||
<i class="fas fa-exclamation-triangle me-2 text-warning"></i>Delete Bucket |
|||
</h5> |
|||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
|||
</div> |
|||
<div class="modal-body"> |
|||
<p>Are you sure you want to delete the bucket <strong id="deleteBucketName"></strong>?</p> |
|||
<div class="alert alert-warning"> |
|||
<i class="fas fa-exclamation-triangle me-2"></i> |
|||
<strong>Warning:</strong> This action cannot be undone. All objects in the bucket will be permanently deleted. |
|||
</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" onclick="deleteBucket()"> |
|||
<i class="fas fa-trash me-1"></i>Delete Bucket |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
} |
|||
|
|||
// Helper functions for template |
|||
func getBucketStatusColor(status string) string { |
|||
switch status { |
|||
case "active": |
|||
return "success" |
|||
case "error": |
|||
return "danger" |
|||
case "warning": |
|||
return "warning" |
|||
default: |
|||
return "secondary" |
|||
} |
|||
} |
|||
|
|||
func countActiveBuckets(buckets []dash.S3Bucket) int { |
|||
count := 0 |
|||
for _, bucket := range buckets { |
|||
if bucket.Status == "active" { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
@ -0,0 +1,277 @@ |
|||
// Code generated by templ - DO NOT EDIT.
|
|||
|
|||
// templ: version: v0.3.833
|
|||
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 ( |
|||
"fmt" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
) |
|||
|
|||
func S3Buckets(data dash.S3BucketsData) 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) |
|||
templ_7745c5c3_Var1 := templ.GetChildren(ctx) |
|||
if templ_7745c5c3_Var1 == nil { |
|||
templ_7745c5c3_Var1 = templ.NopComponent |
|||
} |
|||
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=\"fas fa-cube me-2\"></i>S3 Buckets</h1><div class=\"btn-toolbar mb-2 mb-md-0\"><div class=\"btn-group me-2\"><button type=\"button\" class=\"btn btn-sm btn-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#createBucketModal\"><i class=\"fas fa-plus me-1\"></i>Create Bucket</button></div></div></div><div id=\"s3-buckets-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 Buckets</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var2 string |
|||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", data.TotalBuckets)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 37, Col: 73} |
|||
} |
|||
_, 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, 2, "</div></div><div class=\"col-auto\"><i class=\"fas fa-cube 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\">Total Storage</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var3 string |
|||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(data.TotalSize)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 57, Col: 64} |
|||
} |
|||
_, 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, 3, "</div></div><div class=\"col-auto\"><i class=\"fas fa-hdd 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\">Active Buckets</div><div class=\"h5 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var4 string |
|||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", countActiveBuckets(data.Buckets))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 77, Col: 88} |
|||
} |
|||
_, 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, 4, "</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-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\">Last Updated</div><div class=\"h6 mb-0 font-weight-bold text-gray-800\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var5 string |
|||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("15:04:05")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 97, Col: 72} |
|||
} |
|||
_, 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, 5, "</div></div><div class=\"col-auto\"><i class=\"fas fa-clock fa-2x text-gray-300\"></i></div></div></div></div></div></div><!-- Buckets Table --><div class=\"row\"><div class=\"col-12\"><div class=\"card shadow mb-4\"><div class=\"card-header py-3 d-flex flex-row align-items-center justify-content-between\"><h6 class=\"m-0 font-weight-bold text-primary\"><i class=\"fas fa-cube me-2\"></i>S3 Buckets</h6><div class=\"dropdown no-arrow\"><a class=\"dropdown-toggle\" href=\"#\" role=\"button\" data-bs-toggle=\"dropdown\"><i class=\"fas fa-ellipsis-v fa-sm fa-fw text-gray-400\"></i></a><div class=\"dropdown-menu dropdown-menu-right shadow animated--fade-in\"><div class=\"dropdown-header\">Actions:</div><a class=\"dropdown-item\" href=\"#\" onclick=\"exportBucketList()\"><i class=\"fas fa-download me-2\"></i>Export List</a></div></div></div><div class=\"card-body\"><div class=\"table-responsive\"><table class=\"table table-hover\" width=\"100%\" cellspacing=\"0\" id=\"bucketsTable\"><thead><tr><th>Name</th><th>Created</th><th>Objects</th><th>Size</th><th>Status</th><th>Actions</th></tr></thead> <tbody>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
for _, bucket := range data.Buckets { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<tr><td><a href=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var6 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/s3/buckets/%s", bucket.Name)) |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var6))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\" class=\"text-decoration-none\"><i class=\"fas fa-cube me-2\"></i> ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var7 string |
|||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Name) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 149, Col: 64} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</a></td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var8 string |
|||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.CreatedAt.Format("2006-01-02 15:04")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 152, Col: 92} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var9 string |
|||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", bucket.ObjectCount)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 153, Col: 86} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var10 string |
|||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(formatBytes(bucket.Size)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 154, Col: 73} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</td><td>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var11 = []any{fmt.Sprintf("badge bg-%s", getBucketStatusColor(bucket.Status))} |
|||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<span class=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var12 string |
|||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(templ.CSSClasses(templ_7745c5c3_Var11).String()) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 1, Col: 0} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var13 string |
|||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Status) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 157, Col: 66} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</span></td><td><div class=\"btn-group btn-group-sm\" role=\"group\"><a href=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var14 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/files?path=/buckets/%s", bucket.Name)) |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var14))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" class=\"btn btn-outline-success btn-sm\" title=\"Browse Files\"><i class=\"fas fa-folder-open\"></i></a> <a href=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var15 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/s3/buckets/%s", bucket.Name)) |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var15))) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\" class=\"btn btn-outline-primary btn-sm\" title=\"View Details\"><i class=\"fas fa-eye\"></i></a> <button type=\"button\" class=\"btn btn-outline-danger btn-sm delete-bucket-btn\" data-bucket-name=\"") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var16 string |
|||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(bucket.Name) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 174, Col: 89} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "\" title=\"Delete Bucket\"><i class=\"fas fa-trash\"></i></button></div></td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
if len(data.Buckets) == 0 { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<tr><td colspan=\"6\" class=\"text-center text-muted py-4\"><i class=\"fas fa-cube fa-3x mb-3 text-muted\"></i><div><h5>No S3 buckets found</h5><p>Create your first bucket to get started with S3 storage.</p><button type=\"button\" class=\"btn btn-primary\" data-bs-toggle=\"modal\" data-bs-target=\"#createBucketModal\"><i class=\"fas fa-plus me-1\"></i>Create Bucket</button></div></td></tr>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "</tbody></table></div></div></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: ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var17 string |
|||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(data.LastUpdated.Format("2006-01-02 15:04:05")) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/s3_buckets.templ`, Line: 211, Col: 81} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</small></div></div></div><!-- Create Bucket Modal --><div class=\"modal fade\" id=\"createBucketModal\" tabindex=\"-1\" aria-labelledby=\"createBucketModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"createBucketModalLabel\"><i class=\"fas fa-plus me-2\"></i>Create New S3 Bucket</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><form id=\"createBucketForm\"><div class=\"modal-body\"><div class=\"mb-3\"><label for=\"bucketName\" class=\"form-label\">Bucket Name</label> <input type=\"text\" class=\"form-control\" id=\"bucketName\" name=\"name\" placeholder=\"my-bucket-name\" required pattern=\"[a-z0-9.-]+\" title=\"Bucket name must contain only lowercase letters, numbers, dots, and hyphens\"><div class=\"form-text\">Bucket names must be between 3 and 63 characters, contain only lowercase letters, numbers, dots, and hyphens.</div></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\"><i class=\"fas fa-plus me-1\"></i>Create Bucket</button></div></form></div></div></div><!-- Delete Confirmation Modal --><div class=\"modal fade\" id=\"deleteBucketModal\" tabindex=\"-1\" aria-labelledby=\"deleteBucketModalLabel\" aria-hidden=\"true\"><div class=\"modal-dialog\"><div class=\"modal-content\"><div class=\"modal-header\"><h5 class=\"modal-title\" id=\"deleteBucketModalLabel\"><i class=\"fas fa-exclamation-triangle me-2 text-warning\"></i>Delete Bucket</h5><button type=\"button\" class=\"btn-close\" data-bs-dismiss=\"modal\" aria-label=\"Close\"></button></div><div class=\"modal-body\"><p>Are you sure you want to delete the bucket <strong id=\"deleteBucketName\"></strong>?</p><div class=\"alert alert-warning\"><i class=\"fas fa-exclamation-triangle me-2\"></i> <strong>Warning:</strong> This action cannot be undone. All objects in the bucket will be permanently deleted.</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\" onclick=\"deleteBucket()\"><i class=\"fas fa-trash me-1\"></i>Delete Bucket</button></div></div></div></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
// Helper functions for template
|
|||
func getBucketStatusColor(status string) string { |
|||
switch status { |
|||
case "active": |
|||
return "success" |
|||
case "error": |
|||
return "danger" |
|||
case "warning": |
|||
return "warning" |
|||
default: |
|||
return "secondary" |
|||
} |
|||
} |
|||
|
|||
func countActiveBuckets(buckets []dash.S3Bucket) int { |
|||
count := 0 |
|||
for _, bucket := range buckets { |
|||
if bucket.Status == "active" { |
|||
count++ |
|||
} |
|||
} |
|||
return count |
|||
} |
|||
|
|||
var _ = templruntime.GeneratedTemplate |
@ -0,0 +1,84 @@ |
|||
package app |
|||
|
|||
import ( |
|||
"fmt" |
|||
"strconv" |
|||
) |
|||
|
|||
// getStatusColor returns Bootstrap color class for status
|
|||
func getStatusColor(status string) string { |
|||
switch status { |
|||
case "active", "healthy": |
|||
return "success" |
|||
case "warning": |
|||
return "warning" |
|||
case "critical", "unreachable": |
|||
return "danger" |
|||
default: |
|||
return "secondary" |
|||
} |
|||
} |
|||
|
|||
// getHealthColor returns Bootstrap color class for health status
|
|||
func getHealthColor(health string) string { |
|||
switch health { |
|||
case "excellent": |
|||
return "success" |
|||
case "good": |
|||
return "primary" |
|||
case "fair": |
|||
return "warning" |
|||
case "poor": |
|||
return "danger" |
|||
default: |
|||
return "secondary" |
|||
} |
|||
} |
|||
|
|||
// formatBytes converts bytes to human readable format
|
|||
func formatBytes(bytes int64) string { |
|||
if bytes == 0 { |
|||
return "0 B" |
|||
} |
|||
|
|||
units := []string{"B", "KB", "MB", "GB", "TB", "PB"} |
|||
var i int |
|||
value := float64(bytes) |
|||
|
|||
for value >= 1024 && i < len(units)-1 { |
|||
value /= 1024 |
|||
i++ |
|||
} |
|||
|
|||
if i == 0 { |
|||
return fmt.Sprintf("%.0f %s", value, units[i]) |
|||
} |
|||
return fmt.Sprintf("%.1f %s", value, units[i]) |
|||
} |
|||
|
|||
// formatNumber formats large numbers with commas
|
|||
func formatNumber(num int64) string { |
|||
if num == 0 { |
|||
return "0" |
|||
} |
|||
|
|||
str := strconv.FormatInt(num, 10) |
|||
result := "" |
|||
|
|||
for i, char := range str { |
|||
if i > 0 && (len(str)-i)%3 == 0 { |
|||
result += "," |
|||
} |
|||
result += string(char) |
|||
} |
|||
|
|||
return result |
|||
} |
|||
|
|||
// calculatePercent calculates percentage for progress bars
|
|||
func calculatePercent(current, max int) int { |
|||
if max == 0 { |
|||
return 0 |
|||
} |
|||
return (current * 100) / max |
|||
} |
@ -0,0 +1,263 @@ |
|||
package layout |
|||
|
|||
import ( |
|||
"fmt" |
|||
"time" |
|||
"github.com/gin-gonic/gin" |
|||
) |
|||
|
|||
templ Layout(c *gin.Context, content templ.Component) { |
|||
{{ |
|||
username := c.GetString("username") |
|||
if username == "" { |
|||
username = "admin" |
|||
} |
|||
}} |
|||
<!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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> |
|||
<!-- Font Awesome CSS --> |
|||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> |
|||
<!-- HTMX --> |
|||
<script src="https://unpkg.com/htmx.org@1.9.8/dist/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 |
|||
</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/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> |
|||
</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"> |
|||
<a class="nav-link" href="/config"> |
|||
<i class="fas fa-cog me-2"></i>Configuration |
|||
</a> |
|||
</li> |
|||
<li class="nav-item"> |
|||
<a class="nav-link" href="/maintenance"> |
|||
<i class="fas fa-tools me-2"></i>Maintenance |
|||
</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"> |
|||
© {fmt.Sprintf("%d", time.Now().Year())} SeaweedFS Admin |
|||
</small> |
|||
</div> |
|||
</footer> |
|||
|
|||
<!-- Bootstrap JS --> |
|||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> |
|||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> |
|||
</body> |
|||
</html> |
|||
} |
@ -0,0 +1,163 @@ |
|||
// Code generated by templ - DO NOT EDIT.
|
|||
|
|||
// templ: version: v0.3.833
|
|||
package layout |
|||
|
|||
//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 ( |
|||
"fmt" |
|||
"github.com/gin-gonic/gin" |
|||
"time" |
|||
) |
|||
|
|||
func Layout(c *gin.Context, content templ.Component) 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) |
|||
templ_7745c5c3_Var1 := templ.GetChildren(ctx) |
|||
if templ_7745c5c3_Var1 == nil { |
|||
templ_7745c5c3_Var1 = templ.NopComponent |
|||
} |
|||
ctx = templ.ClearChildren(ctx) |
|||
|
|||
username := c.GetString("username") |
|||
if username == "" { |
|||
username = "admin" |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!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=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css\" rel=\"stylesheet\"><!-- Font Awesome CSS --><link href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css\" rel=\"stylesheet\"><!-- HTMX --><script src=\"https://unpkg.com/htmx.org@1.9.8/dist/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</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>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var2 string |
|||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(username) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 51, Col: 73} |
|||
} |
|||
_, 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, 2, "</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/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></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\"><a class=\"nav-link\" href=\"/config\"><i class=\"fas fa-cog me-2\"></i>Configuration</a></li><li class=\"nav-item\"><a class=\"nav-link\" href=\"/maintenance\"><i class=\"fas fa-tools me-2\"></i>Maintenance</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\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = content.Render(ctx, templ_7745c5c3_Buffer) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</div></main></div></div><!-- Footer --><footer class=\"footer mt-auto py-3 bg-light\"><div class=\"container-fluid text-center\"><small class=\"text-muted\">© ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var3 string |
|||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", time.Now().Year())) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 186, Col: 60} |
|||
} |
|||
_, 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, 4, " SeaweedFS Admin</small></div></footer><!-- Bootstrap JS --><script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js\"></script><!-- Custom JS --><script src=\"/static/js/admin.js\"></script></body></html>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
func LoginForm(c *gin.Context, title string, errorMessage string) 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) |
|||
templ_7745c5c3_Var4 := templ.GetChildren(ctx) |
|||
if templ_7745c5c3_Var4 == nil { |
|||
templ_7745c5c3_Var4 = templ.NopComponent |
|||
} |
|||
ctx = templ.ClearChildren(ctx) |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var5 string |
|||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 204, Col: 17} |
|||
} |
|||
_, 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, 6, " - 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=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css\" rel=\"stylesheet\"><link href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.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\">") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var6 string |
|||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(title) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 218, Col: 57} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</h4><p class=\"text-muted\">Please sign in to continue</p></div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
if errorMessage != "" { |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"alert alert-danger\" role=\"alert\"><i class=\"fas fa-exclamation-triangle me-2\"></i> ") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
var templ_7745c5c3_Var7 string |
|||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 225, Col: 45} |
|||
} |
|||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
} |
|||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<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=\"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js\"></script></body></html>") |
|||
if templ_7745c5c3_Err != nil { |
|||
return templ_7745c5c3_Err |
|||
} |
|||
return nil |
|||
}) |
|||
} |
|||
|
|||
var _ = templruntime.GeneratedTemplate |
@ -0,0 +1,236 @@ |
|||
package command |
|||
|
|||
import ( |
|||
"context" |
|||
"crypto/rand" |
|||
"crypto/tls" |
|||
"fmt" |
|||
"log" |
|||
"net/http" |
|||
"os" |
|||
"os/signal" |
|||
"path/filepath" |
|||
"strings" |
|||
"syscall" |
|||
"time" |
|||
|
|||
"github.com/gin-contrib/sessions" |
|||
"github.com/gin-contrib/sessions/cookie" |
|||
"github.com/gin-gonic/gin" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/admin/dash" |
|||
"github.com/seaweedfs/seaweedfs/weed/admin/handlers" |
|||
) |
|||
|
|||
var ( |
|||
a AdminOptions |
|||
) |
|||
|
|||
type AdminOptions struct { |
|||
port *int |
|||
masters *string |
|||
tlsCertPath *string |
|||
tlsKeyPath *string |
|||
adminUser *string |
|||
adminPassword *string |
|||
} |
|||
|
|||
func init() { |
|||
cmdAdmin.Run = runAdmin // break init cycle
|
|||
a.port = cmdAdmin.Flag.Int("port", 23646, "admin server port") |
|||
a.masters = cmdAdmin.Flag.String("masters", "localhost:9333", "comma-separated master servers") |
|||
a.tlsCertPath = cmdAdmin.Flag.String("tlsCert", "", "path to TLS certificate file") |
|||
a.tlsKeyPath = cmdAdmin.Flag.String("tlsKey", "", "path to TLS private key file") |
|||
|
|||
a.adminUser = cmdAdmin.Flag.String("adminUser", "admin", "admin interface username") |
|||
a.adminPassword = cmdAdmin.Flag.String("adminPassword", "", "admin interface password (if empty, auth is disabled)") |
|||
} |
|||
|
|||
var cmdAdmin = &Command{ |
|||
UsageLine: "admin -port=23646 -masters=localhost:9333", |
|||
Short: "start SeaweedFS web admin interface", |
|||
Long: `Start a web admin interface for SeaweedFS cluster management. |
|||
|
|||
The admin interface provides a modern web interface for: |
|||
- Cluster topology visualization and monitoring |
|||
- Volume management and operations |
|||
- File browser and management |
|||
- System metrics and performance monitoring |
|||
- Configuration management |
|||
- Maintenance operations |
|||
|
|||
The admin interface automatically discovers filers from the master servers. |
|||
|
|||
Example Usage: |
|||
weed admin -port=23646 -masters="master1:9333,master2:9333" |
|||
weed admin -port=443 -tlsCert=/etc/ssl/admin.crt -tlsKey=/etc/ssl/admin.key |
|||
|
|||
Authentication: |
|||
- If adminPassword is not set, the admin interface runs without authentication |
|||
- If adminPassword is set, users must login with adminUser/adminPassword |
|||
- Sessions are secured with auto-generated session keys |
|||
|
|||
Security: |
|||
- Use HTTPS in production by providing TLS certificates |
|||
- Set strong adminPassword for production deployments |
|||
- Configure firewall rules to restrict admin interface access |
|||
|
|||
`, |
|||
} |
|||
|
|||
func runAdmin(cmd *Command, args []string) bool { |
|||
// Validate required parameters
|
|||
if *a.masters == "" { |
|||
fmt.Println("Error: masters parameter is required") |
|||
fmt.Println("Usage: weed admin -masters=master1:9333,master2:9333") |
|||
return false |
|||
} |
|||
|
|||
// Validate TLS configuration
|
|||
if (*a.tlsCertPath != "" && *a.tlsKeyPath == "") || |
|||
(*a.tlsCertPath == "" && *a.tlsKeyPath != "") { |
|||
fmt.Println("Error: Both tlsCert and tlsKey must be provided for TLS") |
|||
return false |
|||
} |
|||
|
|||
// Security warnings
|
|||
if *a.adminPassword == "" { |
|||
fmt.Println("WARNING: Admin interface is running without authentication!") |
|||
fmt.Println(" Set -adminPassword for production use") |
|||
} |
|||
|
|||
if *a.tlsCertPath == "" { |
|||
fmt.Println("WARNING: Admin interface is running without TLS encryption!") |
|||
fmt.Println(" Use -tlsCert and -tlsKey for production use") |
|||
} |
|||
|
|||
fmt.Printf("Starting SeaweedFS Admin Interface on port %d\n", *a.port) |
|||
fmt.Printf("Masters: %s\n", *a.masters) |
|||
fmt.Printf("Filers will be discovered automatically from masters\n") |
|||
if *a.adminPassword != "" { |
|||
fmt.Printf("Authentication: Enabled (user: %s)\n", *a.adminUser) |
|||
} else { |
|||
fmt.Printf("Authentication: Disabled\n") |
|||
} |
|||
if *a.tlsCertPath != "" { |
|||
fmt.Printf("TLS: Enabled\n") |
|||
} else { |
|||
fmt.Printf("TLS: Disabled\n") |
|||
} |
|||
|
|||
// Set up graceful shutdown
|
|||
ctx, cancel := context.WithCancel(context.Background()) |
|||
defer cancel() |
|||
|
|||
// Handle interrupt signals
|
|||
sigChan := make(chan os.Signal, 1) |
|||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) |
|||
|
|||
go func() { |
|||
sig := <-sigChan |
|||
fmt.Printf("\nReceived signal %v, shutting down gracefully...\n", sig) |
|||
cancel() |
|||
}() |
|||
|
|||
// Start the admin server
|
|||
err := startAdminServer(ctx, a) |
|||
if err != nil { |
|||
fmt.Printf("Admin server error: %v\n", err) |
|||
return false |
|||
} |
|||
|
|||
fmt.Println("Admin server stopped") |
|||
return true |
|||
} |
|||
|
|||
// startAdminServer starts the actual admin server
|
|||
func startAdminServer(ctx context.Context, options AdminOptions) error { |
|||
// Set Gin mode
|
|||
gin.SetMode(gin.ReleaseMode) |
|||
|
|||
// Create router
|
|||
r := gin.New() |
|||
r.Use(gin.Logger(), gin.Recovery()) |
|||
|
|||
// Session store - always auto-generate session key
|
|||
sessionKeyBytes := make([]byte, 32) |
|||
_, err := rand.Read(sessionKeyBytes) |
|||
if err != nil { |
|||
return fmt.Errorf("failed to generate session key: %v", err) |
|||
} |
|||
store := cookie.NewStore(sessionKeyBytes) |
|||
r.Use(sessions.Sessions("admin-session", store)) |
|||
|
|||
// Static files - serve from filesystem
|
|||
staticPath := filepath.Join("weed", "admin", "static") |
|||
if _, err := os.Stat(staticPath); err == nil { |
|||
r.Static("/static", staticPath) |
|||
} else { |
|||
log.Printf("Warning: Static files not found at %s", staticPath) |
|||
} |
|||
|
|||
// Create admin server
|
|||
adminServer := dash.NewAdminServer(*options.masters, nil) |
|||
|
|||
// Show discovered filers
|
|||
filers := adminServer.GetAllFilers() |
|||
if len(filers) > 0 { |
|||
fmt.Printf("Discovered filers: %s\n", strings.Join(filers, ", ")) |
|||
} else { |
|||
fmt.Printf("No filers discovered from masters\n") |
|||
} |
|||
|
|||
// Create handlers and setup routes
|
|||
adminHandlers := handlers.NewAdminHandlers(adminServer) |
|||
adminHandlers.SetupRoutes(r, *options.adminPassword != "", *options.adminUser, *options.adminPassword) |
|||
|
|||
// Server configuration
|
|||
addr := fmt.Sprintf(":%d", *options.port) |
|||
server := &http.Server{ |
|||
Addr: addr, |
|||
Handler: r, |
|||
} |
|||
|
|||
// TLS configuration
|
|||
if *options.tlsCertPath != "" && *options.tlsKeyPath != "" { |
|||
server.TLSConfig = &tls.Config{ |
|||
MinVersion: tls.VersionTLS12, |
|||
} |
|||
} |
|||
|
|||
// Start server
|
|||
go func() { |
|||
log.Printf("Starting SeaweedFS Admin Server on port %d", *options.port) |
|||
|
|||
var err error |
|||
if *options.tlsCertPath != "" && *options.tlsKeyPath != "" { |
|||
log.Printf("Using TLS with cert: %s, key: %s", *options.tlsCertPath, *options.tlsKeyPath) |
|||
err = server.ListenAndServeTLS(*options.tlsCertPath, *options.tlsKeyPath) |
|||
} else { |
|||
err = server.ListenAndServe() |
|||
} |
|||
|
|||
if err != nil && err != http.ErrServerClosed { |
|||
log.Printf("Failed to start server: %v", err) |
|||
} |
|||
}() |
|||
|
|||
// Wait for context cancellation
|
|||
<-ctx.Done() |
|||
|
|||
// Graceful shutdown
|
|||
log.Println("Shutting down admin server...") |
|||
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) |
|||
defer cancel() |
|||
|
|||
if err := server.Shutdown(shutdownCtx); err != nil { |
|||
return fmt.Errorf("admin server forced to shutdown: %v", err) |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// GetAdminOptions returns the admin command options for testing
|
|||
func GetAdminOptions() *AdminOptions { |
|||
return &AdminOptions{} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue