cluster_volumes.templ 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. package app
  2. import (
  3. "fmt"
  4. "strings"
  5. "github.com/seaweedfs/seaweedfs/weed/admin/dash"
  6. )
  7. templ ClusterVolumes(data dash.ClusterVolumesData) {
  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>Cluster Volumes
  12. </h1>
  13. if data.FilterCollection != "" {
  14. <div class="d-flex align-items-center mt-2">
  15. <span class="badge bg-info me-2">
  16. <i class="fas fa-filter me-1"></i>Collection: {data.FilterCollection}
  17. </span>
  18. <a href="/cluster/volumes" class="btn btn-sm btn-outline-secondary">
  19. <i class="fas fa-times me-1"></i>Clear Filter
  20. </a>
  21. </div>
  22. }
  23. </div>
  24. <div class="btn-toolbar mb-2 mb-md-0">
  25. <div class="btn-group me-2">
  26. <select class="form-select form-select-sm me-2" id="pageSizeSelect" onchange="changePageSize()" style="width: auto;">
  27. <option value="50" if data.PageSize == 50 { selected="selected" }>50 per page</option>
  28. <option value="100" if data.PageSize == 100 { selected="selected" }>100 per page</option>
  29. <option value="200" if data.PageSize == 200 { selected="selected" }>200 per page</option>
  30. <option value="500" if data.PageSize == 500 { selected="selected" }>500 per page</option>
  31. </select>
  32. <button type="button" class="btn btn-sm btn-outline-primary" onclick="exportVolumes()">
  33. <i class="fas fa-download me-1"></i>Export
  34. </button>
  35. </div>
  36. </div>
  37. </div>
  38. <div id="volumes-content">
  39. <!-- Summary Cards -->
  40. <div class="row mb-4">
  41. <div class="col-xl-2 col-md-4 col-sm-6 mb-4">
  42. <div class="card border-left-primary shadow h-100 py-2">
  43. <div class="card-body">
  44. <div class="row no-gutters align-items-center">
  45. <div class="col mr-2">
  46. <div class="text-xs font-weight-bold text-primary text-uppercase mb-1">
  47. Total Volumes
  48. </div>
  49. <div class="h5 mb-0 font-weight-bold text-gray-800">
  50. {fmt.Sprintf("%d", data.TotalVolumes)}
  51. </div>
  52. </div>
  53. <div class="col-auto">
  54. <i class="fas fa-database fa-2x text-gray-300"></i>
  55. </div>
  56. </div>
  57. </div>
  58. </div>
  59. </div>
  60. <div class="col-xl-2 col-md-4 col-sm-6 mb-4">
  61. <div class="card border-left-success shadow h-100 py-2">
  62. <div class="card-body">
  63. <div class="row no-gutters align-items-center">
  64. <div class="col mr-2">
  65. <div class="text-xs font-weight-bold text-success text-uppercase mb-1">
  66. if data.CollectionCount == 1 {
  67. Collection
  68. } else {
  69. Collections
  70. }
  71. </div>
  72. <div class="h5 mb-0 font-weight-bold text-gray-800">
  73. if data.CollectionCount == 1 {
  74. {data.SingleCollection}
  75. } else {
  76. {fmt.Sprintf("%d", data.CollectionCount)}
  77. }
  78. </div>
  79. </div>
  80. <div class="col-auto">
  81. <i class="fas fa-layer-group fa-2x text-gray-300"></i>
  82. </div>
  83. </div>
  84. </div>
  85. </div>
  86. </div>
  87. <div class="col-xl-2 col-md-4 col-sm-6 mb-4">
  88. <div class="card border-left-info shadow h-100 py-2">
  89. <div class="card-body">
  90. <div class="row no-gutters align-items-center">
  91. <div class="col mr-2">
  92. <div class="text-xs font-weight-bold text-info text-uppercase mb-1">
  93. if data.DataCenterCount == 1 {
  94. Data Center
  95. } else {
  96. Data Centers
  97. }
  98. </div>
  99. <div class="h5 mb-0 font-weight-bold text-gray-800">
  100. if data.DataCenterCount == 1 {
  101. {data.SingleDataCenter}
  102. } else {
  103. {fmt.Sprintf("%d", data.DataCenterCount)}
  104. }
  105. </div>
  106. </div>
  107. <div class="col-auto">
  108. <i class="fas fa-building fa-2x text-gray-300"></i>
  109. </div>
  110. </div>
  111. </div>
  112. </div>
  113. </div>
  114. <div class="col-xl-2 col-md-4 col-sm-6 mb-4">
  115. <div class="card border-left-secondary shadow h-100 py-2">
  116. <div class="card-body">
  117. <div class="row no-gutters align-items-center">
  118. <div class="col mr-2">
  119. <div class="text-xs font-weight-bold text-secondary text-uppercase mb-1">
  120. if data.RackCount == 1 {
  121. Rack
  122. } else {
  123. Racks
  124. }
  125. </div>
  126. <div class="h5 mb-0 font-weight-bold text-gray-800">
  127. if data.RackCount == 1 {
  128. {data.SingleRack}
  129. } else {
  130. {fmt.Sprintf("%d", data.RackCount)}
  131. }
  132. </div>
  133. </div>
  134. <div class="col-auto">
  135. <i class="fas fa-server fa-2x text-gray-300"></i>
  136. </div>
  137. </div>
  138. </div>
  139. </div>
  140. </div>
  141. <div class="col-xl-2 col-md-4 col-sm-6 mb-4">
  142. <div class="card border-left-dark shadow h-100 py-2">
  143. <div class="card-body">
  144. <div class="row no-gutters align-items-center">
  145. <div class="col mr-2">
  146. <div class="text-xs font-weight-bold text-dark text-uppercase mb-1">
  147. if data.DiskTypeCount == 1 {
  148. Disk Type
  149. } else {
  150. Disk Types
  151. }
  152. </div>
  153. <div class="h5 mb-0 font-weight-bold text-gray-800">
  154. if data.DiskTypeCount == 1 {
  155. {data.SingleDiskType}
  156. } else {
  157. {strings.Join(data.AllDiskTypes, ", ")}
  158. }
  159. </div>
  160. </div>
  161. <div class="col-auto">
  162. <i class="fas fa-hdd fa-2x text-gray-300"></i>
  163. </div>
  164. </div>
  165. </div>
  166. </div>
  167. </div>
  168. <div class="col-xl-2 col-md-4 col-sm-6 mb-4">
  169. <div class="card border-left-purple shadow h-100 py-2">
  170. <div class="card-body">
  171. <div class="row no-gutters align-items-center">
  172. <div class="col mr-2">
  173. <div class="text-xs font-weight-bold text-purple text-uppercase mb-1">
  174. if data.VersionCount == 1 {
  175. Version
  176. } else {
  177. Versions
  178. }
  179. </div>
  180. <div class="h5 mb-0 font-weight-bold text-gray-800">
  181. if data.VersionCount == 1 {
  182. {data.SingleVersion}
  183. } else {
  184. {strings.Join(data.AllVersions, ", ")}
  185. }
  186. </div>
  187. </div>
  188. <div class="col-auto">
  189. <i class="fas fa-code-branch fa-2x text-gray-300"></i>
  190. </div>
  191. </div>
  192. </div>
  193. </div>
  194. </div>
  195. <div class="col-xl-2 col-md-4 col-sm-6 mb-4">
  196. <div class="card border-left-warning shadow h-100 py-2">
  197. <div class="card-body">
  198. <div class="row no-gutters align-items-center">
  199. <div class="col mr-2">
  200. <div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
  201. Total Size
  202. </div>
  203. <div class="h5 mb-0 font-weight-bold text-gray-800">
  204. {formatBytes(data.TotalSize)}
  205. </div>
  206. </div>
  207. <div class="col-auto">
  208. <i class="fas fa-chart-area fa-2x text-gray-300"></i>
  209. </div>
  210. </div>
  211. </div>
  212. </div>
  213. </div>
  214. </div>
  215. <!-- Volumes Table -->
  216. <div class="card shadow mb-4">
  217. <div class="card-header py-3">
  218. <h6 class="m-0 font-weight-bold text-primary">
  219. <i class="fas fa-database me-2"></i>Volume Details
  220. </h6>
  221. </div>
  222. <div class="card-body">
  223. if len(data.Volumes) > 0 {
  224. <div class="table-responsive">
  225. <table class="table table-hover" id="volumesTable">
  226. <thead>
  227. <tr>
  228. <th>
  229. <a href="#" onclick="sortTable('id')" class="text-decoration-none text-dark">
  230. Volume ID
  231. @getSortIcon("id", data.SortBy, data.SortOrder)
  232. </a>
  233. </th>
  234. <th>
  235. <a href="#" onclick="sortTable('server')" class="text-decoration-none text-dark">
  236. Server
  237. @getSortIcon("server", data.SortBy, data.SortOrder)
  238. </a>
  239. </th>
  240. if data.ShowDataCenterColumn {
  241. <th>
  242. <a href="#" onclick="sortTable('datacenter')" class="text-decoration-none text-dark">
  243. Data Center
  244. @getSortIcon("datacenter", data.SortBy, data.SortOrder)
  245. </a>
  246. </th>
  247. }
  248. if data.ShowRackColumn {
  249. <th>
  250. <a href="#" onclick="sortTable('rack')" class="text-decoration-none text-dark">
  251. Rack
  252. @getSortIcon("rack", data.SortBy, data.SortOrder)
  253. </a>
  254. </th>
  255. }
  256. if data.ShowCollectionColumn {
  257. <th>
  258. <a href="#" onclick="sortTable('collection')" class="text-decoration-none text-dark">
  259. Collection
  260. @getSortIcon("collection", data.SortBy, data.SortOrder)
  261. </a>
  262. </th>
  263. }
  264. <th>
  265. <a href="#" onclick="sortTable('size')" class="text-decoration-none text-dark">
  266. Size
  267. @getSortIcon("size", data.SortBy, data.SortOrder)
  268. </a>
  269. </th>
  270. <th>Volume Utilization</th>
  271. <th>
  272. <a href="#" onclick="sortTable('filecount')" class="text-decoration-none text-dark">
  273. File Count
  274. @getSortIcon("filecount", data.SortBy, data.SortOrder)
  275. </a>
  276. </th>
  277. <th>
  278. <a href="#" onclick="sortTable('replication')" class="text-decoration-none text-dark">
  279. Replication
  280. @getSortIcon("replication", data.SortBy, data.SortOrder)
  281. </a>
  282. </th>
  283. if data.ShowDiskTypeColumn {
  284. <th>
  285. <a href="#" onclick="sortTable('disktype')" class="text-decoration-none text-dark">
  286. Disk Type
  287. @getSortIcon("disktype", data.SortBy, data.SortOrder)
  288. </a>
  289. </th>
  290. }
  291. if data.ShowVersionColumn {
  292. <th>
  293. <a href="#" onclick="sortTable('version')" class="text-decoration-none text-dark">
  294. Version
  295. @getSortIcon("version", data.SortBy, data.SortOrder)
  296. </a>
  297. </th>
  298. }
  299. <th>Actions</th>
  300. </tr>
  301. </thead>
  302. <tbody>
  303. for _, volume := range data.Volumes {
  304. <tr>
  305. <td>
  306. <code class="volume-id-link" style="cursor: pointer; text-decoration: underline; color: #0d6efd;"
  307. data-volume-id={fmt.Sprintf("%d", volume.Id)}
  308. title="Click to view volume details">
  309. {fmt.Sprintf("%d", volume.Id)}
  310. </code>
  311. </td>
  312. <td>
  313. <a href={templ.SafeURL(fmt.Sprintf("http://%s/ui/index.html", volume.Server))} target="_blank" class="text-decoration-none">
  314. {volume.Server}
  315. <i class="fas fa-external-link-alt ms-1 text-muted"></i>
  316. </a>
  317. </td>
  318. if data.ShowDataCenterColumn {
  319. <td>
  320. <span class="badge bg-light text-dark">{volume.DataCenter}</span>
  321. </td>
  322. }
  323. if data.ShowRackColumn {
  324. <td>
  325. <span class="badge bg-light text-dark">{volume.Rack}</span>
  326. </td>
  327. }
  328. if data.ShowCollectionColumn {
  329. <td>
  330. if volume.Collection == "" {
  331. <a href={templ.SafeURL("/cluster/volumes?collection=default")} class="text-decoration-none">
  332. <span class="badge bg-secondary">default</span>
  333. </a>
  334. } else {
  335. <a href={templ.SafeURL(fmt.Sprintf("/cluster/volumes?collection=%s", volume.Collection))} class="text-decoration-none">
  336. <span class="badge bg-secondary">{volume.Collection}</span>
  337. </a>
  338. }
  339. </td>
  340. }
  341. <td>{formatBytes(int64(volume.Size))}</td>
  342. <td>
  343. <div class="d-flex align-items-center">
  344. <div class="progress me-2" style="width: 80px; height: 16px; background-color: #e9ecef;">
  345. <!-- Active data (green) -->
  346. <div class="progress-bar bg-success" role="progressbar"
  347. style={fmt.Sprintf("width: %.1f%%",
  348. func() float64 {
  349. if volume.Size > 0 {
  350. activePct := float64(volume.Size - volume.DeletedByteCount) / float64(volume.Size) * 100
  351. if data.VolumeSizeLimit > 0 {
  352. return activePct * float64(volume.Size) / float64(data.VolumeSizeLimit) * 100
  353. }
  354. return activePct
  355. }
  356. return 0
  357. }())}
  358. title={fmt.Sprintf("Active: %s", formatBytes(int64(volume.Size - volume.DeletedByteCount)))}>
  359. </div>
  360. <!-- Garbage data (red) -->
  361. <div class="progress-bar bg-danger" role="progressbar"
  362. style={fmt.Sprintf("width: %.1f%%",
  363. func() float64 {
  364. if volume.Size > 0 && volume.DeletedByteCount > 0 {
  365. garbagePct := float64(volume.DeletedByteCount) / float64(volume.Size) * 100
  366. if data.VolumeSizeLimit > 0 {
  367. return garbagePct * float64(volume.Size) / float64(data.VolumeSizeLimit) * 100
  368. }
  369. return garbagePct
  370. }
  371. return 0
  372. }())}
  373. title={fmt.Sprintf("Garbage: %s", formatBytes(int64(volume.DeletedByteCount)))}>
  374. </div>
  375. </div>
  376. <small class="text-muted">
  377. {func() string {
  378. if data.VolumeSizeLimit > 0 {
  379. return fmt.Sprintf("%.0f%%", float64(volume.Size)/float64(data.VolumeSizeLimit)*100)
  380. }
  381. return "N/A"
  382. }()}
  383. </small>
  384. </div>
  385. </td>
  386. <td>{fmt.Sprintf("%d", volume.FileCount)}</td>
  387. <td>
  388. <span class="badge bg-info">{fmt.Sprintf("%03d", volume.ReplicaPlacement)}</span>
  389. </td>
  390. if data.ShowDiskTypeColumn {
  391. <td>
  392. <span class="badge bg-primary">{volume.DiskType}</span>
  393. </td>
  394. }
  395. if data.ShowVersionColumn {
  396. <td>
  397. <span class="badge bg-dark">{fmt.Sprintf("v%d", volume.Version)}</span>
  398. </td>
  399. }
  400. <td>
  401. <div class="btn-group btn-group-sm">
  402. <button type="button" class="btn btn-outline-primary btn-sm view-details-btn"
  403. title="View Details" data-volume-id={fmt.Sprintf("%d", volume.Id)}>
  404. <i class="fas fa-eye"></i>
  405. </button>
  406. <button type="button" class="btn btn-outline-secondary btn-sm vacuum-btn"
  407. title="Vacuum"
  408. data-volume-id={fmt.Sprintf("%d", volume.Id)}
  409. data-server={volume.Server}>
  410. <i class="fas fa-compress-alt"></i>
  411. </button>
  412. </div>
  413. </td>
  414. </tr>
  415. }
  416. </tbody>
  417. </table>
  418. </div>
  419. <!-- Volume Summary -->
  420. <div class="d-flex justify-content-between align-items-center mt-3">
  421. <div>
  422. <small class="text-muted">
  423. 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
  424. </small>
  425. </div>
  426. if data.TotalPages > 1 {
  427. <div>
  428. <small class="text-muted">
  429. Page {fmt.Sprintf("%d", data.CurrentPage)} of {fmt.Sprintf("%d", data.TotalPages)}
  430. </small>
  431. </div>
  432. }
  433. </div>
  434. <!-- Pagination Controls -->
  435. if data.TotalPages > 1 {
  436. <div class="d-flex justify-content-center mt-3">
  437. <nav aria-label="Volumes pagination">
  438. <ul class="pagination pagination-sm mb-0">
  439. <!-- Previous Button -->
  440. if data.CurrentPage > 1 {
  441. <li class="page-item">
  442. <a class="page-link pagination-link" href="#" data-page={fmt.Sprintf("%d", data.CurrentPage-1)}>
  443. <i class="fas fa-chevron-left"></i>
  444. </a>
  445. </li>
  446. } else {
  447. <li class="page-item disabled">
  448. <span class="page-link">
  449. <i class="fas fa-chevron-left"></i>
  450. </span>
  451. </li>
  452. }
  453. <!-- Page Numbers -->
  454. for i := maxInt(1, data.CurrentPage-2); i <= minInt(data.TotalPages, data.CurrentPage+2); i++ {
  455. if i == data.CurrentPage {
  456. <li class="page-item active">
  457. <span class="page-link">{fmt.Sprintf("%d", i)}</span>
  458. </li>
  459. } else {
  460. <li class="page-item">
  461. <a class="page-link pagination-link" href="#" data-page={fmt.Sprintf("%d", i)}>{fmt.Sprintf("%d", i)}</a>
  462. </li>
  463. }
  464. }
  465. <!-- Next Button -->
  466. if data.CurrentPage < data.TotalPages {
  467. <li class="page-item">
  468. <a class="page-link pagination-link" href="#" data-page={fmt.Sprintf("%d", data.CurrentPage+1)}>
  469. <i class="fas fa-chevron-right"></i>
  470. </a>
  471. </li>
  472. } else {
  473. <li class="page-item disabled">
  474. <span class="page-link">
  475. <i class="fas fa-chevron-right"></i>
  476. </span>
  477. </li>
  478. }
  479. </ul>
  480. </nav>
  481. </div>
  482. }
  483. } else {
  484. <div class="text-center py-5">
  485. <i class="fas fa-database fa-3x text-muted mb-3"></i>
  486. <h5 class="text-muted">No Volumes Found</h5>
  487. <p class="text-muted">No volumes are currently available in the cluster.</p>
  488. </div>
  489. }
  490. </div>
  491. </div>
  492. <!-- Last Updated -->
  493. <div class="row">
  494. <div class="col-12">
  495. <small class="text-muted">
  496. <i class="fas fa-clock me-1"></i>
  497. Last updated: {data.LastUpdated.Format("2006-01-02 15:04:05")}
  498. </small>
  499. </div>
  500. </div>
  501. </div>
  502. <!-- JavaScript for pagination and sorting -->
  503. <script>
  504. // Initialize pagination links when page loads
  505. document.addEventListener('DOMContentLoaded', function() {
  506. // Add click handlers to pagination links
  507. document.querySelectorAll('.pagination-link').forEach(link => {
  508. link.addEventListener('click', function(e) {
  509. e.preventDefault();
  510. const page = this.getAttribute('data-page');
  511. goToPage(page);
  512. });
  513. });
  514. // Add click handlers to view details buttons
  515. document.querySelectorAll('.view-details-btn').forEach(button => {
  516. button.addEventListener('click', function(e) {
  517. e.preventDefault();
  518. const volumeId = this.getAttribute('data-volume-id');
  519. viewVolumeDetails(volumeId);
  520. });
  521. });
  522. // Add click handlers to volume ID links
  523. document.querySelectorAll('.volume-id-link').forEach(link => {
  524. link.addEventListener('click', function(e) {
  525. e.preventDefault();
  526. const volumeId = this.getAttribute('data-volume-id');
  527. viewVolumeDetails(volumeId);
  528. });
  529. });
  530. // Add click handlers to vacuum buttons
  531. document.querySelectorAll('.vacuum-btn').forEach(button => {
  532. button.addEventListener('click', function(e) {
  533. e.preventDefault();
  534. const volumeId = this.getAttribute('data-volume-id');
  535. const server = this.getAttribute('data-server');
  536. performVacuum(volumeId, server, this);
  537. });
  538. });
  539. });
  540. function goToPage(page) {
  541. const url = new URL(window.location);
  542. url.searchParams.set('page', page);
  543. window.location.href = url.toString();
  544. }
  545. function changePageSize() {
  546. const pageSize = document.getElementById('pageSizeSelect').value;
  547. const url = new URL(window.location);
  548. url.searchParams.set('pageSize', pageSize);
  549. url.searchParams.set('page', '1'); // Reset to first page
  550. window.location.href = url.toString();
  551. }
  552. function sortTable(column) {
  553. const url = new URL(window.location);
  554. const currentSort = url.searchParams.get('sortBy');
  555. const currentOrder = url.searchParams.get('sortOrder') || 'asc';
  556. let newOrder = 'asc';
  557. if (currentSort === column && currentOrder === 'asc') {
  558. newOrder = 'desc';
  559. }
  560. url.searchParams.set('sortBy', column);
  561. url.searchParams.set('sortOrder', newOrder);
  562. url.searchParams.set('page', '1'); // Reset to first page
  563. window.location.href = url.toString();
  564. }
  565. function exportVolumes() {
  566. // TODO: Implement volume export functionality
  567. alert('Export functionality to be implemented');
  568. }
  569. function viewVolumeDetails(volumeId) {
  570. // Get the server from the current row - works for both buttons and volume ID links
  571. const clickedElement = event.target;
  572. const row = clickedElement.closest('tr');
  573. const serverCell = row.querySelector('td:nth-child(2) a');
  574. const server = serverCell ? serverCell.textContent.trim() : 'unknown';
  575. window.location.href = `/cluster/volumes/${volumeId}/${encodeURIComponent(server)}`;
  576. }
  577. function performVacuum(volumeId, server, button) {
  578. // Disable button and show loading state
  579. const originalHTML = button.innerHTML;
  580. button.disabled = true;
  581. button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
  582. // Send vacuum request
  583. fetch(`/api/volumes/${volumeId}/${encodeURIComponent(server)}/vacuum`, {
  584. method: 'POST',
  585. headers: {
  586. 'Content-Type': 'application/json',
  587. }
  588. })
  589. .then(response => response.json())
  590. .then(data => {
  591. if (data.error) {
  592. showMessage(data.error, 'error');
  593. } else {
  594. showMessage(data.message || 'Volume vacuum started successfully', 'success');
  595. // Optionally refresh the page after a delay to show updated vacuum status
  596. setTimeout(() => {
  597. window.location.reload();
  598. }, 2000);
  599. }
  600. })
  601. .catch(error => {
  602. console.error('Error:', error);
  603. showMessage('Failed to start vacuum operation', 'error');
  604. })
  605. .finally(() => {
  606. // Re-enable button
  607. button.disabled = false;
  608. button.innerHTML = originalHTML;
  609. });
  610. }
  611. function showMessage(message, type) {
  612. // Create toast notification
  613. const toast = document.createElement('div');
  614. toast.className = `alert alert-${type === 'error' ? 'danger' : 'success'} alert-dismissible fade show position-fixed`;
  615. toast.style.top = '20px';
  616. toast.style.right = '20px';
  617. toast.style.zIndex = '9999';
  618. toast.style.minWidth = '300px';
  619. toast.innerHTML = `
  620. ${message}
  621. <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
  622. `;
  623. document.body.appendChild(toast);
  624. // Auto-remove after 5 seconds
  625. setTimeout(() => {
  626. if (toast.parentNode) {
  627. toast.parentNode.removeChild(toast);
  628. }
  629. }, 5000);
  630. }
  631. </script>
  632. }
  633. func countActiveVolumes(volumes []dash.VolumeWithTopology) int {
  634. // Since we removed status tracking, consider all volumes as active
  635. return len(volumes)
  636. }
  637. func countUniqueDataCenters(volumes []dash.VolumeWithTopology) int {
  638. dcMap := make(map[string]bool)
  639. for _, volume := range volumes {
  640. dcMap[volume.DataCenter] = true
  641. }
  642. return len(dcMap)
  643. }
  644. func countUniqueRacks(volumes []dash.VolumeWithTopology) int {
  645. rackMap := make(map[string]bool)
  646. for _, volume := range volumes {
  647. if volume.Rack != "" {
  648. rackMap[volume.Rack] = true
  649. }
  650. }
  651. return len(rackMap)
  652. }
  653. func countUniqueDiskTypes(volumes []dash.VolumeWithTopology) int {
  654. diskTypeMap := make(map[string]bool)
  655. for _, volume := range volumes {
  656. diskType := volume.DiskType
  657. if diskType == "" {
  658. diskType = "hdd"
  659. }
  660. diskTypeMap[diskType] = true
  661. }
  662. return len(diskTypeMap)
  663. }
  664. templ getSortIcon(column, currentSort, currentOrder string) {
  665. if column != currentSort {
  666. <i class="fas fa-sort text-muted ms-1"></i>
  667. } else if currentOrder == "asc" {
  668. <i class="fas fa-sort-up text-primary ms-1"></i>
  669. } else {
  670. <i class="fas fa-sort-down text-primary ms-1"></i>
  671. }
  672. }
  673. func minInt(a, b int) int {
  674. if a < b {
  675. return a
  676. }
  677. return b
  678. }
  679. func maxInt(a, b int) int {
  680. if a > b {
  681. return a
  682. }
  683. return b
  684. }