raid.html 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. <style>
  2. #activate_raid_btn{
  3. background: var(--ts-positive-400) !important;
  4. border: 0px solid transparent !important;
  5. }
  6. .raid-details{
  7. position: relative;
  8. }
  9. .raid-array-opr-btns{
  10. position: absolute !important;
  11. top: 1em;
  12. right: 1em;
  13. }
  14. .raid-menu-badge{
  15. margin-top: -3px !important;
  16. margin-left: 0.15em !important;
  17. }
  18. </style>
  19. <div class="ts-content">
  20. <div class="ts-container is-padded">
  21. <div class="ts-grid mobile:is-stacked">
  22. <div class="column is-6-wide">
  23. <div id="raid_array_list" class="ts-menu is-start-icon is-separated">
  24. <div class="ts-blankslate" style="pointer-events: none; user-select: none; opacity: 0.7;">
  25. <div class="description">
  26. <span class="ts-icon is-circle-notch-icon is-spinning"></span>
  27. <span class="has-start-spaced-small">Loading...</span>
  28. </div>
  29. </div>
  30. </div>
  31. <div class="ts-divider has-top-spaced-small"></div>
  32. <div class="ts-content is-center-aligned">
  33. <button class="ts-button is-start-icon is-positive is-circular">
  34. <span class="ts-icon is-circle-plus-icon" style="color: var(--ts-positive-500);"></span>
  35. <span i18n>Create RAID
  36. // 新增陣列
  37. </span>
  38. </button>
  39. <button class="ts-button is-start-icon is-positive is-circular">
  40. <span class="ts-icon is-rotate-icon" style="color: var(--ts-primary-500);"></span>
  41. <span i18n>Assemble
  42. // 重組陣列
  43. </span>
  44. </button>
  45. </div>
  46. </div>
  47. <div class="column is-fluid">
  48. <div id="raid_details">
  49. </div>
  50. </div>
  51. </div>
  52. </div>
  53. </div>
  54. <script>
  55. function initRAIDDeviceList(){
  56. $.ajax({
  57. url: './api/raid/list',
  58. type: 'GET',
  59. dataType: 'json',
  60. success: function(data) {
  61. $('#raid_array_list').html("");
  62. if (data.error != undefined){
  63. // Handle error response
  64. console.error('Error fetching RAID devices:', data.error);
  65. $('#raid_array_list').append('<div class="ts-text is-error">Error: ' + data.error + '</div>');
  66. }else{
  67. data.forEach((raid, index) => {
  68. let raidDetails = renderRAIDPoolDetail(raid, index);
  69. $("#raid_array_list").append(raidDetails[0]);
  70. $('#raid_details').append(raidDetails[1]);
  71. getRAIDSpaceInfoForDev(raid.DevicePath);
  72. });
  73. if (data.length == 0){
  74. $('#raid_array_list').append(`
  75. <div class="ts-blankslate" style="pointer-events: none; user-select: none; opacity: 0.7;">
  76. <div class="description">
  77. <span class="ts-icon is-circle-check-icon" style="color: var(--ts-positive-400);"></span>
  78. <span class="has-start-spaced-small" i18n> No RAID array found.
  79. // 沒有 RAID 陣列
  80. </span>
  81. </div>
  82. </div>`);
  83. }
  84. }
  85. // Show the first RAID details by default
  86. if (data.length > 0) {
  87. showRAIDDetails(0);
  88. }
  89. relocale(); // Recalculate layout
  90. syncProgressTicker(); // Start the sync progress ticker
  91. },
  92. error: function(xhr, status, error) {
  93. console.error('Error fetching RAID devices:', error);
  94. }
  95. });
  96. }
  97. initRAIDDeviceList();
  98. function refreshRAIDArrayStatus(devname){
  99. //Hide the raid details
  100. $(`.raid-details[mdx=${devname}]`).hide();
  101. updateRAIDArrayStatus(devname, function(data){
  102. if (data.error == undefined){
  103. msgbox(i18nc("raid_device_updated_succ"));
  104. }
  105. $(`.raid-details[mdx=${devname}]`).show();
  106. });
  107. }
  108. function updateRAIDArrayStatus(devname, callback=undefined){
  109. if (devname.startsWith('/dev/')) {
  110. devname = devname.slice(5);
  111. }
  112. $.ajax({
  113. url: './api/raid/info?dev=' + devname,
  114. type: 'GET',
  115. dataType: 'json',
  116. success: function(data) {
  117. if (data.error != undefined){
  118. // Handle error response
  119. console.error('Error fetching RAID status:', data.error);
  120. msgbox("Error: " + data.error);
  121. if (callback){
  122. callback(data);
  123. }
  124. return
  125. }
  126. // Update the RAID array status
  127. // Find the corresponding menu item and details tab
  128. let menuItem = $(`.raid-array[mdx="${devname}"]`);
  129. let raidDetails = $(`.raid-details[mdx="${devname}"]`);
  130. let index = menuItem.attr("idx");
  131. let domEles = renderRAIDPoolDetail(data, index);
  132. let currentShownDetailIndex = 0;
  133. if ($(`.raid-array.is-active`).length > 0 && $(`.raid-array.is-active`).attr("idx")){
  134. currentShownDetailIndex = parseInt($(`.raid-array.is-active`).attr("idx"));
  135. }
  136. menuItem.replaceWith(domEles[0]);
  137. raidDetails.replaceWith(domEles[1]);
  138. showRAIDDetails(currentShownDetailIndex);
  139. syncProgressTicker();
  140. if (callback){
  141. callback(data);
  142. }
  143. },
  144. error: function(xhr, status, error) {
  145. console.error('Error updating RAID status:', error);
  146. }
  147. });
  148. }
  149. function deleteRAIDArray(devname){
  150. if (devname.startsWith('/dev/')) {
  151. devname = devname.slice(5);
  152. }
  153. /*
  154. //TODO: API still not ready
  155. $.cjax({
  156. url: './api/raid/delete',
  157. method: 'POST',
  158. data: { dev: devname},
  159. success: function(data) {
  160. if (data.error != undefined){
  161. // Handle error response
  162. console.error('Error deleting RAID device:', data.error);
  163. msgbox("Error: " + data.error);
  164. }else{
  165. // Successfully deleted the device
  166. console.log('RAID device deleted successfully:', data);
  167. msgbox(i18nc("raid_device_deleted_succ"));
  168. setTimeout(function() {
  169. // Refresh the RAID device list after a short delay
  170. initRAIDDeviceList();
  171. }, 300);
  172. }
  173. },
  174. });
  175. */
  176. }
  177. // Utility function to convert bytes to human-readable format
  178. function bytesToHumanReadable(bytes) {
  179. const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
  180. if (bytes === 0) return '0 B';
  181. const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
  182. return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
  183. }
  184. function getRAIDSpaceInfoForDev(devname){
  185. if (devname.startsWith('/dev/')) {
  186. devname = devname.slice(5);
  187. }
  188. let gaugeElement = $(`.raid-details[mdx=${devname}]`).find(".raid-usage-info");
  189. let updateElement = $(`.raid-details[mdx=${devname}]`).find(".raid-total-used-space");
  190. $.get("./api/raid/overview", function(data){
  191. if (data.error != undefined){
  192. // Handle error response
  193. return;
  194. }
  195. if (data && Array.isArray(data)) {
  196. let raidInfo = data.find(raid => raid.Name === devname);
  197. if (raidInfo) {
  198. let usedPercentage = (raidInfo.UsedSize / raidInfo.TotalSize) * 100;
  199. gaugeElement.find('.bar').css('--value', usedPercentage.toFixed(1));
  200. gaugeElement.find('.bar .text').text(`${usedPercentage.toFixed(1)}%`);
  201. updateElement.html(bytesToHumanReadable(raidInfo.UsedSize));
  202. }
  203. }
  204. })
  205. }
  206. // Function to render RAID pool details
  207. // This function creates the HTML structure for each RAID pool
  208. // return the DOM element for the side menu and detail tab
  209. function renderRAIDPoolDetail(raid, index){
  210. // Add a new menu item for each RAID array
  211. let mdX = raid.DevicePath.split('/').pop();
  212. let isSyncing = false;
  213. let isResyncPending = false;
  214. let icon = '';
  215. if (raid.State.includes('clean') && !raid.State.includes('sync')) {
  216. icon = '<span class="ts-icon is-check-icon" style="color: var(--ts-positive-500); font-size: 2em;"></span>';
  217. } else if (raid.State.includes('sync')) {
  218. isSyncing = true;
  219. if (raid.State.includes('resyncing') && raid.State.includes('PENDING')) {
  220. //Syncing is pending
  221. isResyncPending = true;
  222. icon = '<span class="ts-icon is-rotate-icon" style="color: var(--ts-positive-500); font-size: 2em;"></span>';
  223. }else{
  224. icon = '<span class="ts-icon is-spinning is-rotate-icon" style="color: var(--ts-positive-500); font-size: 2em;"></span>';
  225. }
  226. } else if (raid.State.includes('degraded')) {
  227. icon = '<span class="ts-icon is-triangle-exclamation-icon" style="color: var(--ts-warning-600); font-size: 2em;"></span>';
  228. } else if (raid.State.includes('fail')) {
  229. icon = '<span class="ts-icon is-circle-xmark-icon" style="color: var(--ts-negative-500); font-size: 2em;"></span>';
  230. } else {
  231. icon = '<span class="ts-icon is-question-icon" style="color: var(--ts-gray-500); font-size: 2em;"></span>';
  232. }
  233. // Add a new menu item for each RAID array
  234. const menuItem = `
  235. <a class="raid-array item ${index==0?'is-active':''}" idx="${index}" id="raid_menu_${index}" mdx="${mdX}" onclick="showRAIDDetails(${index})">
  236. ${icon}
  237. <div class="ts-content is-dense">
  238. <div>
  239. <span class="ts-text is-heavy">${raid.DevicePath}</span>
  240. <span class="ts-badge is-secondary raid-menu-badge">${raid.RaidLevel.toUpperCase()}</span>
  241. </div>
  242. <div class="ts-text is-tiny has-top-spaced-small">
  243. ${raid.Name}
  244. </div>
  245. </div>
  246. </a>
  247. `;
  248. // Add a hidden div for each RAID array's details
  249. const raidDetails = `
  250. <div id="raid_details_${index}" mdx="${mdX}" idx="${index}" class="raid-details" style="display: none ;">
  251. <div class="ts-box">
  252. <div class="ts-content is-padded">
  253. <div class="ts-header is-start-icon">
  254. ${icon}
  255. ${raid.DevicePath} <span class="ts-badge is-start-spaced">${raid.RaidLevel.toUpperCase()}</span>
  256. </div>
  257. <div class="ts-text is-description">
  258. ${raid.UUID}<br>
  259. ${raid.Name}
  260. </div>
  261. <div class="ts-text">
  262. <span i18n> State
  263. // 狀態
  264. </span>: ${raid.State}<br>
  265. <!-- For Sync progress -->
  266. ${isSyncing?getRAIDSyncElement(raid, isSyncing):``}
  267. <!-- For RAID Completed -->
  268. ${isResyncPending? getRAIDResumeResyncElement(raid):``}
  269. <span i18n> Array Size
  270. // 陣列大小
  271. </span>: ${bytesToHumanReadable(raid.ArraySize * 1024)}<br>
  272. <span i18n> Created
  273. // 建立時間
  274. </span>: <span>${new Date(raid.CreationTime).toLocaleString('en-US', { timeZone: 'UTC' })}</span><br>
  275. </div>
  276. <!-- Disk States Summary -->
  277. <table class="ts-table is-single-line has-top-spaced-large">
  278. <thead>
  279. <tr>
  280. <th i18n>Disk Status
  281. // 磁碟狀態
  282. </th>
  283. <th i18n>Counts
  284. // 數量
  285. </th>
  286. </tr>
  287. </thead>
  288. <tbody>
  289. <tr>
  290. <td i18n> Active Devices
  291. // 啟用的磁碟
  292. </td>
  293. <td>${raid.ActiveDevices}</td>
  294. </tr>
  295. <tr>
  296. <td i18n> Working Devices
  297. // 工作中的磁碟
  298. </td>
  299. <td>${raid.WorkingDevices}</td>
  300. </tr>
  301. <tr>
  302. <td i18n> Failed Devices
  303. // 故障的磁碟
  304. </td>
  305. <td>${raid.FailedDevices}</td>
  306. </tr>
  307. <tr>
  308. <td i18n> Spare Devices
  309. // 備用磁碟
  310. </td>
  311. <td>${raid.SpareDevices}</td>
  312. </tr>
  313. </tbody>
  314. </table>
  315. <!-- Usage Counters -->
  316. <div class="ts-grid is-evenly-divided has-top-spaced-large">
  317. <div class="column">
  318. <div class="ts-wrap is-middle-aligned">
  319. <div class="ts-gauge is-small is-circular raid-usage-info">
  320. <div class="bar" style="--value: 0">
  321. <div class="text">0%</div>
  322. </div>
  323. </div>
  324. <div>
  325. <div class="ts-text is-bold" i18n> Used Space
  326. // 已使用空間
  327. </div>
  328. <span class="raid-total-used-space">???</span> / ${bytesToHumanReadable(raid.ArraySize * 1024)}
  329. </div>
  330. </div>
  331. </div>
  332. <div class="column">
  333. <div class="ts-wrap is-middle-aligned">
  334. <div class="ts-gauge is-small is-circular">
  335. <div class="bar" style="--value: 100">
  336. <div class="text">---</div>
  337. </div>
  338. </div>
  339. <div>
  340. <div class="ts-text is-bold">傳輸</div>
  341. 0 B 已使用
  342. </div>
  343. </div>
  344. </div>
  345. </div>
  346. </div>
  347. </div>
  348. <div class="has-top-spaced-small">
  349. ${getRAIDChildDiskElement(raid.DeviceInfo)}
  350. </div>
  351. <!-- Operations -->
  352. <div class="raid-array-opr-btns">
  353. <div class="ts-content">
  354. <button class="ts-button is-circular is-icon" onclick="refreshRAIDArrayStatus('${mdX}');">
  355. <span class="ts-icon is-arrows-rotate-icon"></span>
  356. </button>
  357. <button class="ts-button is-circular is-icon is-negative" onclick="deleteRAIDArray('${mdX}');">
  358. <span class="ts-icon is-trash-icon"></span>
  359. </button>
  360. </div>
  361. </div>
  362. </div>
  363. `;
  364. return [menuItem, raidDetails];
  365. }
  366. function getRAIDSyncElement(raid, isSyncing=true){
  367. return `<div class="sync-progress has-top-spaced-small ${isSyncing?'need-update-raid-sync-progress':''}" devname="${raid.DevicePath}" style="display: ${isSyncing?"auto":"none"};">
  368. <div class="ts-progress is-processing">
  369. <div class="bar" style="--value: 0">
  370. <div class="text">0%</div>
  371. </div>
  372. </div>
  373. <div class="ts-text is-description has-top-spaced-small">
  374. <span i18n> Synchronized
  375. // 已處理</span>
  376. <span class="processed_blocks"></span>
  377. <span>/</span>
  378. <span class="total_blocks"></span>
  379. <span i18n> blocks
  380. // 個區塊
  381. </span><br>
  382. <!-- <span i18n> Speed
  383. // 速度
  384. </span>: <span class="speed"></span><br>
  385. <span i18n> Expected Time
  386. // 預估時間
  387. </span>: <span class="expected_time"></span>
  388. -->
  389. </div>
  390. </div>`;
  391. }
  392. // DOM element for RAID resume resync
  393. function getRAIDResumeResyncElement(raid){
  394. return `<div class="ts-notice has-top-spaced-small has-bottom-spaced-small">
  395. <div class="title">
  396. <span i18n> RAID Resync Pending
  397. // RAID 重組待處理
  398. </span>
  399. </div>
  400. <div class="content">
  401. <span i18n> The previous resync operation was interrupted. Click to resume.
  402. // 先前的重組操作已中斷,點擊以繼續。
  403. </span>
  404. </div>
  405. </div>
  406. <button id="activate_raid_btn" onclick="activateSyncPendingDisk('${raid.DevicePath}');" class="ts-button is-fluid has-bottom-spaced-small" i18n> Start Resync
  407. // 開始重組
  408. </button>`
  409. }
  410. // DOM elements for child disks
  411. function getRAIDChildDiskElement(raidDeviceInfo){
  412. if (raidDeviceInfo.length == 0 || raidDeviceInfo == null){
  413. return `<div class="ts-blankslate" style="pointer-events: none; user-select: none; opacity: 0.7;">
  414. <div class="description" i18n>No assigned disks
  415. // 沒有分配的磁碟
  416. </div>
  417. </div>`;
  418. }
  419. //Render each disk
  420. let result = '';
  421. for (let i = 0; i < raidDeviceInfo.length; i++) {
  422. let disk = raidDeviceInfo[i];
  423. if (disk.RaidDevice == -1){
  424. continue;
  425. }
  426. let elementUUID = "raid_child_" + disk.DevicePath.replace(/\//g, "_");
  427. let diskSdx = disk.DevicePath.split('/').pop();
  428. let thisDiskInfo = getDiskInfoDevicePath(diskSdx); //Try to load disk info from cache
  429. if (thisDiskInfo == null){
  430. //Make a request to get the disk info
  431. let thisDiskInfo = null;
  432. $.ajax({
  433. url: `./api/info/disk/${diskSdx}`,
  434. type: 'GET',
  435. dataType: 'json',
  436. success: function(data) {
  437. if (data.error != undefined){
  438. return;
  439. }
  440. thisDiskInfo = data;
  441. $(`.raid-child-disk[diskid='${elementUUID}']`).find(".raid-disk-name").text(thisDiskInfo.model);
  442. },
  443. error: function(xhr, status, error) {
  444. console.error('Error fetching disk info:', error);
  445. }
  446. });
  447. }
  448. result += `
  449. <div class="ts-box is-padded has-bottom-spaced-small raid-child-disk" diskid="${elementUUID}">
  450. <div class="ts-content">
  451. <div>
  452. <span class="ts-badge is-secondary has-end-spaced-small" style="margin-top: -0.3em;">${disk.DevicePath}</span>
  453. <span class="ts-text is-heavy raid-disk-name">Raid Device ${disk.RaidDevice}</span>
  454. </div>
  455. <div class="ts-text is-tiny has-top-spaced-small">
  456. <div class="has-start-spaced-small">
  457. <span i18n> State
  458. // 狀態
  459. </span>: ${disk.State.join(', ')}
  460. </div>
  461. </div>
  462. </div>
  463. </div>
  464. `;
  465. }
  466. return result;
  467. }
  468. // Function to activate a finished RAID sync
  469. // Will set the RAID device to -readwrite state
  470. function activateSyncPendingDisk(devname){
  471. $.cjax({
  472. url: './api/raid/start-resync',
  473. method: 'POST',
  474. data: { dev: devname},
  475. success: function(data) {
  476. if (data.error != undefined){
  477. // Handle error response
  478. console.error('Error start resyncing RAID device:', data.error);
  479. }else{
  480. // Successfully activated the device
  481. console.log('RAID device resync started successfully:', data);
  482. msgbox(i18nc("raid_resync_started_succ"));
  483. setTimeout(function() {
  484. // Refresh the RAID device list after a short delay
  485. updateRAIDArrayStatus(devname);
  486. }, 300);
  487. }
  488. },
  489. });
  490. }
  491. //Create a ticker to check for RAID sync progress
  492. function syncProgressTicker(){
  493. let syncProgressTracker = $(".need-update-raid-sync-progress");
  494. if (syncProgressTracker.length > 0){
  495. syncProgressTracker.each(function(){
  496. let devname = $(this).attr("devname");
  497. $.ajax({
  498. url: './api/raid/sync?dev=' + devname,
  499. type: 'GET',
  500. dataType: 'json',
  501. data: { devname: devname },
  502. success: function(data) {
  503. if (data.error != undefined){
  504. // The device is no longer in sync state. Hide the sync progress bar
  505. $(`.sync-progress[devname="${devname}"]`).hide();
  506. $(`.sync-progress[devname="${devname}"]`).removeClass("need-update-raid-sync-progress");
  507. }else{
  508. let progress = parseFloat(data.ResyncPercent);
  509. let total_blocks = parseInt(data.TotalBlocks);
  510. let processed_blocks = parseInt(data.CompletedBlocks);
  511. let expected_time = data.ExpectedTime;
  512. let speed = data.Speed;
  513. $(`.sync-progress[devname="${devname}"] .bar`).css('--value', progress);
  514. $(`.sync-progress[devname="${devname}"] .bar .text`).text(`${progress.toFixed(1)}%`);
  515. $(`.sync-progress[devname="${devname}"] .processed_blocks`).text(processed_blocks);
  516. $(`.sync-progress[devname="${devname}"] .total_blocks`).text(total_blocks);
  517. //$(`.sync-progress[devname="${devname}"] .ts-text.is-description .speed`).text(speed);
  518. //$(`.sync-progress[devname="${devname}"] .ts-text.is-description .expected_time`).text(expected_time);
  519. }
  520. },
  521. error: function(xhr, status, error) {
  522. console.error('Error fetching RAID sync progress:', error);
  523. }
  524. });
  525. });
  526. }
  527. }
  528. setInterval(syncProgressTicker, 5000); // Check every 5 seconds
  529. function showRAIDDetails(index) {
  530. $('.raid-details').hide(); // Hide all RAID details
  531. $(`#raid_details_${index}`).show(); // Show the selected RAID details
  532. $('.raid-array.is-active').removeClass('is-active'); // Remove active class from all menu items
  533. $(`#raid_menu_${index}`).addClass('is-active'); // Add active class to the selected menu item
  534. relocale(); // Recalculate layout
  535. }
  536. </script>