raid_new.html 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. <!-- This will be shown in a ts-modal in the raid.html-->
  2. <style>
  3. .new-raid-modal-content{
  4. max-height: 70vh;
  5. overflow-y: auto;
  6. }
  7. @media screen and (max-width: 767px) {
  8. .new-raid-modal-content {
  9. max-height: none;
  10. }
  11. }
  12. .new-raid-disk-info{
  13. cursor: pointer;
  14. }
  15. .new-raid-disk-info:hover{
  16. opacity: 0.5;
  17. }
  18. /* RAID Type space visualizer */
  19. .raidTypeSpaceVisualizer{
  20. width: 100%;
  21. display: flex;
  22. height: 42px;
  23. }
  24. .raidTypeSpaceVisualizer .bars {
  25. flex: 1; /* Occupy the remaining space */
  26. display: flex; /* Nested flex container */
  27. min-height: 2.6em;
  28. border-radius: 0.4em;
  29. overflow: hidden;
  30. background-color: var(--ts-gray-500);
  31. }
  32. .raidTypeSpaceVisualizer .bar{
  33. text-align: center;
  34. float: left;
  35. color: white;
  36. padding-top: 0.6em;
  37. }
  38. .raidTypeSpaceVisualizer .bar.protected{
  39. background-color: var(--ts-primary-500);
  40. min-width: 100px;
  41. }
  42. .raidTypeSpaceVisualizer .bar.usable{
  43. background-color: var(--ts-positive-400);
  44. min-width: 100px;
  45. }
  46. .raidTypeSpaceVisualizer .bar.wasted{
  47. background-color: var(--ts-gray-500);
  48. width: 0%;
  49. }
  50. </style>
  51. <div class="content">
  52. <div class="ts-content">
  53. <div class="ts-header" i18n>Create New RAID Array
  54. // 建立 RAID 陣列
  55. </div>
  56. </div>
  57. <div class="ts-divider"></div>
  58. <div class="ts-content new-raid-modal-content">
  59. <div class="ts-grid mobile:is-stacked" >
  60. <div class="column is-3-wide">
  61. <div class="ts-procedure is-vertical has-top-spaced-large" style="position: sticky; top: 0;">
  62. <a step="1" class="item is-active">
  63. <div class="content">
  64. <div class="indicator"></div>
  65. <div class="label" i18n>RAID Name
  66. // 陣列名稱
  67. </div>
  68. </div>
  69. </a>
  70. <a step="2" class="item">
  71. <div class="content">
  72. <div class="indicator"></div>
  73. <div class="label" i18n>Select Disks
  74. // 選擇磁碟
  75. </div>
  76. </div>
  77. </a>
  78. <a step="3" class="item">
  79. <div class="content">
  80. <div class="indicator"></div>
  81. <div class="label" i18n>Raid Type
  82. // 陣列類型
  83. </div>
  84. </div>
  85. </a>
  86. <a step="4" class="item">
  87. <div class="content">
  88. <div class="indicator"></div>
  89. <div class="label" i18n>Confirm
  90. // 確認設定
  91. </div>
  92. </div>
  93. </a>
  94. </div>
  95. </div>
  96. <div class="column is-13-wide" style="overflow-y: auto;">
  97. <div class="ts-content">
  98. <!-- Array Name -->
  99. <div class="ts-text" i18n>RAID Name
  100. // 陣列名稱
  101. </div>
  102. <div class="ts-input has-top-spaced-small">
  103. <input type="text" placeholder="my-raid" id="raid_name" i18n>
  104. <span class="ts-icon is-circle-check-icon raid_name_valid_icon" style="color: var(--ts-positive-400); display:none;"></span>
  105. </div>
  106. <div class="ts-text is-description" i18n>Only alphabet, digits, _ (underscore) and - (hyphen) are allowed
  107. // 只允許字母、數字、_ (底線) 和 - (連字符)
  108. </div>
  109. <!-- Select Disks -->
  110. <div class="ts-divider has-top-spaced-small has-bottom-spaced-large"></div>
  111. <div id="new_raid_disk_select">
  112. <div class="ts-content">
  113. <div class="ts-text is-description" i18n>Loading disks, please wait...
  114. // 正在載入磁碟,請稍候...
  115. </div>
  116. </div>
  117. </div>
  118. <div class="ts-text is-description has-top-spaced-small" i18n> Tips: For any extra disks selected, it will be used as spare disks.
  119. // 提示:選擇的任何額外磁碟將用作備用磁碟。
  120. </div>
  121. <div class="ts-wrap is-end-aligned">
  122. <button class="ts-button is-outlined" i18n>Refresh
  123. // 重新整理
  124. </button>
  125. </div>
  126. <!-- Array Type -->
  127. <div class="ts-divider has-top-spaced-small has-bottom-spaced-small"></div>
  128. <div class="ts-text" i18n>RAID Type
  129. // 陣列類型
  130. </div>
  131. <button class="ts-button is-start-icon has-top-spaced-small" data-dropdown="new_raid_type_select">
  132. <span class="ts-icon is-chevron-down-icon"></span>
  133. <span class="raid_type_selected" i18n>Select RAID Type
  134. // 選擇陣列類型
  135. </span>
  136. </button>
  137. <div class="ts-dropdown is-end-icon" id="new_raid_type_select">
  138. <button class="item raid_type" value="raid1">RAID 1 <span class="ts-icon is-star-icon" style="color: var(--ts-warning-400);"></span></button>
  139. <button class="item raid_type" value="raid6">RAID 6 <span class="ts-icon is-star-icon" style="color: var(--ts-warning-400);"></span></button>
  140. <button class="item raid_type" value="raid5">RAID 5 </button>
  141. <button class="item raid_type" value="raid0">RAID 0</button>
  142. </div>
  143. <!-- Space Estimation -->
  144. <div class="ts-divider has-top-spaced-small has-bottom-spaced-small"></div>
  145. <div class="ts-text" i18n>Usable Space
  146. // 可使用空間
  147. </div>
  148. <div class="ts-blankslate" id="capacityVisualizerInformationSlate" style="pointer-events: none; user-select: none; opacity: 0.5;">
  149. <div class="description" i18n> Select disks and RAID type to estimate usable space.
  150. // 選擇磁碟和陣列類型以估算可用空間。
  151. </div>
  152. </div>
  153. <div id="capacityVisualizer" class="ts-content" style="display: none;">
  154. <div class="raidTypeSpaceVisualizer">
  155. <div class="bars">
  156. <div class="bar usable" id="estimatedUsableSpace">0%</div>
  157. <div class="bar protected" id="estimatedProtectionSpace">0%</div>
  158. <div class="bar wasted" id="estimatedWastedSpace">0%</div>
  159. </div>
  160. </div>
  161. <div class="ts-content" style="float: right;">
  162. <div class="ts-badge is-spaced-small" style="background-color: var(--ts-positive-400); color: white;" i18n>Available Space
  163. // 可用空間
  164. </div>
  165. <div class="ts-badge is-spaced-small" style="background-color: var(--ts-primary-500); color: white;" i18n>Redundancy
  166. // 冗餘
  167. </div>
  168. <div class="ts-badge is-spaced-small" style="background-color: var(--ts-gray-500); color: white;" i18n>Unused
  169. // 未使用
  170. </div>
  171. </div>
  172. <br>
  173. </div>
  174. </div>
  175. </div>
  176. </div>
  177. </div>
  178. <div class="ts-divider"></div>
  179. <div class="ts-content is-tertiary">
  180. <div class="ts-wrap is-end-aligned">
  181. <button class="ts-button" i18n>Confirm
  182. // 確認
  183. </button>
  184. <button class="ts-button is-outlined" i18n>Cancel
  185. // 取消
  186. </button>
  187. </div>
  188. </div>
  189. <script>
  190. /* RAID name validation */
  191. $("#raid_name").on("keydown", function() {
  192. validateRaidName();
  193. });
  194. $("#raid_name").on("change", function() {
  195. validateRaidName();
  196. });
  197. function validateRaidName(){
  198. var raidName = $("#raid_name").val().trim();
  199. var isValid = /^[a-zA-Z0-9_-]+$/.test(raidName) && raidName.trim() !== "";
  200. if (raidName == ""){
  201. $("#raid_name").parent().removeClass("is-end-icon");
  202. $("a[step='1']").removeClass("is-completed").removeClass("is-active");
  203. $("a[step='2']").removeClass("is-active");
  204. $(".ts-icon.raid_name_valid_icon").hide();
  205. } else if (isValid) {
  206. $("#raid_name").parent().removeClass("is-negative");
  207. $("a[step='1']").removeClass("is-active").addClass("is-completed");
  208. $("a[step='2']").addClass("is-active");
  209. $("#raid_name").parent().addClass("is-end-icon");
  210. $(".ts-icon.raid_name_valid_icon").show();
  211. } else {
  212. $("#raid_name").parent().addClass("is-negative");
  213. $("a[step='1']").removeClass("is-completed").addClass("is-active");
  214. $("a[step='2']").removeClass("is-active");
  215. $("#raid_name").parent().removeClass("is-end-icon");
  216. $(".ts-icon.raid_name_valid_icon").hide();
  217. }
  218. }
  219. /* Disk selection */
  220. function initNewRAIDDiskList(){
  221. $.get("./api/info/list", function(data) {
  222. if (data) {
  223. var disks = data;
  224. var diskList = $("#new_raid_disk_select");
  225. diskList.empty();
  226. for (var i = 0; i < disks.length; i++) {
  227. let disk = disks[i];
  228. let partitionTable = "";
  229. let encodedDiskInfo = encodeURIComponent(JSON.stringify(disk));
  230. let diskIsMounted = false;
  231. let diskIsAlreadyRAID = false; //The disk already belongs to another RAID array
  232. // Render the partition table
  233. if (disk.partitions.length > 0) {
  234. partitionTable += `<table class="ts-table is-bordered is-striped">
  235. <thead>
  236. <tr>
  237. <th i18n>Partition Name
  238. // 分割區
  239. </th>
  240. <th i18n>Size
  241. // 大小
  242. </th>
  243. <th i18n>Type
  244. // 類型
  245. </th>
  246. <th i18n>Mount Point
  247. // 掛載點
  248. </th>
  249. </tr>
  250. </thead>
  251. <tbody>`;
  252. for (var j = 0; j < disk.partitions.length; j++) {
  253. let partition = disk.partitions[j];
  254. partitionTable += `<tr>
  255. <td>${partition.name}</td>
  256. <td>${humanFileSize(partition.size)}</td>
  257. <td>${partition.fstype || partition.blocktype}</td>
  258. <td>${partition.mountpoint || ""}</td>
  259. </tr>`;
  260. if (partition.mountpoint) {
  261. diskIsMounted = true;
  262. }
  263. if (partition.blocktype.includes("raid")) {
  264. diskIsAlreadyRAID = true;
  265. }
  266. }
  267. partitionTable += `</tbody></table>`;
  268. } else {
  269. partitionTable = `<div class="ts-text is-description" i18n>No Partitions
  270. // 無分割區
  271. </div>`;
  272. }
  273. let warningClass = "";
  274. let warningMessage = `<div class="ts-text is-negative" i18n>
  275. This disk is mounted and might be in use.
  276. // 此磁碟已被掛載,可能正在使用中。
  277. </div>`;
  278. if (diskIsMounted) {
  279. warningClass = "is-negative is-start-indicated";
  280. }
  281. let disabledClass = "";
  282. if (diskIsAlreadyRAID) {
  283. disabledClass = "is-disabled";
  284. }
  285. // Append disk information with partition table
  286. diskList.append(`<div class="ts-box ts-content has-top-spaced-small new-raid-disk-info ${warningClass} ${disabledClass}" data-disk-info='${encodedDiskInfo}'>
  287. ${diskIsMounted ? warningMessage : ""}
  288. <div class="ts-item">
  289. <div class="ts-header">${disk.model} <span class="ts-badge has-start-spaced-small">${humanFileSize(disk.size)}</span></div>
  290. <div class="ts-text is-description">
  291. /dev/${disk.name}
  292. </div>
  293. </div>
  294. <div class="has-top-spaced-small">
  295. ${partitionTable}
  296. </div>
  297. <div class="new_raid_disk_selected">
  298. <span class="ts-icon is-circle-check-icon" style="color: var(--ts-positive-400); font-size: 1.5rem;"></span>
  299. </div>
  300. </div>`);
  301. }
  302. } else {
  303. console.error("Failed to load disk info: " + data.message);
  304. }
  305. //Bind click event to each disk info
  306. $(".new-raid-disk-info").off("click").on("click", function() {
  307. var selectedDisk = $(this);
  308. var selectedIcon = selectedDisk.find(".new_raid_disk_selected");
  309. var isSelected = $(this).hasClass("selected");
  310. if (isSelected) {
  311. selectedDisk.removeClass("selected");
  312. selectedIcon.hide();
  313. } else {
  314. selectedDisk.addClass("selected");
  315. selectedIcon.show();
  316. }
  317. // Check if any disk is selected
  318. if ($(".new-raid-disk-info.selected").length > 0) {
  319. // Mark step 2 as completed and step 3 as active
  320. $("a[step='2']").removeClass("is-active").addClass("is-completed");
  321. $("a[step='3']").addClass("is-active");
  322. } else {
  323. // Restore the not complete state of the steps
  324. $("a[step='2']").removeClass("is-completed").addClass("is-active");
  325. $("a[step='3']").removeClass("is-active");
  326. }
  327. //Render the space estimation if any disk is selected
  328. renderNewRaidSpaceEstimation();
  329. });
  330. relocale();
  331. });
  332. }
  333. initNewRAIDDiskList();
  334. /* RAID type selection */
  335. $(".raid_type").click(function() {
  336. var raid_type = $(this).text();
  337. var raid_type_value = $(this).val();
  338. $(".raid_type_selected").text(raid_type);
  339. $(".raid_type_selected").val(raid_type_value);
  340. $("#new_raid_type_select").removeClass("is-active");
  341. $("#new_raid_type_select").removeAttr("data-dropdown");
  342. $("#new_raid_type_select").attr("data-dropdown", "new_raid_type_select");
  343. $(".raid_type").removeClass("is-active");
  344. $(this).addClass("is-active");
  345. $(".raid_type").removeAttr("aria-pressed");
  346. $(this).attr("aria-pressed", "true");
  347. $(".raid_type").removeAttr("aria-selected");
  348. $(this).attr("aria-selected", "true");
  349. $(".raid_type").removeAttr("aria-expanded");
  350. $(this).attr("aria-expanded", "true");
  351. $(".raid_type").removeAttr("aria-hidden");
  352. $(this).attr("aria-hidden", "false");
  353. //Write the selected type to attribute
  354. var selectedRAIDType = $(this).val();
  355. $(".raid_type_selected").attr("value", selectedRAIDType);
  356. // Mark step 3 as completed and step 2 as active
  357. $("a[step='3']").removeClass("is-active").addClass("is-completed");
  358. $("a[step='2']").addClass("is-active");
  359. renderNewRaidSpaceEstimation();
  360. });
  361. function getCurrentSelectedDisks(){
  362. var selectedDisks = [];
  363. $(".new-raid-disk-info.selected").each(function() {
  364. var diskInfo = $(this).attr("data-disk-info");
  365. var disk = JSON.parse(decodeURIComponent(diskInfo));
  366. selectedDisks.push(disk);
  367. });
  368. return selectedDisks;
  369. }
  370. function getCurrentSelectedRAIDType(){
  371. var selectedRAIDType = $(".raid_type_selected").val();
  372. if (selectedRAIDType == undefined || selectedRAIDType == "") {
  373. selectedRAIDType = "";
  374. }
  375. return selectedRAIDType;
  376. }
  377. /* Render the space estimation */
  378. function renderNewRaidSpaceEstimation(){
  379. function bytesToHumanReadable(size){
  380. var i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
  381. return +((size / Math.pow(1024, i)).toFixed(1)) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
  382. }
  383. //Check if any disk is selected and raid type is selected
  384. let selectedDisks = getCurrentSelectedDisks();
  385. let selectedRAIDType = getCurrentSelectedRAIDType();
  386. if (selectedDisks.length == 0 || selectedRAIDType == "") {
  387. $("#capacityVisualizer").hide();
  388. $("3capacityVisualizerInformationSlate").show();
  389. return;
  390. } else {
  391. $("#capacityVisualizer").show();
  392. $("#capacityVisualizerInformationSlate").hide();
  393. }
  394. console.log(selectedDisks, selectedRAIDType);
  395. //Calculate the total size of selected disks
  396. let totalSize = 0;
  397. let totalUsableSpace = 0;
  398. let totalRedundancySpace = 0;
  399. let totalWastedSpace = 0;
  400. let totalDiskCount = selectedDisks.length;
  401. let diskCountIsEnough = false;
  402. if (selectedRAIDType == "raid0"){
  403. // RAID 0: No redundancy, all space is usable
  404. // Usable space is determined by the smallest disk size * the number of disks
  405. let smallestDiskSize = 0;
  406. selectedDisks.forEach(disk => {
  407. totalSize += disk.size;
  408. if (smallestDiskSize == 0 || disk.size < smallestDiskSize) {
  409. smallestDiskSize = disk.size;
  410. }
  411. });
  412. totalUsableSpace = smallestDiskSize * totalDiskCount;
  413. totalRedundancySpace = 0;
  414. totalWastedSpace = totalSize - totalUsableSpace;
  415. if (totalDiskCount >= 2){
  416. diskCountIsEnough = true;
  417. }
  418. }else if (selectedRAIDType == "raid1"){
  419. // RAID 1: Mirroring, usable space is the size of the smallest disk
  420. let smallestDiskSize = 0;
  421. selectedDisks.forEach(disk => {
  422. totalSize += disk.size;
  423. if (smallestDiskSize == 0 || disk.size < smallestDiskSize) {
  424. smallestDiskSize = disk.size;
  425. }
  426. });
  427. totalUsableSpace = smallestDiskSize;
  428. totalRedundancySpace = smallestDiskSize * (totalDiskCount - 1);
  429. totalWastedSpace = totalSize - totalUsableSpace;
  430. if (totalDiskCount >= 2){
  431. diskCountIsEnough = true;
  432. }
  433. }else if (selectedRAIDType == "raid5"){
  434. // RAID 5: Striping with parity, usable space is total size - size of one disk
  435. let smallestDiskSize = 0;
  436. selectedDisks.forEach(disk => {
  437. totalSize += disk.size;
  438. if (smallestDiskSize == 0 || disk.size < smallestDiskSize) {
  439. smallestDiskSize = disk.size;
  440. }
  441. });
  442. totalUsableSpace = smallestDiskSize * (totalDiskCount - 1);
  443. totalRedundancySpace = smallestDiskSize;
  444. totalWastedSpace = totalSize - totalUsableSpace - totalRedundancySpace;
  445. if (totalDiskCount >= 3){
  446. diskCountIsEnough = true;
  447. }
  448. }else if (selectedRAIDType == "raid6"){
  449. // RAID 6: Striping with double parity, usable space is total size - size of two disks
  450. let smallestDiskSize = 0;
  451. selectedDisks.forEach(disk => {
  452. totalSize += disk.size;
  453. if (smallestDiskSize == 0 || disk.size < smallestDiskSize) {
  454. smallestDiskSize = disk.size;
  455. }
  456. });
  457. totalUsableSpace = smallestDiskSize * (totalDiskCount - 2);
  458. totalRedundancySpace = smallestDiskSize * 2;
  459. totalWastedSpace = totalSize - totalUsableSpace - totalRedundancySpace;
  460. if (totalDiskCount >= 4){
  461. diskCountIsEnough = true;
  462. }
  463. }else{
  464. console.error("Unknown RAID type: " + selectedRAIDType);
  465. return;
  466. }
  467. //Check if the disk count is enough for the selected RAID type
  468. if (!diskCountIsEnough){
  469. $("#capacityVisualizer").hide();
  470. $("3capacityVisualizerInformationSlate").show();
  471. $("a[step='3']").removeClass("is-completed").addClass("is-active");
  472. $("a[step='4']").removeClass("is-active");
  473. return;
  474. }
  475. //Update the visualizer
  476. let barMinWidth = 100;
  477. let usableSpacePercentage = (totalUsableSpace / totalSize) * 100;
  478. let redundancySpacePercentage = (totalRedundancySpace / totalSize) * 100;
  479. let wastedSpacePercentage = (totalWastedSpace / totalSize) * 100;
  480. $("#estimatedUsableSpace").text(bytesToHumanReadable(totalUsableSpace));
  481. $("#estimatedProtectionSpace").text(bytesToHumanReadable(totalRedundancySpace));
  482. $("#estimatedWastedSpace").text(bytesToHumanReadable(totalWastedSpace));
  483. $("#estimatedUsableSpace").css("width", usableSpacePercentage + "%");
  484. if (usableSpacePercentage == 0){
  485. $("#estimatedUsableSpace").css("min-width", "0");
  486. }else{
  487. $("#estimatedUsableSpace").css("min-width", barMinWidth + "px");
  488. }
  489. $("#estimatedProtectionSpace").css("width", redundancySpacePercentage + "%");
  490. if (redundancySpacePercentage == 0 ){
  491. $("#estimatedProtectionSpace").css("min-width", "0");
  492. }else{
  493. $("#estimatedProtectionSpace").css("min-width", barMinWidth + "px");
  494. }
  495. $("#estimatedWastedSpace").css("width", wastedSpacePercentage + "%");
  496. }
  497. </script>