You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							388 lines
						
					
					
						
							20 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							388 lines
						
					
					
						
							20 KiB
						
					
					
				| package app | |
| 
 | |
| import "fmt" | |
| import "strings" | |
| import "github.com/seaweedfs/seaweedfs/weed/admin/dash" | |
| 
 | |
| templ Topics(data dash.TopicsData) { | |
|     <div class="container-fluid"> | |
|         <div class="row"> | |
|             <div class="col-12"> | |
|                 <div class="d-flex justify-content-between align-items-center mb-4"> | |
|                     <h1 class="h3 mb-0">Message Queue Topics</h1> | |
|                     <small class="text-muted">Last updated: {data.LastUpdated.Format("2006-01-02 15:04:05")}</small> | |
|                 </div> | |
| 
 | |
|                 <!-- Summary Cards --> | |
|                 <div class="row mb-4"> | |
|                     <div class="col-md-6"> | |
|                         <div class="card text-center"> | |
|                             <div class="card-body"> | |
|                                 <h5 class="card-title">Total Topics</h5> | |
|                                 <h3 class="text-primary">{fmt.Sprintf("%d", data.TotalTopics)}</h3> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                     <div class="col-md-6"> | |
|                         <div class="card text-center"> | |
|                             <div class="card-body"> | |
|                                 <h5 class="card-title">Available Topics</h5> | |
|                                 <h3 class="text-info">{fmt.Sprintf("%d", len(data.Topics))}</h3> | |
|                             </div> | |
|                         </div> | |
|                     </div> | |
|                 </div> | |
| 
 | |
|                 <!-- Topics Table --> | |
|                 <div class="card"> | |
|                     <div class="card-header d-flex justify-content-between align-items-center"> | |
|                         <h5 class="mb-0">Topics</h5> | |
|                         <div> | |
|                             <button class="btn btn-sm btn-primary me-2" onclick="showCreateTopicModal()"> | |
|                                 <i class="fas fa-plus me-1"></i>Create Topic | |
|                             </button> | |
|                             <button class="btn btn-sm btn-outline-secondary" onclick="exportTopicsCSV()"> | |
|                                 <i class="fas fa-download me-1"></i>Export CSV | |
|                             </button> | |
|                         </div> | |
|                     </div> | |
|                     <div class="card-body"> | |
|                         if len(data.Topics) == 0 { | |
|                             <div class="text-center py-4"> | |
|                                 <i class="fas fa-list-alt fa-3x text-muted mb-3"></i> | |
|                                 <h5>No Topics Found</h5> | |
|                                 <p class="text-muted">No message queue topics are currently configured.</p> | |
|                             </div> | |
|                         } else { | |
|                             <div class="table-responsive"> | |
|                                 <table class="table table-striped" id="topicsTable"> | |
|                                     <thead> | |
|                                         <tr> | |
|                                             <th>Namespace</th> | |
|                                             <th>Topic Name</th> | |
|                                             <th>Partitions</th> | |
|                                             <th>Retention</th> | |
|                                             <th>Actions</th> | |
|                                         </tr> | |
|                                     </thead> | |
|                                     <tbody> | |
|                                         for _, topic := range data.Topics { | |
|                                             <tr class="topic-row" data-topic-name={topic.Name} style="cursor: pointer;"> | |
|                                                 <td> | |
|                                                     <span class="badge bg-secondary">{func() string { | |
|                                                         idx := strings.LastIndex(topic.Name, ".") | |
|                                                         if idx == -1 { | |
|                                                             return "default" | |
|                                                         } | |
|                                                         return topic.Name[:idx] | |
|                                                     }()}</span> | |
|                                                 </td> | |
|                                                 <td> | |
|                                                     <strong>{func() string { | |
|                                                         idx := strings.LastIndex(topic.Name, ".") | |
|                                                         if idx == -1 { | |
|                                                             return topic.Name | |
|                                                         } | |
|                                                         return topic.Name[idx+1:] | |
|                                                     }()}</strong> | |
|                                                 </td> | |
|                                                 <td> | |
|                                                     <span class="badge bg-info">{fmt.Sprintf("%d", topic.Partitions)}</span> | |
|                                                 </td> | |
|                                                 <td> | |
|                                                     if topic.Retention.Enabled { | |
|                                                         <span class="badge bg-success"> | |
|                                                             <i class="fas fa-clock me-1"></i> | |
|                                                             {fmt.Sprintf("%d %s", topic.Retention.DisplayValue, topic.Retention.DisplayUnit)} | |
|                                                         </span> | |
|                                                     } else { | |
|                                                         <span class="badge bg-secondary"> | |
|                                                             <i class="fas fa-times me-1"></i>Disabled | |
|                                                         </span> | |
|                                                     } | |
|                                                 </td> | |
|                                                 <td> | |
|                                                     <button class="btn btn-sm btn-outline-primary" data-action="view-topic-details" data-topic-name={ topic.Name }> | |
|                                                         <i class="fas fa-eye"></i> | |
|                                                     </button> | |
|                                                 </td> | |
|                                             </tr> | |
|                                             <tr class="topic-details-row" id={ fmt.Sprintf("details-%s", strings.ReplaceAll(topic.Name, ".", "_")) } style="display: none;"> | |
|                                                 <td colspan="5"> | |
|                                                     <div class="topic-details-content"> | |
|                                                         <div class="text-center py-3"> | |
|                                                             <i class="fas fa-spinner fa-spin"></i> Loading topic details... | |
|                                                         </div> | |
|                                                     </div> | |
|                                                 </td> | |
|                                             </tr> | |
|                                         } | |
|                                     </tbody> | |
|                                 </table> | |
|                             </div> | |
|                         } | |
|                     </div> | |
|                 </div> | |
|             </div> | |
|         </div> | |
|     </div> | |
| 
 | |
|     <!-- Create Topic Modal --> | |
|     <div class="modal fade" id="createTopicModal" tabindex="-1" role="dialog"> | |
|         <div class="modal-dialog modal-lg" role="document"> | |
|             <div class="modal-content"> | |
|                 <div class="modal-header"> | |
|                     <h5 class="modal-title"> | |
|                         <i class="fas fa-plus me-2"></i>Create New Topic | |
|                     </h5> | |
|                     <button type="button" class="btn-close" data-bs-dismiss="modal"></button> | |
|                 </div> | |
|                 <div class="modal-body"> | |
|                     <form id="createTopicForm"> | |
|                         <div class="row"> | |
|                             <div class="col-md-6"> | |
|                                 <div class="mb-3"> | |
|                                     <label for="topicNamespace" class="form-label">Namespace *</label> | |
|                                     <input type="text" class="form-control" id="topicNamespace" name="namespace" required  | |
|                                            placeholder="e.g., default"> | |
|                                 </div> | |
|                             </div> | |
|                             <div class="col-md-6"> | |
|                                 <div class="mb-3"> | |
|                                     <label for="topicName" class="form-label">Topic Name *</label> | |
|                                     <input type="text" class="form-control" id="topicName" name="name" required  | |
|                                            placeholder="e.g., user-events"> | |
|                                 </div> | |
|                             </div> | |
|                         </div> | |
|                         <div class="row"> | |
|                             <div class="col-md-6"> | |
|                                 <div class="mb-3"> | |
|                                     <label for="partitionCount" class="form-label">Partition Count *</label> | |
|                                     <input type="number" class="form-control" id="partitionCount" name="partitionCount"  | |
|                                            required min="1" max="100" value="6"> | |
|                                 </div> | |
|                             </div> | |
|                         </div> | |
|                          | |
|                         <!-- Retention Configuration --> | |
|                         <div class="card mt-3"> | |
|                             <div class="card-header"> | |
|                                 <h6 class="mb-0"> | |
|                                     <i class="fas fa-clock me-2"></i>Retention Policy | |
|                                 </h6> | |
|                             </div> | |
|                             <div class="card-body"> | |
|                                 <div class="form-check mb-3"> | |
|                                     <input class="form-check-input" type="checkbox" id="enableRetention"  | |
|                                            name="enableRetention" onchange="toggleRetentionFields()"> | |
|                                     <label class="form-check-label" for="enableRetention"> | |
|                                         Enable data retention | |
|                                     </label> | |
|                                 </div> | |
|                                 <div id="retentionFields" style="display: none;"> | |
|                                     <div class="row"> | |
|                                         <div class="col-md-6"> | |
|                                             <div class="mb-3"> | |
|                                                 <label for="retentionValue" class="form-label">Retention Duration</label> | |
|                                                 <input type="number" class="form-control" id="retentionValue"  | |
|                                                        name="retentionValue" min="1" value="7"> | |
|                                             </div> | |
|                                         </div> | |
|                                         <div class="col-md-6"> | |
|                                             <div class="mb-3"> | |
|                                                 <label for="retentionUnit" class="form-label">Unit</label> | |
|                                                 <select class="form-control" id="retentionUnit" name="retentionUnit"> | |
|                                                     <option value="hours">Hours</option> | |
|                                                     <option value="days" selected>Days</option> | |
|                                                 </select> | |
|                                             </div> | |
|                                         </div> | |
|                                     </div> | |
|                                     <div class="alert alert-info"> | |
|                                         <i class="fas fa-info-circle me-2"></i> | |
|                                         Data older than this duration will be automatically purged to save storage space. | |
|                                     </div> | |
|                                 </div> | |
|                             </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="createTopic()"> | |
|                         <i class="fas fa-plus me-1"></i>Create Topic | |
|                     </button> | |
|                 </div> | |
|             </div> | |
|         </div> | |
|     </div> | |
| 
 | |
|     <script type="text/javascript"> | |
|         // Topic management functions | |
|         function showCreateTopicModal() { | |
|             var modal = new bootstrap.Modal(document.getElementById('createTopicModal')); | |
|             modal.show(); | |
|         } | |
| 
 | |
|         function toggleRetentionFields() { | |
|             var enableRetention = document.getElementById('enableRetention').checked; | |
|             var retentionFields = document.getElementById('retentionFields'); | |
|              | |
|             if (enableRetention) { | |
|                 retentionFields.style.display = 'block'; | |
|             } else { | |
|                 retentionFields.style.display = 'none'; | |
|             } | |
|         } | |
| 
 | |
|         function createTopic() { | |
|             var form = document.getElementById('createTopicForm'); | |
|             var formData = new FormData(form); | |
|              | |
|             if (!form.checkValidity()) { | |
|                 form.classList.add('was-validated'); | |
|                 return; | |
|             } | |
|              | |
|             var namespace = formData.get('namespace'); | |
|             var name = formData.get('name'); | |
|             var partitionCount = formData.get('partitionCount'); | |
|             var enableRetention = formData.get('enableRetention'); | |
|             var retentionValue = enableRetention === 'on' ? parseInt(formData.get('retentionValue')) : 0; | |
|             var retentionUnit = enableRetention === 'on' ? formData.get('retentionUnit') : 'hours'; | |
|              | |
|             // Convert retention to seconds | |
|             var retentionSeconds = 0; | |
|             if (enableRetention === 'on' && retentionValue > 0) { | |
|                 if (retentionUnit === 'hours') { | |
|                     retentionSeconds = retentionValue * 3600; | |
|                 } else if (retentionUnit === 'days') { | |
|                     retentionSeconds = retentionValue * 86400; | |
|                 } | |
|             } | |
|              | |
|             var topicData = { | |
|                 namespace: namespace, | |
|                 name: name, | |
|                 partition_count: parseInt(partitionCount), | |
|                 retention: { | |
|                     enabled: enableRetention === 'on', | |
|                     retention_seconds: retentionSeconds | |
|                 } | |
|             }; | |
|              | |
|             // Create the topic | |
|             fetch('/api/mq/topics/create', { | |
|                 method: 'POST', | |
|                 headers: { | |
|                     'Content-Type': 'application/json' | |
|                 }, | |
|                 body: JSON.stringify(topicData) | |
|             }) | |
|             .then(response => { | |
|                 if (response.ok) { | |
|                     return response.json(); | |
|                 } | |
|                 throw new Error('Failed to create topic'); | |
|             }) | |
|             .then(data => { | |
|                 // Hide modal and refresh page | |
|                 var modal = bootstrap.Modal.getInstance(document.getElementById('createTopicModal')); | |
|                 modal.hide(); | |
|                 location.reload(); | |
|             }) | |
|             .catch(error => { | |
|                 console.error('Error:', error); | |
|                 alert('Error creating topic: ' + error.message); | |
|             }); | |
|         } | |
| 
 | |
|         function exportTopicsCSV() { | |
|             var csvContent = 'Namespace,Topic Name,Partitions,Retention Enabled,Retention Value,Retention Unit\n'; | |
|              | |
|             var rows = document.querySelectorAll('#topicsTable tbody tr.topic-row'); | |
|             rows.forEach(function(row) { | |
|                 var cells = row.querySelectorAll('td'); | |
|                 var namespace = cells[0].textContent.trim(); | |
|                 var topicName = cells[1].textContent.trim(); | |
|                 var partitions = cells[2].textContent.trim(); | |
|                 var retention = cells[3].textContent.trim(); | |
|                  | |
|                 var retentionEnabled = retention !== 'Disabled'; | |
|                 var retentionValue = retentionEnabled ? retention.split(' ')[0] : ''; | |
|                 var retentionUnit = retentionEnabled ? retention.split(' ')[1] : ''; | |
|                  | |
|                 csvContent += namespace + ',' + topicName + ',' + partitions + ',' + retentionEnabled + ',' + retentionValue + ',' + retentionUnit + '\n'; | |
|             }); | |
|              | |
|             var blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); | |
|             var link = document.createElement('a'); | |
|             var url = URL.createObjectURL(blob); | |
|             link.setAttribute('href', url); | |
|             link.setAttribute('download', 'topics_export.csv'); | |
|             link.style.visibility = 'hidden'; | |
|             document.body.appendChild(link); | |
|             link.click(); | |
|             document.body.removeChild(link); | |
|         } | |
| 
 | |
