raid.html 25 KB

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