collection_details.templ 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. package app
  2. import (
  3. "fmt"
  4. "github.com/seaweedfs/seaweedfs/weed/admin/dash"
  5. "github.com/seaweedfs/seaweedfs/weed/util"
  6. )
  7. templ CollectionDetails(data dash.CollectionDetailsData) {
  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-layer-group me-2"></i>Collection Details: {data.CollectionName}
  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/collections" class="text-decoration-none">Collections</a></li>
  17. <li class="breadcrumb-item active" aria-current="page">{data.CollectionName}</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. <!-- Collection Summary -->
  33. <div class="row mb-4">
  34. <div class="col-md-3">
  35. <div class="card text-bg-primary">
  36. <div class="card-body">
  37. <div class="d-flex justify-content-between">
  38. <div>
  39. <h6 class="card-title">Regular Volumes</h6>
  40. <h4 class="mb-0">{fmt.Sprintf("%d", data.TotalVolumes)}</h4>
  41. <small>Traditional volumes</small>
  42. </div>
  43. <div class="align-self-center">
  44. <i class="fas fa-database fa-2x"></i>
  45. </div>
  46. </div>
  47. </div>
  48. </div>
  49. </div>
  50. <div class="col-md-3">
  51. <div class="card text-bg-info">
  52. <div class="card-body">
  53. <div class="d-flex justify-content-between">
  54. <div>
  55. <h6 class="card-title">EC Volumes</h6>
  56. <h4 class="mb-0">{fmt.Sprintf("%d", data.TotalEcVolumes)}</h4>
  57. <small>Erasure coded volumes</small>
  58. </div>
  59. <div class="align-self-center">
  60. <i class="fas fa-th-large fa-2x"></i>
  61. </div>
  62. </div>
  63. </div>
  64. </div>
  65. </div>
  66. <div class="col-md-3">
  67. <div class="card text-bg-success">
  68. <div class="card-body">
  69. <div class="d-flex justify-content-between">
  70. <div>
  71. <h6 class="card-title">Total Files</h6>
  72. <h4 class="mb-0">{fmt.Sprintf("%d", data.TotalFiles)}</h4>
  73. <small>Files stored</small>
  74. </div>
  75. <div class="align-self-center">
  76. <i class="fas fa-file fa-2x"></i>
  77. </div>
  78. </div>
  79. </div>
  80. </div>
  81. </div>
  82. <div class="col-md-3">
  83. <div class="card text-bg-warning">
  84. <div class="card-body">
  85. <div class="d-flex justify-content-between">
  86. <div>
  87. <h6 class="card-title">Total Size (Logical)</h6>
  88. <h4 class="mb-0">{util.BytesToHumanReadable(uint64(data.TotalSize))}</h4>
  89. <small>Data stored (regular volumes only)</small>
  90. </div>
  91. <div class="align-self-center">
  92. <i class="fas fa-hdd fa-2x"></i>
  93. </div>
  94. </div>
  95. </div>
  96. </div>
  97. </div>
  98. </div>
  99. <!-- Size Information Note -->
  100. <div class="alert alert-info" role="alert">
  101. <i class="fas fa-info-circle me-2"></i>
  102. <strong>Size Information:</strong>
  103. Logical size represents the actual data stored (regular volumes only).
  104. EC volumes show shard counts instead of size - physical storage for EC volumes is approximately 1.4x the original data due to erasure coding redundancy.
  105. </div>
  106. <!-- Pagination Info -->
  107. <div class="d-flex justify-content-between align-items-center mb-3">
  108. <div class="d-flex align-items-center">
  109. <span class="me-3">
  110. Showing {fmt.Sprintf("%d", (data.Page-1)*data.PageSize + 1)} to {fmt.Sprintf("%d", func() int {
  111. end := data.Page * data.PageSize
  112. totalItems := data.TotalVolumes + data.TotalEcVolumes
  113. if end > totalItems {
  114. return totalItems
  115. }
  116. return end
  117. }())} of {fmt.Sprintf("%d", data.TotalVolumes + data.TotalEcVolumes)} items
  118. </span>
  119. <div class="d-flex align-items-center">
  120. <label for="pageSize" class="form-label me-2 mb-0">Show:</label>
  121. <select id="pageSize" class="form-select form-select-sm" style="width: auto;" onchange="changePageSize(this.value)">
  122. <option value="10" if data.PageSize == 10 { selected }>10</option>
  123. <option value="25" if data.PageSize == 25 { selected }>25</option>
  124. <option value="50" if data.PageSize == 50 { selected }>50</option>
  125. <option value="100" if data.PageSize == 100 { selected }>100</option>
  126. </select>
  127. <span class="ms-2">per page</span>
  128. </div>
  129. </div>
  130. </div>
  131. <!-- Volumes Table -->
  132. <div class="table-responsive">
  133. <table class="table table-striped table-hover" id="volumesTable">
  134. <thead>
  135. <tr>
  136. <th>
  137. <a href="#" onclick="sortBy('volume_id')" class="text-dark text-decoration-none">
  138. Volume ID
  139. if data.SortBy == "volume_id" {
  140. if data.SortOrder == "asc" {
  141. <i class="fas fa-sort-up ms-1"></i>
  142. } else {
  143. <i class="fas fa-sort-down ms-1"></i>
  144. }
  145. } else {
  146. <i class="fas fa-sort ms-1 text-muted"></i>
  147. }
  148. </a>
  149. </th>
  150. <th>
  151. <a href="#" onclick="sortBy('type')" class="text-dark text-decoration-none">
  152. Type
  153. if data.SortBy == "type" {
  154. if data.SortOrder == "asc" {
  155. <i class="fas fa-sort-up ms-1"></i>
  156. } else {
  157. <i class="fas fa-sort-down ms-1"></i>
  158. }
  159. } else {
  160. <i class="fas fa-sort ms-1 text-muted"></i>
  161. }
  162. </a>
  163. </th>
  164. <th class="text-dark">Logical Size / Shard Count</th>
  165. <th class="text-dark">Files</th>
  166. <th class="text-dark">Status</th>
  167. <th class="text-dark">Actions</th>
  168. </tr>
  169. </thead>
  170. <tbody>
  171. // Display regular volumes
  172. for _, volume := range data.RegularVolumes {
  173. <tr>
  174. <td>
  175. <strong>{fmt.Sprintf("%d", volume.Id)}</strong>
  176. </td>
  177. <td>
  178. <span class="badge bg-primary">
  179. <i class="fas fa-database me-1"></i>Regular
  180. </span>
  181. </td>
  182. <td>
  183. {util.BytesToHumanReadable(volume.Size)}
  184. </td>
  185. <td>
  186. {fmt.Sprintf("%d", volume.FileCount)}
  187. </td>
  188. <td>
  189. if volume.ReadOnly {
  190. <span class="badge bg-warning">Read Only</span>
  191. } else {
  192. <span class="badge bg-success">Read/Write</span>
  193. }
  194. </td>
  195. <td>
  196. <div class="btn-group" role="group">
  197. <button type="button" class="btn btn-sm btn-outline-primary"
  198. onclick="showVolumeDetails(event)"
  199. data-volume-id={ fmt.Sprintf("%d", volume.Id) }
  200. data-server={ volume.Server }
  201. title="View volume details">
  202. <i class="fas fa-info-circle"></i>
  203. </button>
  204. </div>
  205. </td>
  206. </tr>
  207. }
  208. // Display EC volumes
  209. for _, ecVolume := range data.EcVolumes {
  210. <tr>
  211. <td>
  212. <strong>{fmt.Sprintf("%d", ecVolume.VolumeID)}</strong>
  213. </td>
  214. <td>
  215. <span class="badge bg-info">
  216. <i class="fas fa-th-large me-1"></i>EC
  217. </span>
  218. </td>
  219. <td>
  220. <span class="badge bg-primary">{fmt.Sprintf("%d/14", ecVolume.TotalShards)}</span>
  221. </td>
  222. <td>
  223. <span class="text-muted">-</span>
  224. </td>
  225. <td>
  226. if ecVolume.IsComplete {
  227. <span class="badge bg-success">
  228. <i class="fas fa-check me-1"></i>Complete
  229. </span>
  230. } else {
  231. <span class="badge bg-warning">
  232. <i class="fas fa-exclamation-triangle me-1"></i>
  233. Missing {fmt.Sprintf("%d", len(ecVolume.MissingShards))} shards
  234. </span>
  235. }
  236. </td>
  237. <td>
  238. <div class="btn-group" role="group">
  239. <button type="button" class="btn btn-sm btn-outline-info"
  240. onclick="showEcVolumeDetails(event)"
  241. data-volume-id={ fmt.Sprintf("%d", ecVolume.VolumeID) }
  242. title="View EC volume details">
  243. <i class="fas fa-info-circle"></i>
  244. </button>
  245. if !ecVolume.IsComplete {
  246. <button type="button" class="btn btn-sm btn-outline-warning"
  247. onclick="repairEcVolume(event)"
  248. data-volume-id={ fmt.Sprintf("%d", ecVolume.VolumeID) }
  249. title="Repair missing shards">
  250. <i class="fas fa-wrench"></i>
  251. </button>
  252. }
  253. </div>
  254. </td>
  255. </tr>
  256. }
  257. // Show message when no volumes found
  258. if len(data.RegularVolumes) == 0 && len(data.EcVolumes) == 0 {
  259. <tr>
  260. <td colspan="6" class="text-center text-muted py-4">
  261. <i class="fas fa-info-circle me-2"></i>
  262. No volumes found for collection "{data.CollectionName}"
  263. </td>
  264. </tr>
  265. }
  266. </tbody>
  267. </table>
  268. </div>
  269. <!-- Pagination -->
  270. if data.TotalPages > 1 {
  271. <nav aria-label="Collection volumes pagination">
  272. <ul class="pagination justify-content-center">
  273. if data.Page > 1 {
  274. <li class="page-item">
  275. <a class="page-link" href="#" onclick="goToPage(event)" data-page="1">First</a>
  276. </li>
  277. <li class="page-item">
  278. <a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", data.Page-1) }>Previous</a>
  279. </li>
  280. }
  281. for i := 1; i <= data.TotalPages; i++ {
  282. if i == data.Page {
  283. <li class="page-item active">
  284. <span class="page-link">{fmt.Sprintf("%d", i)}</span>
  285. </li>
  286. } else if i <= 3 || i > data.TotalPages-3 || (i >= data.Page-2 && i <= data.Page+2) {
  287. <li class="page-item">
  288. <a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", i) }>{fmt.Sprintf("%d", i)}</a>
  289. </li>
  290. } else if i == 4 && data.Page > 6 {
  291. <li class="page-item disabled">
  292. <span class="page-link">...</span>
  293. </li>
  294. } else if i == data.TotalPages-3 && data.Page < data.TotalPages-5 {
  295. <li class="page-item disabled">
  296. <span class="page-link">...</span>
  297. </li>
  298. }
  299. }
  300. if data.Page < data.TotalPages {
  301. <li class="page-item">
  302. <a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", data.Page+1) }>Next</a>
  303. </li>
  304. <li class="page-item">
  305. <a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", data.TotalPages) }>Last</a>
  306. </li>
  307. }
  308. </ul>
  309. </nav>
  310. }
  311. <script>
  312. // Sorting functionality
  313. function sortBy(field) {
  314. const currentSort = new URLSearchParams(window.location.search).get('sort_by');
  315. const currentOrder = new URLSearchParams(window.location.search).get('sort_order') || 'asc';
  316. let newOrder = 'asc';
  317. if (currentSort === field && currentOrder === 'asc') {
  318. newOrder = 'desc';
  319. }
  320. const url = new URL(window.location);
  321. url.searchParams.set('sort_by', field);
  322. url.searchParams.set('sort_order', newOrder);
  323. url.searchParams.set('page', '1'); // Reset to first page
  324. window.location.href = url.toString();
  325. }
  326. // Pagination functionality
  327. function goToPage(event) {
  328. event.preventDefault();
  329. const page = event.target.closest('a').getAttribute('data-page');
  330. const url = new URL(window.location);
  331. url.searchParams.set('page', page);
  332. window.location.href = url.toString();
  333. }
  334. // Page size functionality
  335. function changePageSize(newPageSize) {
  336. const url = new URL(window.location);
  337. url.searchParams.set('page_size', newPageSize);
  338. url.searchParams.set('page', '1'); // Reset to first page when changing page size
  339. window.location.href = url.toString();
  340. }
  341. // Volume details
  342. function showVolumeDetails(event) {
  343. const volumeId = event.target.closest('button').getAttribute('data-volume-id');
  344. const server = event.target.closest('button').getAttribute('data-server');
  345. window.location.href = `/cluster/volumes/${volumeId}/${server}`;
  346. }
  347. // EC Volume details
  348. function showEcVolumeDetails(event) {
  349. const volumeId = event.target.closest('button').getAttribute('data-volume-id');
  350. window.location.href = `/cluster/ec-volumes/${volumeId}`;
  351. }
  352. // Repair EC Volume
  353. function repairEcVolume(event) {
  354. const volumeId = event.target.closest('button').getAttribute('data-volume-id');
  355. if (confirm(`Are you sure you want to repair missing shards for EC volume ${volumeId}?`)) {
  356. // TODO: Implement repair functionality
  357. alert('Repair functionality will be implemented soon.');
  358. }
  359. }
  360. </script>
  361. }