topics.templ 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. package app
  2. import "fmt"
  3. import "strings"
  4. import "github.com/seaweedfs/seaweedfs/weed/admin/dash"
  5. templ Topics(data dash.TopicsData) {
  6. <div class="container-fluid">
  7. <div class="row">
  8. <div class="col-12">
  9. <div class="d-flex justify-content-between align-items-center mb-4">
  10. <h1 class="h3 mb-0">Message Queue Topics</h1>
  11. <small class="text-muted">Last updated: {data.LastUpdated.Format("2006-01-02 15:04:05")}</small>
  12. </div>
  13. <!-- Summary Cards -->
  14. <div class="row mb-4">
  15. <div class="col-md-6">
  16. <div class="card text-center">
  17. <div class="card-body">
  18. <h5 class="card-title">Total Topics</h5>
  19. <h3 class="text-primary">{fmt.Sprintf("%d", data.TotalTopics)}</h3>
  20. </div>
  21. </div>
  22. </div>
  23. <div class="col-md-6">
  24. <div class="card text-center">
  25. <div class="card-body">
  26. <h5 class="card-title">Available Topics</h5>
  27. <h3 class="text-info">{fmt.Sprintf("%d", len(data.Topics))}</h3>
  28. </div>
  29. </div>
  30. </div>
  31. </div>
  32. <!-- Topics Table -->
  33. <div class="card">
  34. <div class="card-header d-flex justify-content-between align-items-center">
  35. <h5 class="mb-0">Topics</h5>
  36. <div>
  37. <button class="btn btn-sm btn-primary me-2" onclick="showCreateTopicModal()">
  38. <i class="fas fa-plus me-1"></i>Create Topic
  39. </button>
  40. <button class="btn btn-sm btn-outline-secondary" onclick="exportTopicsCSV()">
  41. <i class="fas fa-download me-1"></i>Export CSV
  42. </button>
  43. </div>
  44. </div>
  45. <div class="card-body">
  46. if len(data.Topics) == 0 {
  47. <div class="text-center py-4">
  48. <i class="fas fa-list-alt fa-3x text-muted mb-3"></i>
  49. <h5>No Topics Found</h5>
  50. <p class="text-muted">No message queue topics are currently configured.</p>
  51. </div>
  52. } else {
  53. <div class="table-responsive">
  54. <table class="table table-striped" id="topicsTable">
  55. <thead>
  56. <tr>
  57. <th>Namespace</th>
  58. <th>Topic Name</th>
  59. <th>Partitions</th>
  60. <th>Retention</th>
  61. <th>Actions</th>
  62. </tr>
  63. </thead>
  64. <tbody>
  65. for _, topic := range data.Topics {
  66. <tr class="topic-row" data-topic-name={topic.Name} style="cursor: pointer;">
  67. <td>
  68. <span class="badge bg-secondary">{func() string {
  69. idx := strings.LastIndex(topic.Name, ".")
  70. if idx == -1 {
  71. return "default"
  72. }
  73. return topic.Name[:idx]
  74. }()}</span>
  75. </td>
  76. <td>
  77. <strong>{func() string {
  78. idx := strings.LastIndex(topic.Name, ".")
  79. if idx == -1 {
  80. return topic.Name
  81. }
  82. return topic.Name[idx+1:]
  83. }()}</strong>
  84. </td>
  85. <td>
  86. <span class="badge bg-info">{fmt.Sprintf("%d", topic.Partitions)}</span>
  87. </td>
  88. <td>
  89. if topic.Retention.Enabled {
  90. <span class="badge bg-success">
  91. <i class="fas fa-clock me-1"></i>
  92. {fmt.Sprintf("%d %s", topic.Retention.DisplayValue, topic.Retention.DisplayUnit)}
  93. </span>
  94. } else {
  95. <span class="badge bg-secondary">
  96. <i class="fas fa-times me-1"></i>Disabled
  97. </span>
  98. }
  99. </td>
  100. <td>
  101. <button class="btn btn-sm btn-outline-primary" data-action="view-topic-details" data-topic-name={ topic.Name }>
  102. <i class="fas fa-eye"></i>
  103. </button>
  104. </td>
  105. </tr>
  106. <tr class="topic-details-row" id={ fmt.Sprintf("details-%s", strings.ReplaceAll(topic.Name, ".", "_")) } style="display: none;">
  107. <td colspan="5">
  108. <div class="topic-details-content">
  109. <div class="text-center py-3">
  110. <i class="fas fa-spinner fa-spin"></i> Loading topic details...
  111. </div>
  112. </div>
  113. </td>
  114. </tr>
  115. }
  116. </tbody>
  117. </table>
  118. </div>
  119. }
  120. </div>
  121. </div>
  122. </div>
  123. </div>
  124. </div>
  125. <!-- Create Topic Modal -->
  126. <div class="modal fade" id="createTopicModal" tabindex="-1" role="dialog">
  127. <div class="modal-dialog modal-lg" role="document">
  128. <div class="modal-content">
  129. <div class="modal-header">
  130. <h5 class="modal-title">
  131. <i class="fas fa-plus me-2"></i>Create New Topic
  132. </h5>
  133. <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
  134. </div>
  135. <div class="modal-body">
  136. <form id="createTopicForm">
  137. <div class="row">
  138. <div class="col-md-6">
  139. <div class="mb-3">
  140. <label for="topicNamespace" class="form-label">Namespace *</label>
  141. <input type="text" class="form-control" id="topicNamespace" name="namespace" required
  142. placeholder="e.g., default">
  143. </div>
  144. </div>
  145. <div class="col-md-6">
  146. <div class="mb-3">
  147. <label for="topicName" class="form-label">Topic Name *</label>
  148. <input type="text" class="form-control" id="topicName" name="name" required
  149. placeholder="e.g., user-events">
  150. </div>
  151. </div>
  152. </div>
  153. <div class="row">
  154. <div class="col-md-6">
  155. <div class="mb-3">
  156. <label for="partitionCount" class="form-label">Partition Count *</label>
  157. <input type="number" class="form-control" id="partitionCount" name="partitionCount"
  158. required min="1" max="100" value="6">
  159. </div>
  160. </div>
  161. </div>
  162. <!-- Retention Configuration -->
  163. <div class="card mt-3">
  164. <div class="card-header">
  165. <h6 class="mb-0">
  166. <i class="fas fa-clock me-2"></i>Retention Policy
  167. </h6>
  168. </div>
  169. <div class="card-body">
  170. <div class="form-check mb-3">
  171. <input class="form-check-input" type="checkbox" id="enableRetention"
  172. name="enableRetention" onchange="toggleRetentionFields()">
  173. <label class="form-check-label" for="enableRetention">
  174. Enable data retention
  175. </label>
  176. </div>
  177. <div id="retentionFields" style="display: none;">
  178. <div class="row">
  179. <div class="col-md-6">
  180. <div class="mb-3">
  181. <label for="retentionValue" class="form-label">Retention Duration</label>
  182. <input type="number" class="form-control" id="retentionValue"
  183. name="retentionValue" min="1" value="7">
  184. </div>
  185. </div>
  186. <div class="col-md-6">
  187. <div class="mb-3">
  188. <label for="retentionUnit" class="form-label">Unit</label>
  189. <select class="form-control" id="retentionUnit" name="retentionUnit">
  190. <option value="hours">Hours</option>
  191. <option value="days" selected>Days</option>
  192. </select>
  193. </div>
  194. </div>
  195. </div>
  196. <div class="alert alert-info">
  197. <i class="fas fa-info-circle me-2"></i>
  198. Data older than this duration will be automatically purged to save storage space.
  199. </div>
  200. </div>
  201. </div>
  202. </div>
  203. </form>
  204. </div>
  205. <div class="modal-footer">
  206. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
  207. <button type="button" class="btn btn-primary" onclick="createTopic()">
  208. <i class="fas fa-plus me-1"></i>Create Topic
  209. </button>
  210. </div>
  211. </div>
  212. </div>
  213. </div>
  214. <script type="text/javascript">
  215. // Topic management functions
  216. function showCreateTopicModal() {
  217. var modal = new bootstrap.Modal(document.getElementById('createTopicModal'));
  218. modal.show();
  219. }
  220. function toggleRetentionFields() {
  221. var enableRetention = document.getElementById('enableRetention').checked;
  222. var retentionFields = document.getElementById('retentionFields');
  223. if (enableRetention) {
  224. retentionFields.style.display = 'block';
  225. } else {
  226. retentionFields.style.display = 'none';
  227. }
  228. }
  229. function createTopic() {
  230. var form = document.getElementById('createTopicForm');
  231. var formData = new FormData(form);
  232. if (!form.checkValidity()) {
  233. form.classList.add('was-validated');
  234. return;
  235. }
  236. var namespace = formData.get('namespace');
  237. var name = formData.get('name');
  238. var partitionCount = formData.get('partitionCount');
  239. var enableRetention = formData.get('enableRetention');
  240. var retentionValue = enableRetention === 'on' ? parseInt(formData.get('retentionValue')) : 0;
  241. var retentionUnit = enableRetention === 'on' ? formData.get('retentionUnit') : 'hours';
  242. // Convert retention to seconds
  243. var retentionSeconds = 0;
  244. if (enableRetention === 'on' && retentionValue > 0) {
  245. if (retentionUnit === 'hours') {
  246. retentionSeconds = retentionValue * 3600;
  247. } else if (retentionUnit === 'days') {
  248. retentionSeconds = retentionValue * 86400;
  249. }
  250. }
  251. var topicData = {
  252. namespace: namespace,
  253. name: name,
  254. partition_count: parseInt(partitionCount),
  255. retention: {
  256. enabled: enableRetention === 'on',
  257. retention_seconds: retentionSeconds
  258. }
  259. };
  260. // Create the topic
  261. fetch('/api/mq/topics/create', {
  262. method: 'POST',
  263. headers: {
  264. 'Content-Type': 'application/json'
  265. },
  266. body: JSON.stringify(topicData)
  267. })
  268. .then(response => {
  269. if (response.ok) {
  270. return response.json();
  271. }
  272. throw new Error('Failed to create topic');
  273. })
  274. .then(data => {
  275. // Hide modal and refresh page
  276. var modal = bootstrap.Modal.getInstance(document.getElementById('createTopicModal'));
  277. modal.hide();
  278. location.reload();
  279. })
  280. .catch(error => {
  281. console.error('Error:', error);
  282. alert('Error creating topic: ' + error.message);
  283. });
  284. }
  285. function exportTopicsCSV() {
  286. var csvContent = 'Namespace,Topic Name,Partitions,Retention Enabled,Retention Value,Retention Unit\n';
  287. var rows = document.querySelectorAll('#topicsTable tbody tr.topic-row');
  288. rows.forEach(function(row) {
  289. var cells = row.querySelectorAll('td');
  290. var namespace = cells[0].textContent.trim();
  291. var topicName = cells[1].textContent.trim();
  292. var partitions = cells[2].textContent.trim();
  293. var retention = cells[3].textContent.trim();
  294. var retentionEnabled = retention !== 'Disabled';
  295. var retentionValue = retentionEnabled ? retention.split(' ')[0] : '';
  296. var retentionUnit = retentionEnabled ? retention.split(' ')[1] : '';
  297. csvContent += namespace + ',' + topicName + ',' + partitions + ',' + retentionEnabled + ',' + retentionValue + ',' + retentionUnit + '\n';
  298. });
  299. var blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
  300. var link = document.createElement('a');
  301. var url = URL.createObjectURL(blob);
  302. link.setAttribute('href', url);
  303. link.setAttribute('download', 'topics_export.csv');
  304. link.style.visibility = 'hidden';
  305. document.body.appendChild(link);
  306. link.click();
  307. document.body.removeChild(link);
  308. }
  309. // Topic details functionality
  310. document.addEventListener('DOMContentLoaded', function() {
  311. // Handle view topic details buttons
  312. document.querySelectorAll('[data-action="view-topic-details"]').forEach(function(button) {
  313. button.addEventListener('click', function(e) {
  314. e.stopPropagation();
  315. var topicName = this.getAttribute('data-topic-name');
  316. var detailsRow = document.getElementById('details-' + topicName.replace(/\./g, '_'));
  317. if (detailsRow.style.display === 'none') {
  318. detailsRow.style.display = 'table-row';
  319. this.innerHTML = '<i class="fas fa-eye-slash"></i>';
  320. // Load topic details
  321. loadTopicDetails(topicName);
  322. } else {
  323. detailsRow.style.display = 'none';
  324. this.innerHTML = '<i class="fas fa-eye"></i>';
  325. }
  326. });
  327. });
  328. });
  329. function loadTopicDetails(topicName) {
  330. var detailsRow = document.getElementById('details-' + topicName.replace(/\./g, '_'));
  331. var contentDiv = detailsRow.querySelector('.topic-details-content');
  332. fetch('/admin/topics/' + encodeURIComponent(topicName) + '/details')
  333. .then(response => response.json())
  334. .then(data => {
  335. var html = '<div class="row">';
  336. html += '<div class="col-md-6">';
  337. html += '<h6>Topic Configuration</h6>';
  338. html += '<ul class="list-unstyled">';
  339. html += '<li><strong>Full Name:</strong> ' + data.name + '</li>';
  340. html += '<li><strong>Partitions:</strong> ' + data.partitions + '</li>';
  341. html += '<li><strong>Created:</strong> ' + (data.created || 'N/A') + '</li>';
  342. html += '</ul>';
  343. html += '</div>';
  344. html += '<div class="col-md-6">';
  345. html += '<h6>Retention Policy</h6>';
  346. if (data.retention && data.retention.enabled) {
  347. html += '<p><i class="fas fa-check-circle text-success"></i> Enabled</p>';
  348. html += '<p><strong>Duration:</strong> ' + data.retention.value + ' ' + data.retention.unit + '</p>';
  349. } else {
  350. html += '<p><i class="fas fa-times-circle text-danger"></i> Disabled</p>';
  351. }
  352. html += '</div>';
  353. html += '</div>';
  354. contentDiv.innerHTML = html;
  355. })
  356. .catch(error => {
  357. console.error('Error loading topic details:', error);
  358. contentDiv.innerHTML = '<div class="alert alert-danger">Failed to load topic details</div>';
  359. });
  360. }
  361. </script>
  362. }