cluster_ec_volumes.templ 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776
  1. package app
  2. import (
  3. "fmt"
  4. "strings"
  5. "github.com/seaweedfs/seaweedfs/weed/admin/dash"
  6. "github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
  7. )
  8. templ ClusterEcVolumes(data dash.ClusterEcVolumesData) {
  9. <!DOCTYPE html>
  10. <html lang="en">
  11. <head>
  12. <title>EC Volumes - SeaweedFS</title>
  13. <meta charset="utf-8">
  14. <meta name="viewport" content="width=device-width, initial-scale=1">
  15. <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
  16. <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
  17. </head>
  18. <body>
  19. <div class="container-fluid">
  20. <div class="row">
  21. <div class="col-12">
  22. <h2 class="mb-4">
  23. <i class="fas fa-database me-2"></i>EC Volumes
  24. <small class="text-muted">({fmt.Sprintf("%d", data.TotalVolumes)} volumes)</small>
  25. </h2>
  26. </div>
  27. </div>
  28. <!-- Statistics Cards -->
  29. <div class="row mb-4">
  30. <div class="col-md-3">
  31. <div class="card text-bg-primary">
  32. <div class="card-body">
  33. <div class="d-flex justify-content-between">
  34. <div>
  35. <h6 class="card-title">Total Volumes</h6>
  36. <h4 class="mb-0">{fmt.Sprintf("%d", data.TotalVolumes)}</h4>
  37. <small>EC encoded volumes</small>
  38. </div>
  39. <div class="align-self-center">
  40. <i class="fas fa-cubes fa-2x"></i>
  41. </div>
  42. </div>
  43. </div>
  44. </div>
  45. </div>
  46. <div class="col-md-3">
  47. <div class="card text-bg-info">
  48. <div class="card-body">
  49. <div class="d-flex justify-content-between">
  50. <div>
  51. <h6 class="card-title">Total Shards</h6>
  52. <h4 class="mb-0">{fmt.Sprintf("%d", data.TotalShards)}</h4>
  53. <small>Distributed shards</small>
  54. </div>
  55. <div class="align-self-center">
  56. <i class="fas fa-puzzle-piece fa-2x"></i>
  57. </div>
  58. </div>
  59. </div>
  60. </div>
  61. </div>
  62. <div class="col-md-3">
  63. <div class="card text-bg-success">
  64. <div class="card-body">
  65. <div class="d-flex justify-content-between">
  66. <div>
  67. <h6 class="card-title">Complete Volumes</h6>
  68. <h4 class="mb-0">{fmt.Sprintf("%d", data.CompleteVolumes)}</h4>
  69. <small>All shards present</small>
  70. </div>
  71. <div class="align-self-center">
  72. <i class="fas fa-check-circle fa-2x"></i>
  73. </div>
  74. </div>
  75. </div>
  76. </div>
  77. </div>
  78. <div class="col-md-3">
  79. <div class="card text-bg-warning">
  80. <div class="card-body">
  81. <div class="d-flex justify-content-between">
  82. <div>
  83. <h6 class="card-title">Incomplete Volumes</h6>
  84. <h4 class="mb-0">{fmt.Sprintf("%d", data.IncompleteVolumes)}</h4>
  85. <small>Missing shards</small>
  86. </div>
  87. <div class="align-self-center">
  88. <i class="fas fa-exclamation-triangle fa-2x"></i>
  89. </div>
  90. </div>
  91. </div>
  92. </div>
  93. </div>
  94. </div>
  95. <!-- EC Storage Information Note -->
  96. <div class="alert alert-info mb-4" role="alert">
  97. <i class="fas fa-info-circle me-2"></i>
  98. <strong>EC Storage Note:</strong>
  99. EC volumes use erasure coding ({ fmt.Sprintf("%d+%d", erasure_coding.DataShardsCount, erasure_coding.ParityShardsCount) }) which stores data across { fmt.Sprintf("%d", erasure_coding.TotalShardsCount) } shards with redundancy.
  100. Physical storage is approximately { fmt.Sprintf("%.1fx", float64(erasure_coding.TotalShardsCount)/float64(erasure_coding.DataShardsCount)) } the original logical data size due to { fmt.Sprintf("%d", erasure_coding.ParityShardsCount) } parity shards.
  101. </div>
  102. <!-- Volumes Table -->
  103. <div class="d-flex justify-content-between align-items-center mb-3">
  104. <div class="d-flex align-items-center">
  105. <span class="me-3">
  106. Showing {fmt.Sprintf("%d", (data.Page-1)*data.PageSize + 1)} to {fmt.Sprintf("%d", func() int {
  107. end := data.Page * data.PageSize
  108. if end > data.TotalVolumes {
  109. return data.TotalVolumes
  110. }
  111. return end
  112. }())} of {fmt.Sprintf("%d", data.TotalVolumes)} volumes
  113. </span>
  114. <div class="d-flex align-items-center">
  115. <label for="pageSize" class="form-label me-2 mb-0">Show:</label>
  116. <select id="pageSize" class="form-select form-select-sm" style="width: auto;" onchange="changePageSize(this.value)">
  117. <option value="5" if data.PageSize == 5 { selected }>5</option>
  118. <option value="10" if data.PageSize == 10 { selected }>10</option>
  119. <option value="25" if data.PageSize == 25 { selected }>25</option>
  120. <option value="50" if data.PageSize == 50 { selected }>50</option>
  121. <option value="100" if data.PageSize == 100 { selected }>100</option>
  122. </select>
  123. <span class="ms-2">per page</span>
  124. </div>
  125. </div>
  126. if data.Collection != "" {
  127. <div>
  128. if data.Collection == "default" {
  129. <span class="badge bg-secondary text-white">Collection: default</span>
  130. } else {
  131. <span class="badge bg-info text-white">Collection: {data.Collection}</span>
  132. }
  133. <a href="/cluster/ec-shards" class="btn btn-sm btn-outline-secondary ms-2">Clear Filter</a>
  134. </div>
  135. }
  136. </div>
  137. <div class="table-responsive">
  138. <table class="table table-striped table-hover" id="ecVolumesTable">
  139. <thead>
  140. <tr>
  141. <th>
  142. <a href="#" onclick="sortBy('volume_id')" class="text-dark text-decoration-none">
  143. Volume ID
  144. if data.SortBy == "volume_id" {
  145. if data.SortOrder == "asc" {
  146. <i class="fas fa-sort-up ms-1"></i>
  147. } else {
  148. <i class="fas fa-sort-down ms-1"></i>
  149. }
  150. } else {
  151. <i class="fas fa-sort ms-1 text-muted"></i>
  152. }
  153. </a>
  154. </th>
  155. if data.ShowCollectionColumn {
  156. <th>
  157. <a href="#" onclick="sortBy('collection')" class="text-dark text-decoration-none">
  158. Collection
  159. if data.SortBy == "collection" {
  160. if data.SortOrder == "asc" {
  161. <i class="fas fa-sort-up ms-1"></i>
  162. } else {
  163. <i class="fas fa-sort-down ms-1"></i>
  164. }
  165. } else {
  166. <i class="fas fa-sort ms-1 text-muted"></i>
  167. }
  168. </a>
  169. </th>
  170. }
  171. <th>
  172. <a href="#" onclick="sortBy('total_shards')" class="text-dark text-decoration-none">
  173. Shard Count
  174. if data.SortBy == "total_shards" {
  175. if data.SortOrder == "asc" {
  176. <i class="fas fa-sort-up ms-1"></i>
  177. } else {
  178. <i class="fas fa-sort-down ms-1"></i>
  179. }
  180. } else {
  181. <i class="fas fa-sort ms-1 text-muted"></i>
  182. }
  183. </a>
  184. </th>
  185. <th class="text-dark">Shard Size</th>
  186. <th class="text-dark">Shard Locations</th>
  187. <th>
  188. <a href="#" onclick="sortBy('completeness')" class="text-dark text-decoration-none">
  189. Status
  190. if data.SortBy == "completeness" {
  191. if data.SortOrder == "asc" {
  192. <i class="fas fa-sort-up ms-1"></i>
  193. } else {
  194. <i class="fas fa-sort-down ms-1"></i>
  195. }
  196. } else {
  197. <i class="fas fa-sort ms-1 text-muted"></i>
  198. }
  199. </a>
  200. </th>
  201. if data.ShowDataCenterColumn {
  202. <th class="text-dark">Data Centers</th>
  203. }
  204. <th class="text-dark">Actions</th>
  205. </tr>
  206. </thead>
  207. <tbody>
  208. for _, volume := range data.EcVolumes {
  209. <tr>
  210. <td>
  211. <strong>{fmt.Sprintf("%d", volume.VolumeID)}</strong>
  212. </td>
  213. if data.ShowCollectionColumn {
  214. <td>
  215. if volume.Collection != "" {
  216. <a href="/cluster/ec-shards?collection={volume.Collection}" class="text-decoration-none">
  217. <span class="badge bg-info text-white">{volume.Collection}</span>
  218. </a>
  219. } else {
  220. <a href="/cluster/ec-shards?collection=default" class="text-decoration-none">
  221. <span class="badge bg-secondary text-white">default</span>
  222. </a>
  223. }
  224. </td>
  225. }
  226. <td>
  227. <span class="badge bg-primary">{fmt.Sprintf("%d/14", volume.TotalShards)}</span>
  228. </td>
  229. <td>
  230. @displayShardSizes(volume.ShardSizes)
  231. </td>
  232. <td>
  233. @displayVolumeDistribution(volume)
  234. </td>
  235. <td>
  236. @displayEcVolumeStatus(volume)
  237. </td>
  238. if data.ShowDataCenterColumn {
  239. <td>
  240. for i, dc := range volume.DataCenters {
  241. if i > 0 {
  242. <span>, </span>
  243. }
  244. <span class="badge bg-primary text-white">{dc}</span>
  245. }
  246. </td>
  247. }
  248. <td>
  249. <div class="btn-group" role="group">
  250. <button type="button" class="btn btn-sm btn-outline-primary"
  251. onclick="showVolumeDetails(event)"
  252. data-volume-id={ fmt.Sprintf("%d", volume.VolumeID) }
  253. title="View EC volume details">
  254. <i class="fas fa-info-circle"></i>
  255. </button>
  256. if !volume.IsComplete {
  257. <button type="button" class="btn btn-sm btn-outline-warning"
  258. onclick="repairVolume(event)"
  259. data-volume-id={ fmt.Sprintf("%d", volume.VolumeID) }
  260. title="Repair missing shards">
  261. <i class="fas fa-wrench"></i>
  262. </button>
  263. }
  264. </div>
  265. </td>
  266. </tr>
  267. }
  268. </tbody>
  269. </table>
  270. </div>
  271. <!-- Pagination -->
  272. if data.TotalPages > 1 {
  273. <nav aria-label="EC Volumes pagination">
  274. <ul class="pagination justify-content-center">
  275. if data.Page > 1 {
  276. <li class="page-item">
  277. <a class="page-link" href="#" onclick="goToPage(event)" data-page="1">First</a>
  278. </li>
  279. <li class="page-item">
  280. <a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", data.Page-1) }>Previous</a>
  281. </li>
  282. }
  283. for i := 1; i <= data.TotalPages; i++ {
  284. if i == data.Page {
  285. <li class="page-item active">
  286. <span class="page-link">{fmt.Sprintf("%d", i)}</span>
  287. </li>
  288. } else if i <= 3 || i > data.TotalPages-3 || (i >= data.Page-2 && i <= data.Page+2) {
  289. <li class="page-item">
  290. <a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", i) }>{fmt.Sprintf("%d", i)}</a>
  291. </li>
  292. } else if i == 4 && data.Page > 6 {
  293. <li class="page-item disabled">
  294. <span class="page-link">...</span>
  295. </li>
  296. } else if i == data.TotalPages-3 && data.Page < data.TotalPages-5 {
  297. <li class="page-item disabled">
  298. <span class="page-link">...</span>
  299. </li>
  300. }
  301. }
  302. if data.Page < data.TotalPages {
  303. <li class="page-item">
  304. <a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", data.Page+1) }>Next</a>
  305. </li>
  306. <li class="page-item">
  307. <a class="page-link" href="#" onclick="goToPage(event)" data-page={ fmt.Sprintf("%d", data.TotalPages) }>Last</a>
  308. </li>
  309. }
  310. </ul>
  311. </nav>
  312. }
  313. </div>
  314. <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
  315. <script>
  316. // Sorting functionality
  317. function sortBy(field) {
  318. const currentSort = new URLSearchParams(window.location.search).get('sort_by');
  319. const currentOrder = new URLSearchParams(window.location.search).get('sort_order') || 'asc';
  320. let newOrder = 'asc';
  321. if (currentSort === field && currentOrder === 'asc') {
  322. newOrder = 'desc';
  323. }
  324. const url = new URL(window.location);
  325. url.searchParams.set('sort_by', field);
  326. url.searchParams.set('sort_order', newOrder);
  327. url.searchParams.set('page', '1'); // Reset to first page
  328. window.location.href = url.toString();
  329. }
  330. // Pagination functionality
  331. function goToPage(event) {
  332. event.preventDefault();
  333. const page = event.target.closest('a').getAttribute('data-page');
  334. const url = new URL(window.location);
  335. url.searchParams.set('page', page);
  336. window.location.href = url.toString();
  337. }
  338. // Page size functionality
  339. function changePageSize(newPageSize) {
  340. const url = new URL(window.location);
  341. url.searchParams.set('page_size', newPageSize);
  342. url.searchParams.set('page', '1'); // Reset to first page when changing page size
  343. window.location.href = url.toString();
  344. }
  345. // Volume details
  346. function showVolumeDetails(event) {
  347. const volumeId = event.target.closest('button').getAttribute('data-volume-id');
  348. window.location.href = `/cluster/ec-volumes/${volumeId}`;
  349. }
  350. // Repair volume
  351. function repairVolume(event) {
  352. const volumeId = event.target.closest('button').getAttribute('data-volume-id');
  353. if (confirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`)) {
  354. // TODO: Implement repair functionality
  355. alert('Repair functionality will be implemented soon.');
  356. }
  357. }
  358. </script>
  359. </body>
  360. </html>
  361. }
  362. // displayShardLocationsHTML renders shard locations as proper HTML
  363. templ displayShardLocationsHTML(shardLocations map[int]string) {
  364. if len(shardLocations) == 0 {
  365. <span class="text-muted">No shards</span>
  366. } else {
  367. for i, serverInfo := range groupShardsByServer(shardLocations) {
  368. if i > 0 {
  369. <br/>
  370. }
  371. <strong>
  372. <a href={ templ.URL("/cluster/volume-servers/" + serverInfo.Server) } class="text-primary text-decoration-none">
  373. { serverInfo.Server }
  374. </a>:
  375. </strong> { serverInfo.ShardRanges }
  376. }
  377. }
  378. }
  379. // displayShardSizes renders shard sizes in a compact format
  380. templ displayShardSizes(shardSizes map[int]int64) {
  381. if len(shardSizes) == 0 {
  382. <span class="text-muted">-</span>
  383. } else {
  384. @renderShardSizesContent(shardSizes)
  385. }
  386. }
  387. // renderShardSizesContent renders the content of shard sizes
  388. templ renderShardSizesContent(shardSizes map[int]int64) {
  389. if areAllShardSizesSame(shardSizes) {
  390. // All shards have the same size, show just the common size
  391. <span class="text-success">{getCommonShardSize(shardSizes)}</span>
  392. } else {
  393. // Shards have different sizes, show individual sizes
  394. <div class="shard-sizes" style="max-width: 300px;">
  395. { formatIndividualShardSizes(shardSizes) }
  396. </div>
  397. }
  398. }
  399. // ServerShardInfo represents server and its shard ranges with sizes
  400. type ServerShardInfo struct {
  401. Server string
  402. ShardRanges string
  403. }
  404. // groupShardsByServer groups shards by server and formats ranges
  405. func groupShardsByServer(shardLocations map[int]string) []ServerShardInfo {
  406. if len(shardLocations) == 0 {
  407. return []ServerShardInfo{}
  408. }
  409. // Group shards by server
  410. serverShards := make(map[string][]int)
  411. for shardId, server := range shardLocations {
  412. serverShards[server] = append(serverShards[server], shardId)
  413. }
  414. var serverInfos []ServerShardInfo
  415. for server, shards := range serverShards {
  416. // Sort shards for each server
  417. for i := 0; i < len(shards); i++ {
  418. for j := i + 1; j < len(shards); j++ {
  419. if shards[i] > shards[j] {
  420. shards[i], shards[j] = shards[j], shards[i]
  421. }
  422. }
  423. }
  424. // Format shard ranges compactly
  425. shardRanges := formatShardRanges(shards)
  426. serverInfos = append(serverInfos, ServerShardInfo{
  427. Server: server,
  428. ShardRanges: shardRanges,
  429. })
  430. }
  431. // Sort by server name
  432. for i := 0; i < len(serverInfos); i++ {
  433. for j := i + 1; j < len(serverInfos); j++ {
  434. if serverInfos[i].Server > serverInfos[j].Server {
  435. serverInfos[i], serverInfos[j] = serverInfos[j], serverInfos[i]
  436. }
  437. }
  438. }
  439. return serverInfos
  440. }
  441. // groupShardsByServerWithSizes groups shards by server and formats ranges with sizes
  442. func groupShardsByServerWithSizes(shardLocations map[int]string, shardSizes map[int]int64) []ServerShardInfo {
  443. if len(shardLocations) == 0 {
  444. return []ServerShardInfo{}
  445. }
  446. // Group shards by server
  447. serverShards := make(map[string][]int)
  448. for shardId, server := range shardLocations {
  449. serverShards[server] = append(serverShards[server], shardId)
  450. }
  451. var serverInfos []ServerShardInfo
  452. for server, shards := range serverShards {
  453. // Sort shards for each server
  454. for i := 0; i < len(shards); i++ {
  455. for j := i + 1; j < len(shards); j++ {
  456. if shards[i] > shards[j] {
  457. shards[i], shards[j] = shards[j], shards[i]
  458. }
  459. }
  460. }
  461. // Format shard ranges compactly with sizes
  462. shardRanges := formatShardRangesWithSizes(shards, shardSizes)
  463. serverInfos = append(serverInfos, ServerShardInfo{
  464. Server: server,
  465. ShardRanges: shardRanges,
  466. })
  467. }
  468. // Sort by server name
  469. for i := 0; i < len(serverInfos); i++ {
  470. for j := i + 1; j < len(serverInfos); j++ {
  471. if serverInfos[i].Server > serverInfos[j].Server {
  472. serverInfos[i], serverInfos[j] = serverInfos[j], serverInfos[i]
  473. }
  474. }
  475. }
  476. return serverInfos
  477. }
  478. // Helper function to format shard ranges compactly (e.g., "0-3,7,9-11")
  479. func formatShardRanges(shards []int) string {
  480. if len(shards) == 0 {
  481. return ""
  482. }
  483. var ranges []string
  484. start := shards[0]
  485. end := shards[0]
  486. for i := 1; i < len(shards); i++ {
  487. if shards[i] == end+1 {
  488. end = shards[i]
  489. } else {
  490. if start == end {
  491. ranges = append(ranges, fmt.Sprintf("%d", start))
  492. } else {
  493. ranges = append(ranges, fmt.Sprintf("%d-%d", start, end))
  494. }
  495. start = shards[i]
  496. end = shards[i]
  497. }
  498. }
  499. // Add the last range
  500. if start == end {
  501. ranges = append(ranges, fmt.Sprintf("%d", start))
  502. } else {
  503. ranges = append(ranges, fmt.Sprintf("%d-%d", start, end))
  504. }
  505. return strings.Join(ranges, ",")
  506. }
  507. // Helper function to format shard ranges with sizes (e.g., "0(1.2MB),1-3(2.5MB),7(800KB)")
  508. func formatShardRangesWithSizes(shards []int, shardSizes map[int]int64) string {
  509. if len(shards) == 0 {
  510. return ""
  511. }
  512. var ranges []string
  513. start := shards[0]
  514. end := shards[0]
  515. var totalSize int64
  516. for i := 1; i < len(shards); i++ {
  517. if shards[i] == end+1 {
  518. end = shards[i]
  519. totalSize += shardSizes[shards[i]]
  520. } else {
  521. // Add current range with size
  522. if start == end {
  523. size := shardSizes[start]
  524. if size > 0 {
  525. ranges = append(ranges, fmt.Sprintf("%d(%s)", start, bytesToHumanReadable(size)))
  526. } else {
  527. ranges = append(ranges, fmt.Sprintf("%d", start))
  528. }
  529. } else {
  530. // Calculate total size for the range
  531. rangeSize := shardSizes[start]
  532. for j := start + 1; j <= end; j++ {
  533. rangeSize += shardSizes[j]
  534. }
  535. if rangeSize > 0 {
  536. ranges = append(ranges, fmt.Sprintf("%d-%d(%s)", start, end, bytesToHumanReadable(rangeSize)))
  537. } else {
  538. ranges = append(ranges, fmt.Sprintf("%d-%d", start, end))
  539. }
  540. }
  541. start = shards[i]
  542. end = shards[i]
  543. totalSize = shardSizes[shards[i]]
  544. }
  545. }
  546. // Add the last range
  547. if start == end {
  548. size := shardSizes[start]
  549. if size > 0 {
  550. ranges = append(ranges, fmt.Sprintf("%d(%s)", start, bytesToHumanReadable(size)))
  551. } else {
  552. ranges = append(ranges, fmt.Sprintf("%d", start))
  553. }
  554. } else {
  555. // Calculate total size for the range
  556. rangeSize := shardSizes[start]
  557. for j := start + 1; j <= end; j++ {
  558. rangeSize += shardSizes[j]
  559. }
  560. if rangeSize > 0 {
  561. ranges = append(ranges, fmt.Sprintf("%d-%d(%s)", start, end, bytesToHumanReadable(rangeSize)))
  562. } else {
  563. ranges = append(ranges, fmt.Sprintf("%d-%d", start, end))
  564. }
  565. }
  566. return strings.Join(ranges, ",")
  567. }
  568. // Helper function to convert bytes to human readable format
  569. func bytesToHumanReadable(bytes int64) string {
  570. const unit = 1024
  571. if bytes < unit {
  572. return fmt.Sprintf("%dB", bytes)
  573. }
  574. div, exp := int64(unit), 0
  575. for n := bytes / unit; n >= unit; n /= unit {
  576. div *= unit
  577. exp++
  578. }
  579. return fmt.Sprintf("%.1f%cB", float64(bytes)/float64(div), "KMGTPE"[exp])
  580. }
  581. // Helper function to format missing shards
  582. func formatMissingShards(missingShards []int) string {
  583. if len(missingShards) == 0 {
  584. return ""
  585. }
  586. var shardStrs []string
  587. for _, shard := range missingShards {
  588. shardStrs = append(shardStrs, fmt.Sprintf("%d", shard))
  589. }
  590. return strings.Join(shardStrs, ", ")
  591. }
  592. // Helper function to check if all shard sizes are the same
  593. func areAllShardSizesSame(shardSizes map[int]int64) bool {
  594. if len(shardSizes) <= 1 {
  595. return true
  596. }
  597. var firstSize int64 = -1
  598. for _, size := range shardSizes {
  599. if firstSize == -1 {
  600. firstSize = size
  601. } else if size != firstSize {
  602. return false
  603. }
  604. }
  605. return true
  606. }
  607. // Helper function to get the common shard size (when all shards are the same size)
  608. func getCommonShardSize(shardSizes map[int]int64) string {
  609. for _, size := range shardSizes {
  610. return bytesToHumanReadable(size)
  611. }
  612. return "-"
  613. }
  614. // Helper function to format individual shard sizes
  615. func formatIndividualShardSizes(shardSizes map[int]int64) string {
  616. if len(shardSizes) == 0 {
  617. return ""
  618. }
  619. // Group shards by size for more compact display
  620. sizeGroups := make(map[int64][]int)
  621. for shardId, size := range shardSizes {
  622. sizeGroups[size] = append(sizeGroups[size], shardId)
  623. }
  624. // If there are only 1-2 different sizes, show them grouped
  625. if len(sizeGroups) <= 3 {
  626. var groupStrs []string
  627. for size, shardIds := range sizeGroups {
  628. // Sort shard IDs
  629. for i := 0; i < len(shardIds); i++ {
  630. for j := i + 1; j < len(shardIds); j++ {
  631. if shardIds[i] > shardIds[j] {
  632. shardIds[i], shardIds[j] = shardIds[j], shardIds[i]
  633. }
  634. }
  635. }
  636. var idRanges []string
  637. if len(shardIds) <= erasure_coding.ParityShardsCount {
  638. // Show individual IDs if few shards
  639. for _, id := range shardIds {
  640. idRanges = append(idRanges, fmt.Sprintf("%d", id))
  641. }
  642. } else {
  643. // Show count if many shards
  644. idRanges = append(idRanges, fmt.Sprintf("%d shards", len(shardIds)))
  645. }
  646. groupStrs = append(groupStrs, fmt.Sprintf("%s: %s", strings.Join(idRanges, ","), bytesToHumanReadable(size)))
  647. }
  648. return strings.Join(groupStrs, " | ")
  649. }
  650. // If too many different sizes, show summary
  651. return fmt.Sprintf("%d different sizes", len(sizeGroups))
  652. }
  653. // displayVolumeDistribution shows the distribution summary for a volume
  654. templ displayVolumeDistribution(volume dash.EcVolumeWithShards) {
  655. <div class="small">
  656. <i class="fas fa-sitemap me-1"></i>
  657. { calculateVolumeDistributionSummary(volume) }
  658. </div>
  659. }
  660. // displayEcVolumeStatus shows an improved status display for EC volumes
  661. templ displayEcVolumeStatus(volume dash.EcVolumeWithShards) {
  662. if volume.IsComplete {
  663. <span class="badge bg-success"><i class="fas fa-check me-1"></i>Complete</span>
  664. } else {
  665. if len(volume.MissingShards) > erasure_coding.DataShardsCount {
  666. <span class="badge bg-danger"><i class="fas fa-skull me-1"></i>Critical ({fmt.Sprintf("%d", len(volume.MissingShards))} missing)</span>
  667. } else if len(volume.MissingShards) > (erasure_coding.DataShardsCount/2) {
  668. <span class="badge bg-warning"><i class="fas fa-exclamation-triangle me-1"></i>Degraded ({fmt.Sprintf("%d", len(volume.MissingShards))} missing)</span>
  669. } else if len(volume.MissingShards) > (erasure_coding.ParityShardsCount/2) {
  670. <span class="badge bg-warning"><i class="fas fa-info-circle me-1"></i>Incomplete ({fmt.Sprintf("%d", len(volume.MissingShards))} missing)</span>
  671. } else {
  672. <span class="badge bg-info"><i class="fas fa-info-circle me-1"></i>Minor Issues ({fmt.Sprintf("%d", len(volume.MissingShards))} missing)</span>
  673. }
  674. }
  675. }
  676. // calculateVolumeDistributionSummary calculates and formats the distribution summary for a volume
  677. func calculateVolumeDistributionSummary(volume dash.EcVolumeWithShards) string {
  678. dataCenters := make(map[string]bool)
  679. racks := make(map[string]bool)
  680. servers := make(map[string]bool)
  681. // Count unique servers from shard locations
  682. for _, server := range volume.ShardLocations {
  683. servers[server] = true
  684. }
  685. // Use the DataCenters field if available
  686. for _, dc := range volume.DataCenters {
  687. dataCenters[dc] = true
  688. }
  689. // Use the Servers field if available
  690. for _, server := range volume.Servers {
  691. servers[server] = true
  692. }
  693. // Use the Racks field if available
  694. for _, rack := range volume.Racks {
  695. racks[rack] = true
  696. }
  697. // If we don't have rack information, estimate it from servers as fallback
  698. rackCount := len(racks)
  699. if rackCount == 0 {
  700. // Fallback estimation - assume each server might be in a different rack
  701. rackCount = len(servers)
  702. if len(dataCenters) > 0 {
  703. // More conservative estimate if we have DC info
  704. rackCount = (len(servers) + len(dataCenters) - 1) / len(dataCenters)
  705. if rackCount == 0 {
  706. rackCount = 1
  707. }
  708. }
  709. }
  710. return fmt.Sprintf("%d DCs, %d racks, %d servers", len(dataCenters), rackCount, len(servers))
  711. }