|
@@ -3,21 +3,33 @@
|
|
|
background: var(--ts-positive-400) !important;
|
|
|
border: 0px solid transparent !important;
|
|
|
}
|
|
|
+
|
|
|
+ .raid-details{
|
|
|
+ position: relative;
|
|
|
+ }
|
|
|
+
|
|
|
+ .raid-array-opr-btns{
|
|
|
+ position: absolute !important;
|
|
|
+ top: 1em;
|
|
|
+ right: 1em;
|
|
|
+ }
|
|
|
+
|
|
|
+ .raid-menu-badge{
|
|
|
+ margin-top: -3px !important;
|
|
|
+ margin-left: 0.15em !important;
|
|
|
+ }
|
|
|
</style>
|
|
|
<div class="ts-content">
|
|
|
<div class="ts-container is-padded">
|
|
|
<div class="ts-grid mobile:is-stacked">
|
|
|
<div class="column is-6-wide">
|
|
|
<div id="raid_array_list" class="ts-menu is-start-icon is-separated">
|
|
|
- <a class="item">
|
|
|
- <span class="ts-icon is-user-icon"></span> 使用者
|
|
|
- </a>
|
|
|
- <a class="item is-active">
|
|
|
- <span class="ts-icon is-house-icon"></span> 首頁
|
|
|
- </a>
|
|
|
- <a class="item">
|
|
|
- <span class="ts-icon is-newspaper-icon"></span> 新聞
|
|
|
- </a>
|
|
|
+ <div class="ts-blankslate" style="pointer-events: none; user-select: none; opacity: 0.7;">
|
|
|
+ <div class="description">
|
|
|
+ <span class="ts-icon is-circle-notch-icon is-spinning"></span>
|
|
|
+ <span class="has-start-spaced-small">Loading...</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<div class="ts-divider has-top-spaced-small"></div>
|
|
|
<div class="ts-content is-center-aligned">
|
|
@@ -60,6 +72,7 @@
|
|
|
let raidDetails = renderRAIDPoolDetail(raid, index);
|
|
|
$("#raid_array_list").append(raidDetails[0]);
|
|
|
$('#raid_details').append(raidDetails[1]);
|
|
|
+ getRAIDSpaceInfoForDev(raid.DevicePath);
|
|
|
});
|
|
|
|
|
|
if (data.length == 0){
|
|
@@ -89,7 +102,19 @@
|
|
|
}
|
|
|
initRAIDDeviceList();
|
|
|
|
|
|
- function updateRAIDArrayStatus(devname){
|
|
|
+ function refreshRAIDArrayStatus(devname){
|
|
|
+ //Hide the raid details
|
|
|
+ $(`.raid-details[mdx=${devname}]`).hide();
|
|
|
+ updateRAIDArrayStatus(devname, function(data){
|
|
|
+ if (data.error == undefined){
|
|
|
+ msgbox(i18nc("raid_device_updated_succ"));
|
|
|
+ }
|
|
|
+ $(`.raid-details[mdx=${devname}]`).show();
|
|
|
+ });
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateRAIDArrayStatus(devname, callback=undefined){
|
|
|
if (devname.startsWith('/dev/')) {
|
|
|
devname = devname.slice(5);
|
|
|
}
|
|
@@ -102,9 +127,14 @@
|
|
|
// Handle error response
|
|
|
console.error('Error fetching RAID status:', data.error);
|
|
|
msgbox("Error: " + data.error);
|
|
|
+ if (callback){
|
|
|
+ callback(data);
|
|
|
+ }
|
|
|
return
|
|
|
}
|
|
|
|
|
|
+ // Update the RAID array status
|
|
|
+ // Find the corresponding menu item and details tab
|
|
|
let menuItem = $(`.raid-array[mdx="${devname}"]`);
|
|
|
let raidDetails = $(`.raid-details[mdx="${devname}"]`);
|
|
|
let index = menuItem.attr("idx");
|
|
@@ -117,6 +147,9 @@
|
|
|
raidDetails.replaceWith(domEles[1]);
|
|
|
showRAIDDetails(currentShownDetailIndex);
|
|
|
syncProgressTicker();
|
|
|
+ if (callback){
|
|
|
+ callback(data);
|
|
|
+ }
|
|
|
},
|
|
|
error: function(xhr, status, error) {
|
|
|
console.error('Error updating RAID status:', error);
|
|
@@ -124,41 +157,95 @@
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ function deleteRAIDArray(devname){
|
|
|
+ if (devname.startsWith('/dev/')) {
|
|
|
+ devname = devname.slice(5);
|
|
|
+ }
|
|
|
+
|
|
|
+ /*
|
|
|
+ //TODO: API still not ready
|
|
|
+ $.cjax({
|
|
|
+ url: './api/raid/delete',
|
|
|
+ method: 'POST',
|
|
|
+ data: { dev: devname},
|
|
|
+ success: function(data) {
|
|
|
+ if (data.error != undefined){
|
|
|
+ // Handle error response
|
|
|
+ console.error('Error deleting RAID device:', data.error);
|
|
|
+ msgbox("Error: " + data.error);
|
|
|
+ }else{
|
|
|
+ // Successfully deleted the device
|
|
|
+ console.log('RAID device deleted successfully:', data);
|
|
|
+ msgbox(i18nc("raid_device_deleted_succ"));
|
|
|
+ setTimeout(function() {
|
|
|
+ // Refresh the RAID device list after a short delay
|
|
|
+ initRAIDDeviceList();
|
|
|
+ }, 300);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ });
|
|
|
+ */
|
|
|
+ }
|
|
|
+
|
|
|
+ // Utility function to convert bytes to human-readable format
|
|
|
+ function bytesToHumanReadable(bytes) {
|
|
|
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
|
+ if (bytes === 0) return '0 B';
|
|
|
+ const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
|
|
+ return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ function getRAIDSpaceInfoForDev(devname){
|
|
|
+ if (devname.startsWith('/dev/')) {
|
|
|
+ devname = devname.slice(5);
|
|
|
+ }
|
|
|
+ let gaugeElement = $(`.raid-details[mdx=${devname}]`).find(".raid-usage-info");
|
|
|
+ let updateElement = $(`.raid-details[mdx=${devname}]`).find(".raid-total-used-space");
|
|
|
+ $.get("./api/raid/overview", function(data){
|
|
|
+ if (data.error != undefined){
|
|
|
+ // Handle error response
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (data && Array.isArray(data)) {
|
|
|
+ let raidInfo = data.find(raid => raid.Name === devname);
|
|
|
+ if (raidInfo) {
|
|
|
+ let usedPercentage = (raidInfo.UsedSize / raidInfo.TotalSize) * 100;
|
|
|
+ gaugeElement.find('.bar').css('--value', usedPercentage.toFixed(1));
|
|
|
+ gaugeElement.find('.bar .text').text(`${usedPercentage.toFixed(1)}%`);
|
|
|
+ updateElement.html(bytesToHumanReadable(raidInfo.UsedSize));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
// Function to render RAID pool details
|
|
|
// This function creates the HTML structure for each RAID pool
|
|
|
// return the DOM element for the side menu and detail tab
|
|
|
function renderRAIDPoolDetail(raid, index){
|
|
|
- // Utility function to convert bytes to human-readable format
|
|
|
- function bytesToHumanReadable(bytes) {
|
|
|
- const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
|
- if (bytes === 0) return '0 B';
|
|
|
- const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
|
|
|
- return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + sizes[i];
|
|
|
- }
|
|
|
-
|
|
|
// Add a new menu item for each RAID array
|
|
|
let mdX = raid.DevicePath.split('/').pop();
|
|
|
let isSyncing = false;
|
|
|
let isResyncPending = false;
|
|
|
let icon = '';
|
|
|
if (raid.State.includes('clean') && !raid.State.includes('sync')) {
|
|
|
- icon = '<span class="ts-icon is-check-icon" style="color: var(--ts-positive-500);"></span>';
|
|
|
+ icon = '<span class="ts-icon is-check-icon" style="color: var(--ts-positive-500); font-size: 2em;"></span>';
|
|
|
} else if (raid.State.includes('sync')) {
|
|
|
isSyncing = true;
|
|
|
if (raid.State.includes('resyncing') && raid.State.includes('PENDING')) {
|
|
|
//Syncing is pending
|
|
|
isResyncPending = true;
|
|
|
- icon = '<span class="ts-icon is-rotate-icon" style="color: var(--ts-positive-500);"></span>';
|
|
|
+ icon = '<span class="ts-icon is-rotate-icon" style="color: var(--ts-positive-500); font-size: 2em;"></span>';
|
|
|
}else{
|
|
|
- icon = '<span class="ts-icon is-spinning is-rotate-icon" style="color: var(--ts-positive-500);"></span>';
|
|
|
+ icon = '<span class="ts-icon is-spinning is-rotate-icon" style="color: var(--ts-positive-500); font-size: 2em;"></span>';
|
|
|
}
|
|
|
|
|
|
} else if (raid.State.includes('degraded')) {
|
|
|
- icon = '<span class="ts-icon is-triangle-exclamation-icon" style="color: var(--ts-warning-600);"></span>';
|
|
|
+ icon = '<span class="ts-icon is-triangle-exclamation-icon" style="color: var(--ts-warning-600); font-size: 2em;"></span>';
|
|
|
} else if (raid.State.includes('fail')) {
|
|
|
- icon = '<span class="ts-icon is-circle-xmark-icon" style="color: var(--ts-negative-500);"></span>';
|
|
|
+ icon = '<span class="ts-icon is-circle-xmark-icon" style="color: var(--ts-negative-500); font-size: 2em;"></span>';
|
|
|
} else {
|
|
|
- icon = '<span class="ts-icon is-question-icon" style="color: var(--ts-gray-500);"></span>';
|
|
|
+ icon = '<span class="ts-icon is-question-icon" style="color: var(--ts-gray-500); font-size: 2em;"></span>';
|
|
|
}
|
|
|
|
|
|
// Add a new menu item for each RAID array
|
|
@@ -167,7 +254,8 @@
|
|
|
${icon}
|
|
|
<div class="ts-content is-dense">
|
|
|
<div>
|
|
|
- <span class="ts-text is-heavy">${raid.DevicePath}</span> | ${raid.RaidLevel.toUpperCase()}
|
|
|
+ <span class="ts-text is-heavy">${raid.DevicePath}</span>
|
|
|
+ <span class="ts-badge is-secondary raid-menu-badge">${raid.RaidLevel.toUpperCase()}</span>
|
|
|
</div>
|
|
|
<div class="ts-text is-tiny has-top-spaced-small">
|
|
|
${raid.Name}
|
|
@@ -183,7 +271,7 @@
|
|
|
<div class="ts-content is-padded">
|
|
|
<div class="ts-header is-start-icon">
|
|
|
${icon}
|
|
|
- ${raid.DevicePath} | ${raid.RaidLevel.toUpperCase()}
|
|
|
+ ${raid.DevicePath} <span class="ts-badge is-start-spaced">${raid.RaidLevel.toUpperCase()}</span>
|
|
|
</div>
|
|
|
<div class="ts-text is-description">
|
|
|
${raid.UUID}<br>
|
|
@@ -204,6 +292,8 @@
|
|
|
// 建立時間
|
|
|
</span>: <span>${new Date(raid.CreationTime).toLocaleString('en-US', { timeZone: 'UTC' })}</span><br>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- Disk States Summary -->
|
|
|
<table class="ts-table is-single-line has-top-spaced-large">
|
|
|
<thead>
|
|
|
<tr>
|
|
@@ -242,13 +332,55 @@
|
|
|
</tr>
|
|
|
</tbody>
|
|
|
</table>
|
|
|
+
|
|
|
+ <!-- Usage Counters -->
|
|
|
+ <div class="ts-grid is-evenly-divided has-top-spaced-large">
|
|
|
+ <div class="column">
|
|
|
+ <div class="ts-wrap is-middle-aligned">
|
|
|
+ <div class="ts-gauge is-small is-circular raid-usage-info">
|
|
|
+ <div class="bar" style="--value: 0">
|
|
|
+ <div class="text">0%</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <div class="ts-text is-bold" i18n> Used Space
|
|
|
+ // 已使用空間
|
|
|
+ </div>
|
|
|
+ <span class="raid-total-used-space">???</span> / ${bytesToHumanReadable(raid.ArraySize * 1024)}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="column">
|
|
|
+ <div class="ts-wrap is-middle-aligned">
|
|
|
+ <div class="ts-gauge is-small is-circular">
|
|
|
+ <div class="bar" style="--value: 100">
|
|
|
+ <div class="text">---</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <div class="ts-text is-bold">傳輸</div>
|
|
|
+ 0 B 已使用
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div class="ts-box is-padded has-top-spaced-small">
|
|
|
+ <div class="has-top-spaced-small">
|
|
|
+ ${getRAIDChildDiskElement(raid.DeviceInfo)}
|
|
|
+ </div>
|
|
|
+ <!-- Operations -->
|
|
|
+ <div class="raid-array-opr-btns">
|
|
|
<div class="ts-content">
|
|
|
-
|
|
|
+ <button class="ts-button is-circular is-icon" onclick="refreshRAIDArrayStatus('${mdX}');">
|
|
|
+ <span class="ts-icon is-arrows-rotate-icon"></span>
|
|
|
+ </button>
|
|
|
+ <button class="ts-button is-circular is-icon is-negative" onclick="deleteRAIDArray('${mdX}');">
|
|
|
+ <span class="ts-icon is-trash-icon"></span>
|
|
|
+ </button>
|
|
|
</div>
|
|
|
- </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
`;
|
|
|
return [menuItem, raidDetails];
|
|
@@ -300,6 +432,67 @@
|
|
|
</button>`
|
|
|
}
|
|
|
|
|
|
+ // DOM elements for child disks
|
|
|
+ function getRAIDChildDiskElement(raidDeviceInfo){
|
|
|
+ if (raidDeviceInfo.length == 0 || raidDeviceInfo == null){
|
|
|
+ return `<div class="ts-blankslate" style="pointer-events: none; user-select: none; opacity: 0.7;">
|
|
|
+ <div class="description" i18n>No assigned disks
|
|
|
+ // 沒有分配的磁碟
|
|
|
+ </div>
|
|
|
+ </div>`;
|
|
|
+ }
|
|
|
+
|
|
|
+ //Render each disk
|
|
|
+ let result = '';
|
|
|
+ for (let i = 0; i < raidDeviceInfo.length; i++) {
|
|
|
+ let disk = raidDeviceInfo[i];
|
|
|
+ if (disk.RaidDevice == -1){
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ let elementUUID = "raid_child_" + disk.DevicePath.replace(/\//g, "_");
|
|
|
+ let diskSdx = disk.DevicePath.split('/').pop();
|
|
|
+ let thisDiskInfo = getDiskInfoDevicePath(diskSdx); //Try to load disk info from cache
|
|
|
+ if (thisDiskInfo == null){
|
|
|
+ //Make a request to get the disk info
|
|
|
+ let thisDiskInfo = null;
|
|
|
+ $.ajax({
|
|
|
+ url: `./api/info/disk/${diskSdx}`,
|
|
|
+ type: 'GET',
|
|
|
+ dataType: 'json',
|
|
|
+ success: function(data) {
|
|
|
+ if (data.error != undefined){
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ thisDiskInfo = data;
|
|
|
+ $(`.raid-child-disk[diskid='${elementUUID}']`).find(".raid-disk-name").text(thisDiskInfo.model);
|
|
|
+ },
|
|
|
+ error: function(xhr, status, error) {
|
|
|
+ console.error('Error fetching disk info:', error);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ result += `
|
|
|
+ <div class="ts-box is-padded has-bottom-spaced-small raid-child-disk" diskid="${elementUUID}">
|
|
|
+ <div class="ts-content">
|
|
|
+ <div>
|
|
|
+ <span class="ts-badge is-secondary has-end-spaced-small" style="margin-top: -0.3em;">${disk.DevicePath}</span>
|
|
|
+ <span class="ts-text is-heavy raid-disk-name">Raid Device ${disk.RaidDevice}</span>
|
|
|
+ </div>
|
|
|
+ <div class="ts-text is-tiny has-top-spaced-small">
|
|
|
+ <div class="has-start-spaced-small">
|
|
|
+ <span i18n> State
|
|
|
+ // 狀態
|
|
|
+ </span>: ${disk.State.join(', ')}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
// Function to activate a finished RAID sync
|
|
|
// Will set the RAID device to -readwrite state
|
|
|
function activateSyncPendingDisk(devname){
|