volume_details.templ 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. package app
  2. import (
  3. "fmt"
  4. "time"
  5. "github.com/seaweedfs/seaweedfs/weed/admin/dash"
  6. )
  7. templ VolumeDetails(data dash.VolumeDetailsData) {
  8. <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
  9. <div>
  10. <h1 class="h2">
  11. <i class="fas fa-database me-2"></i>Volume Details
  12. </h1>
  13. <nav aria-label="breadcrumb">
  14. <ol class="breadcrumb">
  15. <li class="breadcrumb-item"><a href="/admin" class="text-decoration-none">Dashboard</a></li>
  16. <li class="breadcrumb-item"><a href="/cluster/volumes" class="text-decoration-none">Volumes</a></li>
  17. <li class="breadcrumb-item active" aria-current="page">Volume {fmt.Sprintf("%d", data.Volume.Id)}</li>
  18. </ol>
  19. </nav>
  20. </div>
  21. <div class="btn-toolbar mb-2 mb-md-0">
  22. <div class="btn-group me-2">
  23. <button type="button" class="btn btn-sm btn-outline-secondary" onclick="history.back()">
  24. <i class="fas fa-arrow-left me-1"></i>Back
  25. </button>
  26. <button type="button" class="btn btn-sm btn-outline-primary" onclick="window.location.reload()">
  27. <i class="fas fa-refresh me-1"></i>Refresh
  28. </button>
  29. </div>
  30. </div>
  31. </div>
  32. <div class="row">
  33. <!-- Volume Information Card -->
  34. <div class="col-lg-8">
  35. <div class="card shadow mb-4">
  36. <div class="card-header py-3">
  37. <h6 class="m-0 font-weight-bold text-primary">
  38. <i class="fas fa-info-circle me-2"></i>Volume Information
  39. </h6>
  40. </div>
  41. <div class="card-body">
  42. <div class="row">
  43. <div class="col-md-6">
  44. <div class="mb-3">
  45. <label class="form-label"><strong>Volume ID:</strong></label>
  46. <div><code class="fs-5">{fmt.Sprintf("%d", data.Volume.Id)}</code></div>
  47. </div>
  48. <div class="mb-3">
  49. <label class="form-label"><strong>Server:</strong></label>
  50. <div>
  51. <a href={templ.SafeURL(fmt.Sprintf("http://%s/ui/index.html", data.Volume.Server))} target="_blank" class="text-decoration-none">
  52. {data.Volume.Server}
  53. <i class="fas fa-external-link-alt ms-1 text-muted"></i>
  54. </a>
  55. </div>
  56. </div>
  57. <div class="mb-3">
  58. <label class="form-label"><strong>Data Center:</strong></label>
  59. <div><span class="badge bg-light text-dark">{data.Volume.DataCenter}</span></div>
  60. </div>
  61. <div class="mb-3">
  62. <label class="form-label"><strong>Rack:</strong></label>
  63. <div><span class="badge bg-light text-dark">{data.Volume.Rack}</span></div>
  64. </div>
  65. </div>
  66. <div class="col-md-6">
  67. <div class="mb-3">
  68. <label class="form-label"><strong>Collection:</strong></label>
  69. <div>
  70. if data.Volume.Collection == "" {
  71. <a href={templ.SafeURL("/cluster/volumes?collection=default")} class="text-decoration-none">
  72. <span class="badge bg-secondary">default</span>
  73. </a>
  74. } else {
  75. <a href={templ.SafeURL(fmt.Sprintf("/cluster/volumes?collection=%s", data.Volume.Collection))} class="text-decoration-none">
  76. <span class="badge bg-secondary">{data.Volume.Collection}</span>
  77. </a>
  78. }
  79. </div>
  80. </div>
  81. <div class="mb-3">
  82. <label class="form-label"><strong>Replication:</strong></label>
  83. <div><span class="badge bg-info">{fmt.Sprintf("%03d", data.Volume.ReplicaPlacement)}</span></div>
  84. </div>
  85. <div class="mb-3">
  86. <label class="form-label"><strong>Disk Type:</strong></label>
  87. <div>
  88. <span class="badge bg-primary">
  89. if data.Volume.DiskType == "" {
  90. hdd
  91. } else {
  92. {data.Volume.DiskType}
  93. }
  94. </span>
  95. </div>
  96. </div>
  97. <div class="mb-3">
  98. <label class="form-label"><strong>Version:</strong></label>
  99. <div><span class="badge bg-dark">{fmt.Sprintf("v%d", data.Volume.Version)}</span></div>
  100. </div>
  101. </div>
  102. </div>
  103. </div>
  104. </div>
  105. </div>
  106. <!-- Statistics Card -->
  107. <div class="col-lg-4">
  108. <!-- Volume Statistics & Health Card -->
  109. <div class="card shadow mb-4">
  110. <div class="card-header py-3">
  111. <h6 class="m-0 font-weight-bold text-primary">
  112. <i class="fas fa-chart-pie me-2"></i>Volume Statistics & Health
  113. </h6>
  114. </div>
  115. <div class="card-body">
  116. <!-- Storage Metrics -->
  117. <div class="row mb-3">
  118. <div class="col-6">
  119. <div class="text-center">
  120. <div class="h4 mb-0 font-weight-bold text-success">
  121. {formatBytes(int64(data.Volume.Size - data.Volume.DeletedByteCount))}
  122. </div>
  123. <small class="text-muted">Active Bytes</small>
  124. </div>
  125. </div>
  126. <div class="col-6">
  127. <div class="text-center">
  128. <div class="h4 mb-0 font-weight-bold text-danger">
  129. {formatBytes(int64(data.Volume.DeletedByteCount))}
  130. </div>
  131. <small class="text-muted">Deleted Bytes</small>
  132. </div>
  133. </div>
  134. </div>
  135. <!-- File Metrics -->
  136. <div class="row mb-3">
  137. <div class="col-6">
  138. <div class="text-center">
  139. <div class="h4 mb-0 font-weight-bold text-success">
  140. {fmt.Sprintf("%d", data.Volume.FileCount)}
  141. </div>
  142. <small class="text-muted">Active Files</small>
  143. </div>
  144. </div>
  145. <div class="col-6">
  146. <div class="text-center">
  147. <div class="h4 mb-0 font-weight-bold text-danger">
  148. {fmt.Sprintf("%d", data.Volume.DeleteCount)}
  149. </div>
  150. <small class="text-muted">Deleted Files</small>
  151. </div>
  152. </div>
  153. </div>
  154. <!-- Storage Efficiency -->
  155. if data.Volume.FileCount > 0 && data.Volume.Size > 0 {
  156. <div class="mb-3">
  157. <div class="d-flex justify-content-between align-items-center mb-1">
  158. <small class="text-muted">Storage Efficiency</small>
  159. <small class="text-muted">
  160. {fmt.Sprintf("%.1f%%", float64(data.Volume.Size-data.Volume.DeletedByteCount)/float64(data.Volume.Size)*100)}
  161. </small>
  162. </div>
  163. <div class="progress" style="height: 8px;">
  164. <div class="progress-bar bg-info" role="progressbar"
  165. style={fmt.Sprintf("width: %.1f%%", float64(data.Volume.Size-data.Volume.DeletedByteCount)/float64(data.Volume.Size)*100)}
  166. aria-valuenow={fmt.Sprintf("%.1f", float64(data.Volume.Size-data.Volume.DeletedByteCount)/float64(data.Volume.Size)*100)}
  167. aria-valuemin="0" aria-valuemax="100">
  168. </div>
  169. </div>
  170. </div>
  171. }
  172. <hr class="my-3">
  173. <!-- Status & Configuration -->
  174. <div class="row mb-3">
  175. <div class="col-12">
  176. <div class="text-center mb-2">
  177. if data.Volume.ReadOnly {
  178. <span class="badge bg-warning fs-6 px-3 py-2">
  179. <i class="fas fa-lock me-1"></i>Read Only
  180. </span>
  181. if data.Volume.Size >= data.VolumeSizeLimit {
  182. <div class="mt-1">
  183. <small class="text-muted">Size limit exceeded</small>
  184. </div>
  185. }
  186. } else if data.VolumeSizeLimit > data.Volume.Size {
  187. <span class="badge bg-success fs-6 px-3 py-2">
  188. <i class="fas fa-edit me-1"></i>Read/Write
  189. </span>
  190. } else {
  191. <span class="badge bg-warning fs-6 px-3 py-2">
  192. <i class="fas fa-exclamation-triangle me-1"></i>Size Limit Reached
  193. </span>
  194. }
  195. </div>
  196. </div>
  197. </div>
  198. <!-- Maintenance Info -->
  199. <div class="row mb-3">
  200. <div class="col-6">
  201. <div class="text-center">
  202. <div class="h6 mb-0 font-weight-bold text-info">
  203. #{fmt.Sprintf("%d", data.Volume.CompactRevision)}
  204. </div>
  205. <small class="text-muted">Vacuum Revision</small>
  206. </div>
  207. </div>
  208. <div class="col-6">
  209. <div class="text-center">
  210. <div class="h6 mb-0 font-weight-bold text-secondary">
  211. if data.Volume.ModifiedAtSecond > 0 {
  212. {formatTimestamp(data.Volume.ModifiedAtSecond)}
  213. } else {
  214. <span class="text-muted">Never modified</span>
  215. }
  216. </div>
  217. <small class="text-muted">Last Modified</small>
  218. </div>
  219. </div>
  220. </div>
  221. <!-- TTL Configuration -->
  222. if data.Volume.Ttl > 0 {
  223. <div class="mb-3 text-center">
  224. <span class="badge bg-info fs-6 px-3 py-2">
  225. <i class="fas fa-clock me-1"></i>{formatTTL(data.Volume.Ttl)}
  226. </span>
  227. <div class="mt-1">
  228. <small class="text-muted">Time To Live</small>
  229. </div>
  230. </div>
  231. }
  232. <!-- Remote Storage Configuration -->
  233. if data.Volume.RemoteStorageName != "" {
  234. <hr class="my-3">
  235. <div class="mb-2">
  236. <div class="text-center">
  237. <div class="h6 mb-1 font-weight-bold text-info">
  238. <i class="fas fa-cloud me-1"></i>{data.Volume.RemoteStorageName}
  239. </div>
  240. <small class="text-muted">Remote Storage</small>
  241. </div>
  242. </div>
  243. if data.Volume.RemoteStorageKey != "" {
  244. <div class="text-center">
  245. <div class="text-xs font-monospace bg-light p-2 rounded text-truncate" title={data.Volume.RemoteStorageKey}>
  246. {data.Volume.RemoteStorageKey}
  247. </div>
  248. <small class="text-muted">Storage Key</small>
  249. </div>
  250. }
  251. }
  252. </div>
  253. </div>
  254. </div>
  255. </div>
  256. <!-- Replicas Card -->
  257. if len(data.Replicas) > 0 {
  258. <div class="row">
  259. <div class="col-12">
  260. <div class="card shadow mb-4">
  261. <div class="card-header py-3">
  262. <h6 class="m-0 font-weight-bold text-primary">
  263. <i class="fas fa-copy me-2"></i>Replicas ({fmt.Sprintf("%d", data.ReplicationCount)})
  264. </h6>
  265. </div>
  266. <div class="card-body">
  267. <div class="table-responsive">
  268. <table class="table table-hover">
  269. <thead>
  270. <tr>
  271. <th>Server</th>
  272. <th>Data Center</th>
  273. <th>Rack</th>
  274. <th>Size</th>
  275. <th>File Count</th>
  276. <th>Status</th>
  277. <th>Actions</th>
  278. </tr>
  279. </thead>
  280. <tbody>
  281. <!-- Primary Volume (current one) -->
  282. <tr class="table-primary">
  283. <td>
  284. <strong>
  285. <a href={templ.SafeURL(fmt.Sprintf("http://%s/ui/index.html", data.Volume.Server))} target="_blank" class="text-decoration-none">
  286. {data.Volume.Server}
  287. <i class="fas fa-external-link-alt ms-1 text-muted"></i>
  288. </a>
  289. </strong>
  290. <span class="badge bg-success ms-2">Primary</span>
  291. </td>
  292. <td><span class="badge bg-light text-dark">{data.Volume.DataCenter}</span></td>
  293. <td><span class="badge bg-light text-dark">{data.Volume.Rack}</span></td>
  294. <td>{formatBytes(int64(data.Volume.Size))}</td>
  295. <td>{fmt.Sprintf("%d", data.Volume.FileCount)}</td>
  296. <td><span class="badge bg-success">Active</span></td>
  297. <td>
  298. <span class="text-muted">Current Volume</span>
  299. </td>
  300. </tr>
  301. <!-- Replica Volumes -->
  302. for _, replica := range data.Replicas {
  303. <tr>
  304. <td>
  305. <a href={templ.SafeURL(fmt.Sprintf("http://%s/ui/index.html", replica.Server))} target="_blank" class="text-decoration-none">
  306. {replica.Server}
  307. <i class="fas fa-external-link-alt ms-1 text-muted"></i>
  308. </a>
  309. </td>
  310. <td><span class="badge bg-light text-dark">{replica.DataCenter}</span></td>
  311. <td><span class="badge bg-light text-dark">{replica.Rack}</span></td>
  312. <td>{formatBytes(int64(replica.Size))}</td>
  313. <td>{fmt.Sprintf("%d", replica.FileCount)}</td>
  314. <td><span class="badge bg-info">Replica</span></td>
  315. <td>
  316. <a href={templ.SafeURL(fmt.Sprintf("/cluster/volumes/%d/%s", replica.Id, replica.Server))} class="btn btn-sm btn-outline-primary">
  317. <i class="fas fa-eye me-1"></i>View
  318. </a>
  319. </td>
  320. </tr>
  321. }
  322. </tbody>
  323. </table>
  324. </div>
  325. </div>
  326. </div>
  327. </div>
  328. </div>
  329. }
  330. <!-- Actions Card -->
  331. <div class="row">
  332. <div class="col-12">
  333. <div class="card shadow mb-4">
  334. <div class="card-header py-3">
  335. <h6 class="m-0 font-weight-bold text-primary">
  336. <i class="fas fa-tools me-2"></i>Actions
  337. </h6>
  338. </div>
  339. <div class="card-body">
  340. <div class="btn-group" role="group">
  341. <button type="button" class="btn btn-outline-danger vacuum-btn"
  342. title="Vacuum Volume"
  343. data-volume-id={fmt.Sprintf("%d", data.Volume.Id)}
  344. data-server={data.Volume.Server}>
  345. <i class="fas fa-compress-alt me-1"></i>Vacuum
  346. </button>
  347. </div>
  348. <div class="mt-3">
  349. <small class="text-muted">
  350. <i class="fas fa-info-circle me-1"></i>
  351. Use these actions to perform maintenance operations on the volume.
  352. </small>
  353. </div>
  354. </div>
  355. </div>
  356. </div>
  357. </div>
  358. <!-- Last Updated -->
  359. <div class="row">
  360. <div class="col-12">
  361. <small class="text-muted">
  362. <i class="fas fa-clock me-1"></i>
  363. Last updated: {data.LastUpdated.Format("2006-01-02 15:04:05")}
  364. </small>
  365. </div>
  366. </div>
  367. <!-- JavaScript for volume actions -->
  368. <script>
  369. document.addEventListener('DOMContentLoaded', function() {
  370. // Add click handler for vacuum button
  371. const vacuumBtn = document.querySelector('.vacuum-btn');
  372. if (vacuumBtn) {
  373. vacuumBtn.addEventListener('click', function() {
  374. const volumeId = this.getAttribute('data-volume-id');
  375. const server = this.getAttribute('data-server');
  376. performVacuum(volumeId, server, this);
  377. });
  378. }
  379. });
  380. function performVacuum(volumeId, server, button) {
  381. // Disable button and show loading state
  382. const originalText = button.innerHTML;
  383. button.disabled = true;
  384. button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Vacuuming...';
  385. // Send vacuum request
  386. fetch(`/api/volumes/${volumeId}/${encodeURIComponent(server)}/vacuum`, {
  387. method: 'POST',
  388. headers: {
  389. 'Content-Type': 'application/json',
  390. }
  391. })
  392. .then(response => response.json())
  393. .then(data => {
  394. if (data.error) {
  395. showMessage(data.error, 'error');
  396. } else {
  397. showMessage(data.message || 'Volume vacuum started successfully', 'success');
  398. // Optionally refresh the page after a delay
  399. setTimeout(() => {
  400. window.location.reload();
  401. }, 2000);
  402. }
  403. })
  404. .catch(error => {
  405. console.error('Error:', error);
  406. showMessage('Failed to start vacuum operation', 'error');
  407. })
  408. .finally(() => {
  409. // Re-enable button
  410. button.disabled = false;
  411. button.innerHTML = originalText;
  412. });
  413. }
  414. function showMessage(message, type) {
  415. // Create toast notification
  416. const toast = document.createElement('div');
  417. toast.className = `alert alert-${type === 'error' ? 'danger' : 'success'} alert-dismissible fade show position-fixed`;
  418. toast.style.top = '20px';
  419. toast.style.right = '20px';
  420. toast.style.zIndex = '9999';
  421. toast.style.minWidth = '300px';
  422. toast.innerHTML = `
  423. ${message}
  424. <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
  425. `;
  426. document.body.appendChild(toast);
  427. // Auto-remove after 5 seconds
  428. setTimeout(() => {
  429. if (toast.parentNode) {
  430. toast.parentNode.removeChild(toast);
  431. }
  432. }, 5000);
  433. }
  434. </script>
  435. }
  436. func formatTimestamp(unixTimestamp int64) string {
  437. if unixTimestamp <= 0 {
  438. return "Never"
  439. }
  440. t := time.Unix(unixTimestamp, 0)
  441. return t.Format("2006-01-02 15:04:05")
  442. }
  443. func formatTTL(ttlSeconds uint32) string {
  444. if ttlSeconds == 0 {
  445. return "No TTL"
  446. }
  447. duration := time.Duration(ttlSeconds) * time.Second
  448. // Convert to human readable format
  449. days := int(duration.Hours()) / 24
  450. hours := int(duration.Hours()) % 24
  451. minutes := int(duration.Minutes()) % 60
  452. if days > 0 {
  453. if hours > 0 {
  454. return fmt.Sprintf("%dd %dh", days, hours)
  455. }
  456. return fmt.Sprintf("%d days", days)
  457. } else if hours > 0 {
  458. if minutes > 0 {
  459. return fmt.Sprintf("%dh %dm", hours, minutes)
  460. }
  461. return fmt.Sprintf("%d hours", hours)
  462. } else if minutes > 0 {
  463. return fmt.Sprintf("%d minutes", minutes)
  464. } else {
  465. return fmt.Sprintf("%d seconds", int(duration.Seconds()))
  466. }
  467. }