ec_volume_details.templ 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. package app
  2. import (
  3. "fmt"
  4. "github.com/seaweedfs/seaweedfs/weed/admin/dash"
  5. )
  6. templ EcVolumeDetails(data dash.EcVolumeDetailsData) {
  7. <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
  8. <div>
  9. <h1 class="h2">
  10. <i class="fas fa-th-large me-2"></i>EC Volume Details
  11. </h1>
  12. <nav aria-label="breadcrumb">
  13. <ol class="breadcrumb">
  14. <li class="breadcrumb-item"><a href="/admin" class="text-decoration-none">Dashboard</a></li>
  15. <li class="breadcrumb-item"><a href="/cluster/ec-shards" class="text-decoration-none">EC Volumes</a></li>
  16. <li class="breadcrumb-item active" aria-current="page">Volume {fmt.Sprintf("%d", data.VolumeID)}</li>
  17. </ol>
  18. </nav>
  19. </div>
  20. <div class="btn-toolbar mb-2 mb-md-0">
  21. <div class="btn-group me-2">
  22. <button type="button" class="btn btn-sm btn-outline-secondary" onclick="history.back()">
  23. <i class="fas fa-arrow-left me-1"></i>Back
  24. </button>
  25. <button type="button" class="btn btn-sm btn-outline-primary" onclick="window.location.reload()">
  26. <i class="fas fa-refresh me-1"></i>Refresh
  27. </button>
  28. </div>
  29. </div>
  30. </div>
  31. <!-- EC Volume Summary -->
  32. <div class="row mb-4">
  33. <div class="col-md-6">
  34. <div class="card">
  35. <div class="card-header">
  36. <h5 class="card-title mb-0">
  37. <i class="fas fa-info-circle me-2"></i>Volume Information
  38. </h5>
  39. </div>
  40. <div class="card-body">
  41. <table class="table table-borderless">
  42. <tr>
  43. <td><strong>Volume ID:</strong></td>
  44. <td>{fmt.Sprintf("%d", data.VolumeID)}</td>
  45. </tr>
  46. <tr>
  47. <td><strong>Collection:</strong></td>
  48. <td>
  49. if data.Collection != "" {
  50. <span class="badge bg-info">{data.Collection}</span>
  51. } else {
  52. <span class="text-muted">default</span>
  53. }
  54. </td>
  55. </tr>
  56. <tr>
  57. <td><strong>Status:</strong></td>
  58. <td>
  59. if data.IsComplete {
  60. <span class="badge bg-success">
  61. <i class="fas fa-check me-1"></i>Complete ({data.TotalShards}/14 shards)
  62. </span>
  63. } else {
  64. <span class="badge bg-warning">
  65. <i class="fas fa-exclamation-triangle me-1"></i>Incomplete ({data.TotalShards}/14 shards)
  66. </span>
  67. }
  68. </td>
  69. </tr>
  70. if !data.IsComplete {
  71. <tr>
  72. <td><strong>Missing Shards:</strong></td>
  73. <td>
  74. for i, shardID := range data.MissingShards {
  75. if i > 0 {
  76. <span>, </span>
  77. }
  78. <span class="badge bg-danger">{fmt.Sprintf("%02d", shardID)}</span>
  79. }
  80. </td>
  81. </tr>
  82. }
  83. <tr>
  84. <td><strong>Data Centers:</strong></td>
  85. <td>
  86. for i, dc := range data.DataCenters {
  87. if i > 0 {
  88. <span>, </span>
  89. }
  90. <span class="badge bg-primary">{dc}</span>
  91. }
  92. </td>
  93. </tr>
  94. <tr>
  95. <td><strong>Servers:</strong></td>
  96. <td>
  97. <span class="text-muted">{fmt.Sprintf("%d servers", len(data.Servers))}</span>
  98. </td>
  99. </tr>
  100. <tr>
  101. <td><strong>Last Updated:</strong></td>
  102. <td>
  103. <span class="text-muted">{data.LastUpdated.Format("2006-01-02 15:04:05")}</span>
  104. </td>
  105. </tr>
  106. </table>
  107. </div>
  108. </div>
  109. </div>
  110. <div class="col-md-6">
  111. <div class="card">
  112. <div class="card-header">
  113. <h5 class="card-title mb-0">
  114. <i class="fas fa-chart-pie me-2"></i>Shard Distribution
  115. </h5>
  116. </div>
  117. <div class="card-body">
  118. <div class="row text-center">
  119. <div class="col-4">
  120. <div class="border rounded p-3">
  121. <h3 class="text-primary mb-1">{fmt.Sprintf("%d", data.TotalShards)}</h3>
  122. <small class="text-muted">Total Shards</small>
  123. </div>
  124. </div>
  125. <div class="col-4">
  126. <div class="border rounded p-3">
  127. <h3 class="text-success mb-1">{fmt.Sprintf("%d", len(data.DataCenters))}</h3>
  128. <small class="text-muted">Data Centers</small>
  129. </div>
  130. </div>
  131. <div class="col-4">
  132. <div class="border rounded p-3">
  133. <h3 class="text-info mb-1">{fmt.Sprintf("%d", len(data.Servers))}</h3>
  134. <small class="text-muted">Servers</small>
  135. </div>
  136. </div>
  137. </div>
  138. <!-- Shard Distribution Visualization -->
  139. <div class="mt-3">
  140. <h6>Present Shards:</h6>
  141. <div class="d-flex flex-wrap gap-1">
  142. for _, shard := range data.Shards {
  143. <span class="badge bg-success me-1 mb-1">{fmt.Sprintf("%02d", shard.ShardID)}</span>
  144. }
  145. </div>
  146. if len(data.MissingShards) > 0 {
  147. <h6 class="mt-2">Missing Shards:</h6>
  148. <div class="d-flex flex-wrap gap-1">
  149. for _, shardID := range data.MissingShards {
  150. <span class="badge bg-secondary me-1 mb-1">{fmt.Sprintf("%02d", shardID)}</span>
  151. }
  152. </div>
  153. }
  154. </div>
  155. </div>
  156. </div>
  157. </div>
  158. </div>
  159. <!-- Shard Details Table -->
  160. <div class="card">
  161. <div class="card-header">
  162. <h5 class="card-title mb-0">
  163. <i class="fas fa-list me-2"></i>Shard Details
  164. </h5>
  165. </div>
  166. <div class="card-body">
  167. if len(data.Shards) > 0 {
  168. <div class="table-responsive">
  169. <table class="table table-striped table-hover">
  170. <thead>
  171. <tr>
  172. <th>
  173. <a href="#" onclick="sortBy('shard_id')" class="text-dark text-decoration-none">
  174. Shard ID
  175. if data.SortBy == "shard_id" {
  176. if data.SortOrder == "asc" {
  177. <i class="fas fa-sort-up ms-1"></i>
  178. } else {
  179. <i class="fas fa-sort-down ms-1"></i>
  180. }
  181. } else {
  182. <i class="fas fa-sort ms-1 text-muted"></i>
  183. }
  184. </a>
  185. </th>
  186. <th>
  187. <a href="#" onclick="sortBy('server')" class="text-dark text-decoration-none">
  188. Server
  189. if data.SortBy == "server" {
  190. if data.SortOrder == "asc" {
  191. <i class="fas fa-sort-up ms-1"></i>
  192. } else {
  193. <i class="fas fa-sort-down ms-1"></i>
  194. }
  195. } else {
  196. <i class="fas fa-sort ms-1 text-muted"></i>
  197. }
  198. </a>
  199. </th>
  200. <th>
  201. <a href="#" onclick="sortBy('data_center')" class="text-dark text-decoration-none">
  202. Data Center
  203. if data.SortBy == "data_center" {
  204. if data.SortOrder == "asc" {
  205. <i class="fas fa-sort-up ms-1"></i>
  206. } else {
  207. <i class="fas fa-sort-down ms-1"></i>
  208. }
  209. } else {
  210. <i class="fas fa-sort ms-1 text-muted"></i>
  211. }
  212. </a>
  213. </th>
  214. <th>
  215. <a href="#" onclick="sortBy('rack')" class="text-dark text-decoration-none">
  216. Rack
  217. if data.SortBy == "rack" {
  218. if data.SortOrder == "asc" {
  219. <i class="fas fa-sort-up ms-1"></i>
  220. } else {
  221. <i class="fas fa-sort-down ms-1"></i>
  222. }
  223. } else {
  224. <i class="fas fa-sort ms-1 text-muted"></i>
  225. }
  226. </a>
  227. </th>
  228. <th class="text-dark">Disk Type</th>
  229. <th class="text-dark">Shard Size</th>
  230. <th class="text-dark">Actions</th>
  231. </tr>
  232. </thead>
  233. <tbody>
  234. for _, shard := range data.Shards {
  235. <tr>
  236. <td>
  237. <span class="badge bg-primary">{fmt.Sprintf("%02d", shard.ShardID)}</span>
  238. </td>
  239. <td>
  240. <a href={ templ.URL("/cluster/volume-servers/" + shard.Server) } class="text-primary text-decoration-none">
  241. <code class="small">{shard.Server}</code>
  242. </a>
  243. </td>
  244. <td>
  245. <span class="badge bg-primary text-white">{shard.DataCenter}</span>
  246. </td>
  247. <td>
  248. <span class="badge bg-secondary text-white">{shard.Rack}</span>
  249. </td>
  250. <td>
  251. <span class="text-dark">{shard.DiskType}</span>
  252. </td>
  253. <td>
  254. <span class="text-success">{bytesToHumanReadableUint64(shard.Size)}</span>
  255. </td>
  256. <td>
  257. <a href={ templ.SafeURL(fmt.Sprintf("http://%s/ui/index.html", shard.Server)) } target="_blank" class="btn btn-sm btn-primary">
  258. <i class="fas fa-external-link-alt me-1"></i>Volume Server
  259. </a>
  260. </td>
  261. </tr>
  262. }
  263. </tbody>
  264. </table>
  265. </div>
  266. } else {
  267. <div class="text-center py-4">
  268. <i class="fas fa-exclamation-triangle fa-3x text-warning mb-3"></i>
  269. <h5>No EC shards found</h5>
  270. <p class="text-muted">This volume may not be EC encoded yet.</p>
  271. </div>
  272. }
  273. </div>
  274. </div>
  275. <script>
  276. // Sorting functionality
  277. function sortBy(field) {
  278. const currentSort = new URLSearchParams(window.location.search).get('sort_by');
  279. const currentOrder = new URLSearchParams(window.location.search).get('sort_order') || 'asc';
  280. let newOrder = 'asc';
  281. if (currentSort === field && currentOrder === 'asc') {
  282. newOrder = 'desc';
  283. }
  284. const url = new URL(window.location);
  285. url.searchParams.set('sort_by', field);
  286. url.searchParams.set('sort_order', newOrder);
  287. window.location.href = url.toString();
  288. }
  289. </script>
  290. }
  291. // Helper function to convert bytes to human readable format (uint64 version)
  292. func bytesToHumanReadableUint64(bytes uint64) string {
  293. const unit = 1024
  294. if bytes < unit {
  295. return fmt.Sprintf("%dB", bytes)
  296. }
  297. div, exp := uint64(unit), 0
  298. for n := bytes / unit; n >= unit; n /= unit {
  299. div *= unit
  300. exp++
  301. }
  302. return fmt.Sprintf("%.1f%cB", float64(bytes)/float64(div), "KMGTPE"[exp])
  303. }