raid.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. <style>
  2. #activate_raid_btn{
  3. background: var(--ts-positive-400) !important;
  4. border: 0px solid transparent !important;
  5. }
  6. </style>
  7. <div class="ts-content">
  8. <div class="ts-container is-padded">
  9. <div class="ts-grid mobile:is-stacked">
  10. <div class="column is-6-wide">
  11. <div id="raid_array_list" class="ts-menu is-start-icon is-separated">
  12. <a class="item">
  13. <span class="ts-icon is-user-icon"></span> 使用者
  14. </a>
  15. <a class="item is-active">
  16. <span class="ts-icon is-house-icon"></span> 首頁
  17. </a>
  18. <a class="item">
  19. <span class="ts-icon is-newspaper-icon"></span> 新聞
  20. </a>
  21. </div>
  22. <div class="ts-divider has-top-spaced-small"></div>
  23. <div class="ts-content is-center-aligned">
  24. <button class="ts-button is-start-icon is-positive is-circular">
  25. <span class="ts-icon is-circle-plus-icon" style="color: var(--ts-positive-500);"></span>
  26. <span i18n>Create RAID
  27. // 新增陣列
  28. </span>
  29. </button>
  30. <button class="ts-button is-start-icon is-positive is-circular">
  31. <span class="ts-icon is-rotate-icon" style="color: var(--ts-primary-500);"></span>
  32. <span i18n>Assemble
  33. // 重組陣列
  34. </span>
  35. </button>
  36. </div>
  37. </div>
  38. <div class="column is-fluid">
  39. <div id="raid_details">
  40. </div>
  41. </div>
  42. </div>
  43. </div>
  44. </div>
  45. <script>
  46. function initRAIDDeviceList(){
  47. $.ajax({
  48. url: './api/raid/list',
  49. type: 'GET',
  50. dataType: 'json',
  51. success: function(data) {
  52. $('#raid_array_list').html("");
  53. if (data.error != undefined){
  54. // Handle error response
  55. console.error('Error fetching RAID devices:', data.error);
  56. $('#raid_array_list').append('<div class="ts-text is-error">Error: ' + data.error + '</div>');
  57. }else{
  58. data.forEach((raid, index) => {
  59. let raidDetails = renderRAIDPoolDetail(raid, index);
  60. $("#raid_array_list").append(raidDetails[0]);
  61. $('#raid_details').append(raidDetails[1]);
  62. });
  63. if (data.length == 0){
  64. $('#raid_array_list').append(`
  65. <div class="ts-blankslate" style="pointer-events: none; user-select: none; opacity: 0.7;">
  66. <div class="description">
  67. <span class="ts-icon is-circle-check-icon" style="color: var(--ts-positive-400);"></span>
  68. <span class="has-start-spaced-small" i18n> No RAID array found.
  69. // 沒有 RAID 陣列
  70. </span>
  71. </div>
  72. </div>`);
  73. }
  74. }
  75. // Show the first RAID details by default
  76. if (data.length > 0) {
  77. showRAIDDetails(0);
  78. }
  79. relocale(); // Recalculate layout
  80. syncProgressTicker(); // Start the sync progress ticker
  81. },
  82. error: function(xhr, status, error) {
  83. console.error('Error fetching RAID devices:', error);
  84. }
  85. });
  86. }
  87. initRAIDDeviceList();
  88. function updateRAIDArrayStatus(devname){
  89. if (devname.startsWith('/dev/')) {
  90. devname = devname.slice(5);
  91. }
  92. $.ajax({
  93. url: './api/raid/info?dev=' + devname,
  94. type: 'GET',
  95. dataType: 'json',
  96. success: function(data) {
  97. if (data.error != undefined){
  98. // Handle error response
  99. console.error('Error fetching RAID status:', data.error);
  100. msgbox("Error: " + data.error);
  101. return
  102. }
  103. let menuItem = $(`.raid-array[mdx="${devname}"]`);
  104. let raidDetails = $(`.raid-details[mdx="${devname}"]`);
  105. let index = menuItem.attr("idx");
  106. let domEles = renderRAIDPoolDetail(data, index);
  107. let currentShownDetailIndex = 0;
  108. if ($(`.raid-array.is-active`).length > 0 && $(`.raid-array.is-active`).attr("idx")){
  109. currentShownDetailIndex = parseInt($(`.raid-array.is-active`).attr("idx"));
  110. }
  111. menuItem.replaceWith(domEles[0]);
  112. raidDetails.replaceWith(domEles[1]);
  113. showRAIDDetails(currentShownDetailIndex);
  114. syncProgressTicker();
  115. },
  116. error: function(xhr, status, error) {
  117. console.error('Error updating RAID status:', error);
  118. }
  119. });
  120. }
  121. // Function to render RAID pool details
  122. // This function creates the HTML structure for each RAID pool
  123. // return the DOM element for the side menu and detail tab
  124. function renderRAIDPoolDetail(raid, index){
  125. // Utility function to convert bytes to human-readable format
  126. function bytesToHumanReadable(bytes) {
  127. const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
  128. if (bytes === 0) return '0 B';
  129. const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
  130. return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
  131. }
  132. // Add a new menu item for each RAID array
  133. let mdX = raid.DevicePath.split('/').pop();
  134. let isSyncing = false;
  135. let isResyncPending = false;
  136. let icon = '';
  137. if (raid.State.includes('clean') && !raid.State.includes('sync')) {
  138. icon = '<span class="ts-icon is-check-icon" style="color: var(--ts-positive-500);"></span>';
  139. } else if (raid.State.includes('sync')) {
  140. isSyncing = true;
  141. if (raid.State.includes('resyncing') && raid.State.includes('PENDING')) {
  142. //Syncing is pending
  143. isResyncPending = true;
  144. icon = '<span class="ts-icon is-rotate-icon" style="color: var(--ts-positive-500);"></span>';
  145. }else{
  146. icon = '<span class="ts-icon is-spinning is-rotate-icon" style="color: var(--ts-positive-500);"></span>';
  147. }
  148. } else if (raid.State.includes('degraded')) {
  149. icon = '<span class="ts-icon is-triangle-exclamation-icon" style="color: var(--ts-warning-600);"></span>';
  150. } else if (raid.State.includes('fail')) {
  151. icon = '<span class="ts-icon is-circle-xmark-icon" style="color: var(--ts-negative-500);"></span>';
  152. } else {
  153. icon = '<span class="ts-icon is-question-icon" style="color: var(--ts-gray-500);"></span>';
  154. }
  155. // Add a new menu item for each RAID array
  156. const menuItem = `
  157. <a class="raid-array item ${index==0?'is-active':''}" idx="${index}" id="raid_menu_${index}" mdx="${mdX}" onclick="showRAIDDetails(${index})">
  158. ${icon}
  159. <div class="ts-content is-dense">
  160. <div>
  161. <span class="ts-text is-heavy">${raid.DevicePath}</span> | ${raid.RaidLevel.toUpperCase()}
  162. </div>
  163. <div class="ts-text is-tiny has-top-spaced-small">
  164. ${raid.Name}
  165. </div>
  166. </div>
  167. </a>
  168. `;
  169. // Add a hidden div for each RAID array's details
  170. const raidDetails = `
  171. <div id="raid_details_${index}" mdx="${mdX}" idx="${index}" class="raid-details" style="display: none ;">
  172. <div class="ts-box">
  173. <div class="ts-content is-padded">
  174. <div class="ts-header is-start-icon">
  175. ${icon}
  176. ${raid.DevicePath} | ${raid.RaidLevel.toUpperCase()}
  177. </div>
  178. <div class="ts-text is-description">
  179. ${raid.UUID}<br>
  180. ${raid.Name}
  181. </div>
  182. <div class="ts-text">
  183. <span i18n> State
  184. // 狀態
  185. </span>: ${raid.State}<br>
  186. <!-- For Sync progress -->
  187. ${isSyncing?getRAIDSyncElement(raid, isSyncing):``}
  188. <!-- For RAID Completed -->
  189. ${isResyncPending? getRAIDResumeResyncElement(raid):``}
  190. <span i18n> Array Size
  191. // 陣列大小
  192. </span>: ${bytesToHumanReadable(raid.ArraySize * 1024)}<br>
  193. <span i18n> Created
  194. // 建立時間
  195. </span>: <span>${new Date(raid.CreationTime).toLocaleString('en-US', { timeZone: 'UTC' })}</span><br>
  196. </div>
  197. <table class="ts-table is-single-line has-top-spaced-large">
  198. <thead>
  199. <tr>
  200. <th i18n>Disk Status
  201. // 磁碟狀態
  202. </th>
  203. <th i18n>Counts
  204. // 數量
  205. </th>
  206. </tr>
  207. </thead>
  208. <tbody>
  209. <tr>
  210. <td i18n> Active Devices
  211. // 啟用的磁碟
  212. </td>
  213. <td>${raid.ActiveDevices}</td>
  214. </tr>
  215. <tr>
  216. <td i18n> Working Devices
  217. // 工作中的磁碟
  218. </td>
  219. <td>${raid.WorkingDevices}</td>
  220. </tr>
  221. <tr>
  222. <td i18n> Failed Devices
  223. // 故障的磁碟
  224. </td>
  225. <td>${raid.FailedDevices}</td>
  226. </tr>
  227. <tr>
  228. <td i18n> Spare Devices
  229. // 備用磁碟
  230. </td>
  231. <td>${raid.SpareDevices}</td>
  232. </tr>
  233. </tbody>
  234. </table>
  235. </div>
  236. </div>
  237. <div class="ts-box is-padded has-top-spaced-small">
  238. <div class="ts-content">
  239. </div>
  240. </div>
  241. </div>
  242. `;
  243. return [menuItem, raidDetails];
  244. }
  245. function getRAIDSyncElement(raid, isSyncing=true){
  246. return `<div class="sync-progress has-top-spaced-small ${isSyncing?'need-update-raid-sync-progress':''}" devname="${raid.DevicePath}" style="display: ${isSyncing?"auto":"none"};">
  247. <div class="ts-progress is-processing">
  248. <div class="bar" style="--value: 0">
  249. <div class="text">0%</div>
  250. </div>
  251. </div>
  252. <div class="ts-text is-description has-top-spaced-small">
  253. <span i18n> Synchronized
  254. // 已處理</span>
  255. <span class="processed_blocks"></span>
  256. <span>/</span>
  257. <span class="total_blocks"></span>
  258. <span i18n> blocks
  259. // 個區塊
  260. </span><br>
  261. <!-- <span i18n> Speed
  262. // 速度
  263. </span>: <span class="speed"></span><br>
  264. <span i18n> Expected Time
  265. // 預估時間
  266. </span>: <span class="expected_time"></span>
  267. -->
  268. </div>
  269. </div>`;
  270. }
  271. // DOM element for RAID resume resync
  272. function getRAIDResumeResyncElement(raid){
  273. return `<div class="ts-notice has-top-spaced-small has-bottom-spaced-small">
  274. <div class="title">
  275. <span i18n> RAID Resync Pending
  276. // RAID 重組待處理
  277. </span>
  278. </div>
  279. <div class="content">
  280. <span i18n> The previous resync operation was interrupted. Click to resume.
  281. // 先前的重組操作已中斷,點擊以繼續。
  282. </span>
  283. </div>
  284. </div>
  285. <button id="activate_raid_btn" onclick="activateSyncPendingDisk('${raid.DevicePath}');" class="ts-button is-fluid has-bottom-spaced-small" i18n> Start Resync
  286. // 開始重組
  287. </button>`
  288. }
  289. // Function to activate a finished RAID sync
  290. // Will set the RAID device to -readwrite state
  291. function activateSyncPendingDisk(devname){
  292. $.cjax({
  293. url: './api/raid/start-resync',
  294. method: 'POST',
  295. data: { dev: devname},
  296. success: function(data) {
  297. if (data.error != undefined){
  298. // Handle error response
  299. console.error('Error start resyncing RAID device:', data.error);
  300. }else{
  301. // Successfully activated the device
  302. console.log('RAID device resync started successfully:', data);
  303. msgbox(i18nc("raid_resync_started_succ"));
  304. setTimeout(function() {
  305. // Refresh the RAID device list after a short delay
  306. updateRAIDArrayStatus(devname);
  307. }, 300);
  308. }
  309. },
  310. });
  311. }
  312. //Create a ticker to check for RAID sync progress
  313. function syncProgressTicker(){
  314. let syncProgressTracker = $(".need-update-raid-sync-progress");
  315. if (syncProgressTracker.length > 0){
  316. syncProgressTracker.each(function(){
  317. let devname = $(this).attr("devname");
  318. $.ajax({
  319. url: './api/raid/sync?dev=' + devname,
  320. type: 'GET',
  321. dataType: 'json',
  322. data: { devname: devname },
  323. success: function(data) {
  324. if (data.error != undefined){
  325. // The device is no longer in sync state. Hide the sync progress bar
  326. $(`.sync-progress[devname="${devname}"]`).hide();
  327. $(`.sync-progress[devname="${devname}"]`).removeClass("need-update-raid-sync-progress");
  328. }else{
  329. let progress = parseFloat(data.ResyncPercent);
  330. let total_blocks = parseInt(data.TotalBlocks);
  331. let processed_blocks = parseInt(data.CompletedBlocks);
  332. let expected_time = data.ExpectedTime;
  333. let speed = data.Speed;
  334. $(`.sync-progress[devname="${devname}"] .bar`).css('--value', progress);
  335. $(`.sync-progress[devname="${devname}"] .bar .text`).text(`${progress.toFixed(1)}%`);
  336. $(`.sync-progress[devname="${devname}"] .processed_blocks`).text(processed_blocks);
  337. $(`.sync-progress[devname="${devname}"] .total_blocks`).text(total_blocks);
  338. //$(`.sync-progress[devname="${devname}"] .ts-text.is-description .speed`).text(speed);
  339. //$(`.sync-progress[devname="${devname}"] .ts-text.is-description .expected_time`).text(expected_time);
  340. }
  341. },
  342. error: function(xhr, status, error) {
  343. console.error('Error fetching RAID sync progress:', error);
  344. }
  345. });
  346. });
  347. }
  348. }
  349. setInterval(syncProgressTicker, 5000); // Check every 5 seconds
  350. function showRAIDDetails(index) {
  351. $('.raid-details').hide(); // Hide all RAID details
  352. $(`#raid_details_${index}`).show(); // Show the selected RAID details
  353. $('.raid-array.is-active').removeClass('is-active'); // Remove active class from all menu items
  354. $(`#raid_menu_${index}`).addClass('is-active'); // Add active class to the selected menu item
  355. relocale(); // Recalculate layout
  356. }
  357. </script>