|         // Topic details functionality | |
|         document.addEventListener('DOMContentLoaded', function() { | |
|             // Handle view topic details buttons | |
|             document.querySelectorAll('[data-action="view-topic-details"]').forEach(function(button) { | |
|                 button.addEventListener('click', function(e) { | |
|                     e.stopPropagation(); | |
|                     var topicName = this.getAttribute('data-topic-name'); | |
|                     var detailsRow = document.getElementById('details-' + topicName.replace(/\./g, '_')); | |
|                      | |
|                     if (detailsRow.style.display === 'none') { | |
|                         detailsRow.style.display = 'table-row'; | |
|                         this.innerHTML = '<i class="fas fa-eye-slash"></i>'; | |
|                          | |
|                         // Load topic details | |
|                         loadTopicDetails(topicName); | |
|                     } else { | |
|                         detailsRow.style.display = 'none'; | |
|                         this.innerHTML = '<i class="fas fa-eye"></i>'; | |
|                     } | |
|                 }); | |
|             }); | |
|         }); | |
| 
 | |
|         function loadTopicDetails(topicName) { | |
|             var detailsRow = document.getElementById('details-' + topicName.replace(/\./g, '_')); | |
|             var contentDiv = detailsRow.querySelector('.topic-details-content'); | |
|              | |
|             fetch('/admin/topics/' + encodeURIComponent(topicName) + '/details') | |
|                 .then(response => response.json()) | |
|                 .then(data => { | |
|                     var html = '<div class="row">'; | |
|                     html += '<div class="col-md-6">'; | |
|                     html += '<h6>Topic Configuration</h6>'; | |
|                     html += '<ul class="list-unstyled">'; | |
|                     html += '<li><strong>Full Name:</strong> ' + data.name + '</li>'; | |
|                     html += '<li><strong>Partitions:</strong> ' + data.partitions + '</li>'; | |
|                     html += '<li><strong>Created:</strong> ' + (data.created || 'N/A') + '</li>'; | |
|                     html += '</ul>'; | |
|                     html += '</div>'; | |
|                     html += '<div class="col-md-6">'; | |
|                     html += '<h6>Retention Policy</h6>'; | |
|                     if (data.retention && data.retention.enabled) { | |
|                         html += '<p><i class="fas fa-check-circle text-success"></i> Enabled</p>'; | |
|                         html += '<p><strong>Duration:</strong> ' + data.retention.value + ' ' + data.retention.unit + '</p>'; | |
|                     } else { | |
|                         html += '<p><i class="fas fa-times-circle text-danger"></i> Disabled</p>'; | |
|                     } | |
|                     html += '</div>'; | |
|                     html += '</div>'; | |
|                      | |
|                     contentDiv.innerHTML = html; | |
|                 }) | |
|                 .catch(error => { | |
|                     console.error('Error loading topic details:', error); | |
|                     contentDiv.innerHTML = '<div class="alert alert-danger">Failed to load topic details</div>'; | |
|                 }); | |
|         } | |
|     </script> | |
| }  |