web-app/scripts.js
window.chartColors = {
red: 'rgba(222, 66, 91, 1)',
orange: 'rgba(255, 159, 64, 1)',
yellow: 'rgba(255, 236, 152, 1)',
green: 'rgba(72, 143, 49, 1)',
blue: 'rgba(54, 162, 235, 1)',
purple: 'rgba(153, 102, 255, 1)',
grey: 'rgba(201, 203, 207, 1)',
};
window.chartColorsSemi = {
red: 'rgba(222, 66, 91, 0.6)',
orange: 'rgba(255, 159, 64, 0.6)',
yellow: 'rgba(255, 236, 152, 0.6)',
green: 'rgba(72, 143, 49, 0.6)',
blue: 'rgba(54, 162, 235, 0.6)',
purple: 'rgba(153, 102, 255, 0.6)',
grey: 'rgba(201, 203, 207, 0.6)',
};
function defer(method) {
if (window.jQuery) {
$(function () {
method();
});
} else {
setTimeout(function() { defer(method) }, 50);
}
}
let urlParams = {};
function parse_url_params() {
let match,
pl = /\+/g, // Regex for replacing addition symbol with a space
search = /([^&=]+)=?([^&]*)/g,
decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); },
query = window.location.search.substring(1);
urlParams = {};
while (match = search.exec(query)) {
urlParams[decode(match[1])] = decode(match[2]);
}
}
let tab_changed_functions = {
id_l1_status_tab: function () { loadStatus(); topTabChanged(); },
id_l2_status_logs_tab: loadStatusLogs,
id_l2_status_queue_tab: loadStatusQueue,
id_l2_status_past_tasks_tab: loadStatusPastTasks,
id_l2_status_fsck_tab: loadStatusFsckReport,
id_l2_status_balance_tab: loadStatusBalanceReport,
id_l1_spool_tab: loadStoragePool,
id_l1_trashman_tab: loadTrashmanContent,
id_l2_actions_trash_tab: loadActionsTrashContent,
};
function topTabChanged() {
let visible_tab_id = $('.navbar-nav .active').attr('aria-controls');
let $target = $('#' + visible_tab_id).find('.nav-tabs .active');
if ($target.length > 0) {
// Will load data from the currently visible sub-tab
changedTab($target[0]);
} else {
changedTab($('.navbar-nav .active')[0]);
}
}
defer(function() {
$(function () {
$('.nav .nav-link').on('shown.bs.tab', function (evt) {
changedTab(evt.target);
});
$(window).on('popstate', function (evt) {
parse_url_params();
for (let selected_tab of evt.originalEvent.state.selected_tabs) {
skip_changed_tab_event = true;
$('#' + selected_tab).tab('show');
}
});
$(window).resize(function() {
resizeSPDrivesUsageGraphs();
});
$('[data-toggle="tooltip"]').tooltip();
checkSambaConfig();
$('#past-tasks-table').DataTable({
processing: true,
serverSide: true,
ajax: './?ajax=get_status_past_tasks',
columns: [
{ data: 'id', orderSequence: ['desc', 'asc'] },
{ data: 'event_date', orderSequence: ['desc', 'asc'] },
{ data: 'action' },
{ data: 'share' },
{ data: 'full_path' }
],
order: [[0, 'desc']],
pageLength: 10,
responsive: true,
"fnRowCallback": function(nRow, aData, iDisplayIndex, iDisplayIndexFull) {
$(nRow).addClass(aData.action);
},
});
$('#trashman-table').DataTable({
processing: true,
serverSide: true,
ajax: './?ajax=get_trashman_content&dir=' + encodeURIComponent($('#trashman-current-dir').val()),
columns: [
{ data: 'path' },
{ data: 'size', orderSequence: ['desc', 'asc'] },
{ data: 'copies', orderSequence: ['desc', 'asc'], width: '110px' },
{ data: 'modified', width: '140px' },
{ data: 'actions', sorting: false }
],
order: [[0, 'asc']],
paging: false,
searching: false,
info: false,
responsive: true,
"fnRowCallback": function(nRow, aData, iDisplayIndex, iDisplayIndexFull) {
$(nRow).data('path', aData.raw_path);
$(nRow).data('copies', aData.copies);
$(nRow).data('size', aData.size);
$(nRow).data('size-delete', aData.size_delete);
$(nRow).data('copies-restore', aData.copies_restore);
$(nRow).data('size-restore', aData.size_restore);
},
}).on('draw', function () {
// Adjust columns size
$('#trashman-table').DataTable().columns.adjust();
});
loadStatus();
topTabChanged();
});
});
function trashmanGoToParent() {
let current_dir = $('#trashman-current-dir').val().split('/');
current_dir.pop();
let new_dir = current_dir.join('/');
trashmanDataForDir(new_dir);
}
function trashmanEnterFolder(btn) {
let folder = $(btn).closest('tr').data('path');
let current_dir = $('#trashman-current-dir').val();
let new_dir = current_dir + '/' + folder;
trashmanDataForDir(new_dir);
}
function trashmanDelete(btn) {
let $row = $(btn).closest('tr');
let folder = $('#trashman-current-dir').val() + '/' + $row.data('path');
folder = folder.substring(2);
let $modal = $('#modal-trashman-delete');
$modal.find('.copies').text($row.data('copies'));
$modal.find('.path').text(folder);
$modal.find('.size').html($row.data('size-delete'));
$modal.find('.btn-danger').data('folder', folder);
$modal.modal('show');
}
function trashmanConfirmDelete(btn) {
let folder = $(btn).data('folder');
ajaxCallFromButton(btn, 'delete_from_trash', 'folder=' + encodeURIComponent(folder), 'Deleting...', 'Deleted', 'Delete forever', function (data, $button) {
$(btn).closest('.modal').modal('hide');
let dir = $('#trashman-current-dir').val();
trashmanDataForDir(dir);
}, 0);
}
function trashmanRestore(btn) {
let $row = $(btn).closest('tr');
let folder = $('#trashman-current-dir').val() + '/' + $row.data('path');
folder = folder.substring(2);
let $modal = $('#modal-trashman-restore');
$modal.find('.copies').text($row.data('copies-restore'));
$modal.find('.path').text(folder);
$modal.find('.size').html($row.data('size-restore'));
$modal.find('.btn-success').data('folder', folder);
$modal.modal('show');
}
function trashmanConfirmRestore(btn) {
let folder = $(btn).data('folder');
ajaxCallFromButton(btn, 'restore_from_trash', 'folder=' + encodeURIComponent(folder), 'Restoring...', 'Restored', 'Restore', function (data, $button) {
$(btn).closest('.modal').modal('hide');
let dir = $('#trashman-current-dir').val();
trashmanDataForDir(dir);
}, 0);
}
function trashmanDataForDir(new_dir) {
$('#trashman-current-dir').val(new_dir);
// ajax.url is normally only set when the Datatable is initialized; we need to update it because it needs to use new_dir
$('#trashman-table').DataTable().ajax.url('./?ajax=get_trashman_content&dir=' + encodeURIComponent(new_dir));
// Reload data from server
$('#trashman-table').DataTable().ajax.reload();
// Show/hide current folder, based of if we're showing the root folder or not
if (new_dir === '.') {
$('#trashman-current-dir-header').hide();
} else {
$('#trashman-current-dir-header').show();
$('#trashman-current-dir-header').text(' | ' + new_dir.substring(2));
}
}
function resizeSPDrivesUsageGraphs() {
$('.table-sp-drives').each(function(i, el) {
let $table = $(el);
let width_left = $table.closest('.col').width() - 30; // 15+15 padding left-right
let num_rows = $table.find('tr:nth-child(1)').find('th').length;
for (let i=1; i<num_rows; i++) {
width_left -= $table.find('td:nth-child('+i+')').width();
width_left -= 12; // 6+6 padding left-right
}
$table.find('.sp-bar').each(function(i, el) {
let width = $(el).data('width') * width_left;
$(el).css('width', width + 'px');
if ($(el).hasClass('treemap')) {
let $table = $(el).closest('table');
let color = getTreemapColor($(el).data('value'), $table.data('min-value'), $table.data('max-value'));
$(el).css('background-color', color);
}
});
});
}
function toggleSambaShareGreyholeEnabled(el) {
let $el = $(el);
let name = $el.attr('name');
let value = $el.val();
let share_name = $el.data('sharename')
let num_copies_field_name = name.replace('gh_enabled', 'num_copies');
let $num_copies_field = $('[name="' + num_copies_field_name + '"]');
let vfs_objects_field_name = name.replace('gh_enabled', 'vfs_objects');
let $vfs_objects_field_name = $('[name="' + vfs_objects_field_name + '"]');
let vfs_objects = $vfs_objects_field_name.val();
let dfree_command;
if (value === 'yes') {
$num_copies_field.val('1');
if (vfs_objects.indexOf('greyhole') < 0) {
vfs_objects = (vfs_objects + " greyhole").trim();
$vfs_objects_field_name.val(vfs_objects);
}
dfree_command = '/usr/bin/greyhole-dfree';
} else {
$num_copies_field.val('0');
vfs_objects = vfs_objects.replace('greyhole', '').replace(' ', ' ').trim();
$vfs_objects_field_name.val(vfs_objects);
if (vfs_objects === '') {
vfs_objects = '___REMOVE___';
}
dfree_command = '___REMOVE___';
}
ajax_value_changed($el, 'smb.conf:[' + share_name + ']dfree_command', dfree_command, function () {
ajax_value_changed(null, 'smb.conf:[' + share_name + ']vfs_objects', vfs_objects, function () {
config_value_changed($num_copies_field);
});
});
}
function config_value_changed(el, success) {
let $el = $(el);
let name = $el.attr('name');
let new_value = $el.val();
if ($('[name="' + name + '_suffix"]').length > 0) {
let $el2 = $('[name="' + name + '_suffix"]');
new_value = new_value + $el2.val();
} else if (name.indexOf('_suffix') > -1) {
name = name.substr(0, name.length-7);
let $el2 = $('[name="' + name + '"]');
new_value = $el2.val() + new_value;
}
if (name.indexOf('drive_selection_algorithm') === 0) {
name = 'drive_selection_algorithm';
new_value = get_forced_groups_config();
}
ajax_value_changed($el, name, new_value, success);
}
function ajax_value_changed($el, name, value, success) {
// console.log(name + " = " + value);
$.ajax({
type: 'POST',
url: './?ajax=config',
data: 'name=' + encodeURIComponent(name) + '&value=' + encodeURIComponent(value),
success: function(data, textStatus, jqXHR) {
if (data.result === 'success') {
if ($el) {
$el.attr('data-toggle', 'tooltip').attr('data-placement', 'bottom').attr('title', 'New value saved').tooltip({trigger: 'manual'}).tooltip('show');
setTimeout(function() { $el.tooltip('hide'); }, 2*1000);
}
if (data.config_hash === last_known_config_hash) {
$('#needs-daemon-restart').hide();
} else {
$('#needs-daemon-restart').show();
}
if (data.config_hash_samba === last_known_config_hash_samba) {
$('#needs-samba-restart').hide();
} else {
$('#needs-samba-restart').show();
}
if (typeof success !== 'undefined') {
success();
}
} else {
if (data.result === 'error') {
alert(data.message);
} else {
alert("An error occurred. Check your logs for details.");
}
}
},
});
}
function ajaxCallFromButton(button, ajax_action, data, onbusy_btn_text, onsuccess_btn_text, final_btn_text, onsuccess, onsuccess_delay) {
let $button = $(button);
let original_btn_text = $button.text();
$button.text(onbusy_btn_text).prop('disabled', true);
$.ajax({
type: 'POST',
url: './?ajax=' + ajax_action,
data: data,
success: function(data, textStatus, jqXHR) {
if (data.result === 'success') {
if (onsuccess_delay > 0) {
$button.text(onsuccess_btn_text);
}
$button.toggleClass('btn-primary').toggleClass('btn-success');
setTimeout(function() {
$button.text(final_btn_text).prop('disabled', false).toggleClass('btn-primary').toggleClass('btn-success');
onsuccess(data, $button);
}, onsuccess_delay*1000);
} else {
if (data.result === 'error') {
alert(data.message);
} else {
alert("An error occurred. Check your logs for details.");
}
$button.text(original_btn_text).prop('disabled', false);
}
},
});
}
function restartDaemon(button) {
ajaxCallFromButton(button, 'daemon', 'action=restart', 'Restarting...', 'Restarted', 'Restart', function (data, $button) {
last_known_config_hash = data.config_hash;
$('#needs-daemon-restart').hide();
}, 3);
}
function restartSamba(button) {
ajaxCallFromButton(button, 'samba', 'action=restart', 'Restarting...', 'Restarted', 'Restart', function (data, $button) {
last_known_config_hash_samba = data.config_hash_samba;
$('#needs-samba-restart').hide();
}, 3);
}
function get_forced_groups_config() {
if ($('[name="drive_selection_algorithm_forced"]:checked').val() === 'no') {
$('.forced_toggleable').closest('.form-group').hide();
return $('[name="drive_selection_algorithm"]:checked').val();
}
$('.forced_toggleable').closest('.form-group').show();
let groups = [];
for (let i=0; i<100; i++) {
let num = $('[name="drive_selection_algorithm_forced['+i+'][num]"]').val();
let group = $('[name="drive_selection_algorithm_forced['+i+'][group]"]').val();
if (typeof num !== 'undefined' && num !== '' && typeof group !== 'undefined' && group !== '') {
if (num !== 'all') {
num += 'x';
} else {
num += ' ';
}
groups.push(num + group);
}
}
return 'forced (' + groups.join(', ') + ') ' + $('[name="drive_selection_algorithm"]:checked').val();
}
defer(function(){ get_forced_groups_config(); });
function bytes_to_human(bytes) {
let units = 'B';
if (Math.abs(bytes) > 1024) {
bytes /= 1024;
units = 'KiB';
}
if (Math.abs(bytes) > 1024) {
bytes /= 1024;
units = 'MiB';
}
if (Math.abs(bytes) > 1024) {
bytes /= 1024;
units = 'GiB';
}
if (Math.abs(bytes) > 1024) {
bytes /= 1024;
units = 'TiB';
}
let decimals = (Math.abs(bytes) > 100 ? 0 : (Math.abs(bytes) > 10 ? 1 : 2));
return parseFloat(bytes).toFixed(decimals) + ' ' + units;
}
function drawPieChartStorage(ctx, stats) {
let dataset_used = [];
let dataset_trash = [];
let dataset_free = [];
let drives = [];
console.log(stats);
for (let sp_drive in stats) {
let stat = stats[sp_drive];
if (sp_drive === 'Total') {
continue;
}
drives.push(sp_drive);
dataset_used.push(stat.used_space - stat.trash_size);
dataset_trash.push(stat.trash_size);
dataset_free.push(stat.free_space);
}
console.log(dataset_used);
let dataset_all_drives = dataset_used.concat(dataset_trash).concat(dataset_free);
let labels_all_drives = [];
let colors_all_drives = [];
for (let i in dataset_used) {
let v = dataset_used[i];
labels_all_drives.push(drives[i] + " Used: " + bytes_to_human(v * 1024));
colors_all_drives.push(window.chartColorsSemi.red);
}
for (let i in dataset_trash) {
let v = dataset_trash[i];
labels_all_drives.push(drives[i] + " Trash: " + bytes_to_human(v * 1024));
colors_all_drives.push(window.chartColorsSemi.yellow);
}
for (let i in dataset_free) {
let v = dataset_free[i];
labels_all_drives.push(drives[i] + " Free: " + bytes_to_human(v * 1024));
colors_all_drives.push(window.chartColorsSemi.green);
}
let stat = stats['Total'];
let total = stat.used_space + stat.free_space;
let labels_summary = [
'Used: ' + bytes_to_human(stat.used_space * 1024 - stat.trash_size * 1024),
'Trash: ' + bytes_to_human(stat.trash_size * 1024),
'Free: ' + bytes_to_human(stat.free_space * 1024)
];
console.log(dataset_all_drives);
console.log(stat.used_space);
new Chart(ctx, {
type: 'pie',
data: {
datasets: [
{
// "Sum" dataset needs to appear first, for Legend to appear correctly
weight: 0,
data: [stat.used_space - stat.trash_size, stat.trash_size, stat.free_space],
backgroundColor: [
window.chartColors.red,
window.chartColors.yellow,
window.chartColors.green
],
labels: labels_summary,
},
{
weight: 50,
data: dataset_all_drives,
backgroundColor: colors_all_drives,
labels: labels_all_drives
},
{
weight: 50,
data: [stat.used_space - stat.trash_size, stat.trash_size, stat.free_space],
backgroundColor: [
window.chartColors.red,
window.chartColors.yellow,
window.chartColors.green
],
labels: labels_summary,
},
],
labels: labels_summary
},
options: {
maintainAspectRatio: false,
cutoutPercentage: 20,
responsive: true,
responsiveAnimationDuration: 400,
legend: {
position: 'right',
labels: { fontColor: dark_mode_enabled ? 'white' : '#666' },
},
tooltips: {
callbacks: {
label: function (tooltipItem, data) {
var label = data.datasets[tooltipItem.datasetIndex].labels[tooltipItem.index] || '';
if (label) {
label += ' = ';
}
let value = data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index];
var percentage = Math.round(value / total * 100);
label += percentage + "%";
return label;
}
}
}
}
});
}
function drawPieChartDiskUsage(ctx, du_stats) {
let dataset = [];
let labels = [];
let colors = [];
let paths = [];
let avail_colors = ['#003f5c','#58508d','#bc5090','#ff6361','#ffa600'];
for (let i in du_stats) {
let row = du_stats[i];
dataset.push(parseFloat(row.size));
paths.push(row.file_path + ':' + row.depth);
colors.push(avail_colors[i % avail_colors.length]);
let file_path = row.file_path;
labels.push(file_path.split('/').pop() + ": " + bytes_to_human(row.size));
}
function selectAtIndex(index) {
let path = paths[index].split(':');
let next_level = parseInt(path[1]) + 1;
path = path[0];
document.cookie = "back_to_url=" + location.href + "; path=/; expires=Thu, 1 Sep 2050 12:00:00 UTC";
location.href = './du/?level=' + next_level + '&path=' + encodeURIComponent('/' + path);
}
new Chart(ctx, {
type: 'pie',
data: {
datasets: [
{
data: dataset,
backgroundColor: colors,
},
],
labels: labels
},
options: {
cutoutPercentage: 20,
responsive: true,
responsiveAnimationDuration: 400,
legend: {
position: 'right',
labels: { fontColor: dark_mode_enabled ? 'white' : '#666' },
onClick: function (e, legendItem) {
selectAtIndex(legendItem.index);
},
},
tooltips: {
callbacks: {
label: function(tooltipItem, data) {
return data.labels[tooltipItem.index];
}
}
},
onClick: function (e, elements) {
if (elements.length === 0) {
return;
}
selectAtIndex(elements[0]._index);
}
}
});
}
function getTreemapColor(value, min, max) {
value = parseInt(value);
min = parseInt(min);
max = parseInt(max);
let largest, smallest;
if (dark_mode_enabled) {
largest = [72, 143, 49];
smallest = [222, 66, 91];
} else {
largest = [72, 143, 49];
smallest = [222, 66, 91];
}
let middle = [235, 235, 235];
let range = max - min;
if (range === 0) {
range = value;
} else {
value -= min;
}
let where = value / range;
let from, to;
if (where > 0.5) {
where = 2 * (where - 0.5);
from = middle;
to = largest;
} else {
where = 2 * where;
from = smallest;
to = middle;
}
let r = from[0] + (to[0] - from[0]) * where;
let g = from[1] + (to[1] - from[1]) * where;
let b = from[2] + (to[2] - from[2]) * where;
return 'rgb(' + Math.round(r) + ', ' + Math.round(g) + ',' + Math.round(b) + ')';
}
function drawTreeMapDiskUsage(ctx, du_stats) {
let dataset = [];
let paths = [];
let max = 0, min = null;
du_stats.sort(function (a, b) {
return (a.size === b.size ? 0 : ( a.size > b.size ? -1 : 1));
});
for (let i in du_stats) {
let row = du_stats[i];
row.human_size = bytes_to_human(row.size);
row.label = row.file_path.split('/').pop();
row.full_label = row.label + " = " + row.human_size;
paths.push(row.file_path + ':' + row.depth);
dataset.push(row);
if (row.size > max) {
max = row.size;
}
if (min === null || row.size < min) {
min = row.size;
}
}
function selectAtIndex(index) {
let path = paths[index].split(':');
let next_level = parseInt(path[1]) + 1;
path = path[0];
if (path === 'Files') {
return;
}
location.href = './?level=' + next_level + '&path=' + encodeURIComponent(path);
}
new Chart(ctx, {
type: 'treemap',
data: {
datasets: [
{
tree: dataset,
key: 'size',
groups: ['full_label'],
backgroundColor: function(ctx) {
return getTreemapColor(dataset[ctx.dataIndex].size, min, max);
},
fontColor: 'black',
fontFamily: 'OpenSans',
fontSize: 14,
spacing: 0.1,
borderWidth: 2,
borderColor: "rgba(180,180,180, 0.15)"
},
],
},
options: {
maintainAspectRatio: false,
responsive: true,
responsiveAnimationDuration: 400,
legend: {
display: false,
},
tooltips: {
callbacks: {
title: function (item, data) {
return dataset[item[0].index].label;
},
label: function (item, data) {
return dataset[item.index].human_size;
}
}
},
onClick: function (e, elements) {
if (elements.length === 0) {
return;
}
selectAtIndex(elements[0]._index);
}
}
});
}
function toggleDarkMode() {
dark_mode_enabled = !dark_mode_enabled;
document.cookie = "darkmode=" + (dark_mode_enabled ? '1' : '0') + "; path=/; expires=Thu, 1 Sep 2050 12:00:00 UTC";
location.reload();
}
function addStoragePoolDrive(button) {
let $modal = $(button).closest('.modal');
let sp_drive = $modal.find('[name=storage_pool_drive]').val();
$modal.find('input, select').each(function(index, el) {
$(el).attr('name', $(el).attr('name').replace('__new__', sp_drive));
});
// This will save all values as a single line: "storage_pool_drive = /mnt/hddX/gh, min_free: 10gb"
config_value_changed($modal.find('select'), function() {
// Success
location.reload();
});
}
function addSambaUser(button) {
let $button = $(button);
let $modal = $button.closest('.modal');
let username = $modal.find('[name=samba_username]').val();
let password = $modal.find('[name=samba_password]').val();
let data = 'action=add_user&username=' + encodeURIComponent(username) + '&password=' + encodeURIComponent(password);
ajaxCallFromButton(button, 'samba', data, 'Creating...', 'User Created', 'Reloading...', function (data, $button) {
$button.prop('disabled', true);
location.reload();
}, 3);
}
function updateSambaSharePath(el) {
let share_name = $(el).val();
let $el_path = $('[name=samba_share_path]');
let path = $el_path.val().replace('...', share_name);
$el_path.val(path);
}
function addSambaShare(button) {
let $button = $(button);
let $modal = $button.closest('.modal');
let share_name = $modal.find('[name=samba_share_name]').val();
let share_path = $modal.find('[name=samba_share_path]').val();
let share_options = $modal.find('[name=samba_share_options]').val();
let data = 'action=add_share&name=' + encodeURIComponent(share_name) + '&path=' + encodeURIComponent(share_path) + '&options=' + encodeURIComponent(share_options);
ajaxCallFromButton(button, 'samba', data, 'Creating...', 'Share Created', 'Reloading...', function (data, $button) {
$button.prop('disabled', true);
location.reload();
}, 3);
}
function getPageTitle() {
let page_title = ['Greyhole Admin'];
[$active_page_link, $sub_page_active_link] = getActiveLinks();
let page_name = $active_page_link.text();
page_title.push(page_name);
if ($sub_page_active_link.length) {
let subpage_name = $sub_page_active_link.text();
page_title.push(subpage_name);
}
return page_title.join(' | ');
}
function getActiveLinks() {
let $active_page_link = $('.nav[data-name=page] .nav-link.active');
let $visible_content = $($active_page_link.attr('href'));
let $sub_page_active_link = $visible_content.find('.nav .nav-link.active');
return [$active_page_link, $sub_page_active_link];
}
var skip_changed_tab_event = false;
function changedTab(el, first, replace) {
if (skip_changed_tab_event) {
skip_changed_tab_event = false;
return;
}
// console.log("changedTab()", $(el).prop('id'));
$(el).blur();
[$active_page_link, $sub_page_active_link] = getActiveLinks();
let selected_tab = $active_page_link.attr('id');
let selected_tabs = [selected_tab];
let url = './?page=' + encodeURIComponent($active_page_link.attr('id'));
if ($sub_page_active_link.length) {
let param_name = $sub_page_active_link.closest('.nav').data('name');
url += '&' + param_name + '=' + encodeURIComponent($sub_page_active_link.attr('id'));
selected_tabs.push($sub_page_active_link.attr('id'));
}
if (!first) {
history.pushState({selected_tabs: selected_tabs}, null, url);
}
let title = getPageTitle();
if (document.title !== title) {
document.title = title;
}
if (replace) {
history.replaceState({selected_tabs: selected_tabs}, null, url);
}
parse_url_params();
if ($(el).prop('id') in tab_changed_functions) {
tab_changed_functions[$(el).prop('id')]();
}
}
function donate() {
$('#id_l1_ghconfig_tab').tab('show'); // Greyhole Config
$('#id_l2_ghconfig_794df3791a8c800841516007427a2aa3_tab').tab('show'); // License
}
function goto_remove_drive() {
$('#id_l1_actions_tab').tab('show'); // Actions
$('#id_l2_actions_removedrive_tab').tab('show'); // Remove Drive
}
function donationComplete(el) {
let $el = $(el);
let email = $el.val();
ajaxCallFromButton(null, 'donate', 'email=' + encodeURIComponent(email), 'Saving...', null, 'Saved', function (data, $button) {
if ($el) {
$el.attr('data-toggle', 'tooltip').attr('data-placement', 'bottom').attr('title', 'Thank you!').tooltip({trigger: 'manual'}).tooltip('show');
setTimeout(function() { $el.tooltip('hide'); location.reload(); }, 3*1000);
}
}, 0);
}
function checkSambaConfig() {
let wide_link_config = $('[name=' + $.escapeSelector('smb.conf:[global]wide_links') + ']:checked').val();
if (wide_link_config === 'no') {
$('[name=' + $.escapeSelector('smb.conf:[global]wide_links') + ']').parent('label').removeClass('btn-outline-primary').addClass('btn-outline-danger');
} else {
$('[name=' + $.escapeSelector('smb.conf:[global]wide_links') + ']').parent('label').removeClass('btn-outline-danger').addClass('btn-outline-primary');
}
let unix_extensions_config = $('[name=' + $.escapeSelector('smb.conf:[global]unix_extensions') + ']:checked').val();
let allow_insecure_wide_links_config = $('[name=' + $.escapeSelector('smb.conf:[global]allow_insecure_wide_links') + ']:checked').val();
if (unix_extensions_config === 'yes' && allow_insecure_wide_links_config === 'no') {
$('[name=' + $.escapeSelector('smb.conf:[global]unix_extensions') + ']').parent('label').removeClass('btn-outline-primary').addClass('btn-outline-danger');
$('[name=' + $.escapeSelector('smb.conf:[global]allow_insecure_wide_links') + ']').parent('label').removeClass('btn-outline-primary').addClass('btn-outline-danger');
} else {
$('[name=' + $.escapeSelector('smb.conf:[global]unix_extensions') + ']').parent('label').removeClass('btn-outline-danger').addClass('btn-outline-primary');
$('[name=' + $.escapeSelector('smb.conf:[global]allow_insecure_wide_links') + ']').parent('label').removeClass('btn-outline-danger').addClass('btn-outline-primary');
}
}
function continueInstall(button, current_step) {
let data = '';
if (current_step === 4) {
data = '&host=' + encodeURIComponent($('#inputdb_host').val());
data += '&root_pwd=' + encodeURIComponent($('#inputdb_root_password').val());
}
ajaxCallFromButton(button, 'install', 'step=' + encodeURIComponent(current_step) + data, 'Loading...', null, 'Continuing...', function (data, $button) {
$button.prop('disabled', true);
location.href = data.next_page;
}, 0);
}
function parseParams(params) {
let parsedParams = {};
params.split("&").forEach(function (pair) {
if (pair === "") return;
var parts = pair.split("=");
parsedParams[parts[0]] = parts[1] && decodeURIComponent(parts[1].replace(/\+/g, " "));
});
return parsedParams;
}
function getFsckParams() {
let params = {};
let s = $('#id_l2_actions_fsck').find('input, select').serialize();
let parsedParams = parseParams(s);
for (let name in parsedParams) {
let value = parsedParams[name];
if (name === 'walk-metadata-store') {
name = 'dont-walk-metadata-store';
value = (value === 'yes' ? 'no' : 'yes');
}
params[name] = value;
}
return params;
}
function confirmRemoveDrive() {
let drive = $('#inputremove_drive').val();
if (drive === '0') {
alert('meh!');
return;
}
$.ajax({
type: 'POST',
url: './?ajax=pre-remove-drive&drive=' + encodeURIComponent(drive),
success: function(data, textStatus, jqXHR) {
if (data.result === 'success') {
if (data.drive_is_available) {
$('[name=drive_is_available][value=yes]').prop('checked', true).parent().addClass('active');
$('[name=drive_is_available][value=no]').prop('checked', false).parent().removeClass('active');
} else {
$('[name=drive_is_available][value=no]').prop('checked', true).parent().addClass('active');
$('[name=drive_is_available][value=yes]').prop('checked', false).parent().removeClass('active');
}
$('#remove-drive-preparing').addClass('d-none');
$('#remove-drive-ready').removeClass('d-none');
} else {
if (data.result === 'error') {
alert(data.message);
} else {
alert("An error occurred. Check your logs for details.");
}
}
},
});
}
function startRemoveDrive(button) {
let drive = $('#inputremove_drive').val();
let drive_is_available = $('[name=drive_is_available]:checked').val();
ajaxCallFromButton(button, 'remove_drive', 'drive=' + encodeURIComponent(drive) + '&drive_is_available=' + drive_is_available, 'Starting drive removal...', 'Started drive removal', 'Reloading...', function (data, $button) {
$('#remove-drive-ready').addClass('d-none');
$('#remove-drive-done').removeClass('d-none');
let body = $(button).closest('.modal-content').find('.modal-body');
body.text(data.text);
body.html(body.html().replace(/\n/g, "<br>"));
}, 0);
}
function confirmFsckCommand() {
let params = getFsckParams();
let command = "greyhole --fsck ";
for (let k in params) {
let v = params[k];
if (k === 'dir') {
if (v !== '') {
command += "--dir=" + v;
}
} else if (v === 'yes') {
command += "--" + k + " ";
}
}
$('#modal-confirm-fsck code').text(command);
}
function startFsck(button) {
ajaxCallFromButton(button, 'fsck', getFsckParams(), 'Starting fsck...', 'Started fsck', 'Reloading...', function (data, $button) {
$button.prop('disabled', true);
location.href = './';
}, 3);
}
function cancelFsck(button) {
ajaxCallFromButton(button, 'fsck', 'action=cancel', 'Cancelling...', 'fsck Cancelled', 'Reloading...', function (data, $button) {
$button.prop('disabled', true);
location.href = './';
}, 3);
}
function startBalance(button) {
ajaxCallFromButton(button, 'balance', 'action=start', 'Starting...', 'Balance started', 'Reloading...', function (data, $button) {
$button.prop('disabled', true);
location.href = './';
}, 3);
}
function cancelBalance(button) {
ajaxCallFromButton(button, 'balance', 'action=cancel', 'Cancelling...', 'Balance cancelled', 'Reloading...', function (data, $button) {
$button.prop('disabled', true);
location.href = './';
}, 3);
}
function emptyTrash(button) {
ajaxCallFromButton(button, 'trash', 'action=empty', 'Emptying...', 'Trash emptied', 'Reloading...', function (data, $button) {
$button.prop('disabled', true);
location.reload();
}, 3);
}
function colorizeTrashContent() {
let max = 0, min = 9999999999999;
let $els = $('#trash-content .colorize');
$els.each(function () {
let value = $(this).data('value');
if (value > max) {
max = value;
}
if (value < min) {
min = value;
}
});
$els.each(function () {
let value = $(this).data('value');
$(this).css('color', getTreemapColor(value, max, min));
});
}
function pauseDaemon(button) {
ajaxCallFromButton(button, 'pause', 'action=pause', 'Pausing...', 'Daemon paused', 'Reloading...', function (data, $button) {
$button.prop('disabled', true);
location.reload();
}, 3);
}
function resumeDaemon(button) {
ajaxCallFromButton(button, 'pause', 'action=resume', 'Resuming...', 'Daemon resumed', 'Reloading...', function (data, $button) {
$button.prop('disabled', true);
location.reload();
}, 3);
}
let status_logs_timer;
function tailStatusLogs(button) {
if (status_logs_timer) {
clearInterval(status_logs_timer);
}
if ($(button).prop('checked')) {
loadStatusLogs();
status_logs_timer = setInterval(loadStatusLogs, 10*1000);
}
}
defer(function(){ tailStatusLogs($('#tail-status-log').prop('checked', true)); });
function loadStatus() {
$.ajax({
type: 'POST',
url: './?ajax=get_status',
success: function(data, textStatus, jqXHR) {
if (data.result === 'success') {
$('.daemon-status').hide();
$('#daemon-status-' + data.daemon_status).show();
if (data.daemon_status === 'running') {
$('#daemon-status-running').html(data.status_text);
}
if (data.current_action === 'balance') {
$('#id_l2_status_balance_tab').show();
} else {
$('#id_l2_status_balance_tab').hide();
}
} else {
if (data.result === 'error') {
alert(data.message);
} else {
alert("An error occurred. Check your logs for details.");
}
}
},
});
}
function loadStatusLogs() {
if (!('page' in urlParams) || urlParams.page === 'id_l1_status_tab') {
loadStatus();
}
if (!('page_status' in urlParams) || urlParams.page_status === 'id_l2_status_balance_tab') {
loadStatusBalanceReport();
return;
}
if (!('page_status' in urlParams) || urlParams.page_status !== 'id_l2_status_logs_tab') {
// console.log("Skipping loadStatusLogs() because Logs tab is not visible.");
return;
}
$.ajax({
type: 'POST',
url: './?ajax=get_status_logs',
success: function(data, textStatus, jqXHR) {
if (data.result === 'success') {
let $container = $('#status_logs');
$container.text('');
for (let log of data.logs) {
$container.append($('<div/>').text(log).html() + "<br/>");
}
$('#last_action').text('')
.html("Last logged action: ")
.append($('<strong/>').text(data.last_action));
if (data.last_action_time !== null) {
$('#last_action').append(", on " + data.last_action_time + " (" + data.last_action_time_relative + ")");
}
} else {
if (data.result === 'error') {
alert(data.message);
} else {
alert("An error occurred. Check your logs for details.");
}
}
},
});
}
function loadStatusQueue() {
var $table = $('#queue');
var $loading_row = $table.find('.loading');
$loading_row.show();
$.ajax({
type: 'POST',
url: './?ajax=get_status_queue_content',
success: function(data, textStatus, jqXHR) {
if (data.result === 'success') {
$table.find('tr:not(.loading):not(.header)').detach();
for (let row of data.rows) {
let $tr = $('<tr/>');
if (row.share_name === 'Total') {
$tr.addClass('total');
}
$tr.append($('<td/>').text(row.share_name));
for (let prop of ['num_writes_pending', 'num_delete_pending', 'num_rename_pending', 'num_fsck_pending']) {
let $td = $('<td/>').addClass('num').text(row.queue[prop]);
if (row.queue[prop] > 0) {
$td.addClass('nonzero');
}
$tr.append($td);
}
$table.append($tr);
}
$('#num_spooled_ops').text(data.num_spooled_ops);
} else {
if (data.result === 'error') {
alert(data.message);
} else {
alert("An error occurred. Check your logs for details.");
}
}
$loading_row.hide();
},
});
}
function loadStatusPastTasks() {
$('#past-tasks-table').DataTable().ajax.reload();
}
function loadStatusFsckReport() {
$('#fsck-report-code').text('Loading...');
$.ajax({
type: 'POST',
url: './?ajax=get_status_fsck_report',
success: function(data, textStatus, jqXHR) {
if (data.result === 'success') {
$('#fsck-report-code').html(data.report_html);
if (data.show_stop_button) {
$('#cancel_fsck_container').show();
} else {
$('#cancel_fsck_container').hide();
}
} else {
if (data.result === 'error') {
alert(data.message);
} else {
alert("An error occurred. Check your logs for details.");
}
}
},
});
}
function loadStatusBalanceReport() {
let $container = $('#balance_groups');
$.ajax({
type: 'POST',
url: './?ajax=get_status_balance_report',
success: function(data, textStatus, jqXHR) {
if (data.result === 'success') {
if (data.show_stop_button) {
$('#cancel_balance_container').show();
} else {
$('#cancel_balance_container').hide();
}
for (let group of data.groups) {
let $alert = $('<div/>')
.addClass('alert alert-info')
.prop('role', 'alert')
.text('Target free space in ' + group.name + ' storage pool drives: ')
.append($('<strong/>').html(group.target_avail_space_html));
let $table = $('<table/>').addClass('table-sp-drives')
.append(
$('<tr/>')
.append($('<th/>').text('Path'))
.append($('<th/>').text('Needs'))
.append($('<th/>').text('Usage'))
);
for (let sp_drive in group.drives) {
let drive_infos = group.drives[sp_drive];
let $tr = $('<tr/>');
$tr.append($('<td/>').text(sp_drive));
$tr.append($('<td/>').html(drive_infos.direction + ' ' + drive_infos.diff_html));
let $td = $('<td/>').addClass('sp-bar-td');
$td.append(
$('<div/>').addClass('sp-bar target has_tooltip')
.data('width', drive_infos.target_width)
.data('toggle', 'tooltip')
.data('placement', 'bottom')
.prop('title', drive_infos.tooltip)
.tooltip()
);
$td.append(
$('<div/>').addClass('sp-bar has_tooltip')
.addClass(drive_infos.direction === '-' ? 'used' : 'free')
.text(drive_infos.direction === '-' ? '←' : '→')
.data('width', drive_infos.diff_width)
.data('toggle', 'tooltip')
.data('placement', 'bottom')
.prop('title', drive_infos.tooltip)
.tooltip()
);
$tr.append($td);
$table.append($tr);
}
$container.find('.has_tooltip').tooltip('hide'); // Hide any visible tooltip, because it would be stuck visible after detaching it from the document below
$container.text('')
.append($alert)
.append($('<div/>').addClass('col').append($table))
.append($('<hr/>'));
resizeSPDrivesUsageGraphs();
}
} else {
if (data.result === 'error') {
alert(data.message);
} else {
alert("An error occurred. Check your logs for details.");
}
}
},
});
}
function loadStoragePool() {
$.ajax({
type: 'POST',
url: './?ajax=get_storage_pool',
success: function(data, textStatus, jqXHR) {
if (data.result === 'success') {
let $table = $('#table-sp tbody').text('');
for (let sp_drive in data.sp_stats) {
if (sp_drive === 'Total') {
continue;
}
let drive_infos = data.sp_stats[sp_drive];
let $tr = $('<tr/>');
$tr.append($('<td/>').text(sp_drive));
$tr.append($('<td/>').html(drive_infos.min_free_html));
$tr.append($('<td/>').html(drive_infos.size_html));
let $td = $('<td/>').addClass('sp-bar-td');
$td.append(
$('<div/>').addClass('sp-bar used')
.data('width', drive_infos.used_width)
.data('toggle', 'tooltip').data('placement', 'bottom').prop('title', drive_infos.used_tooltip).tooltip()
);
$td.append(
$('<div/>').addClass('sp-bar trash')
.data('width', drive_infos.trash_width)
.data('toggle', 'tooltip').data('placement', 'bottom').prop('title', drive_infos.trash_tooltip).tooltip()
);
$td.append(
$('<div/>').addClass('sp-bar free')
.data('width', drive_infos.free_width)
.data('toggle', 'tooltip').data('placement', 'bottom').prop('title', drive_infos.free_tooltip).tooltip()
);
$tr.append($td);
$table.append($tr);
}
resizeSPDrivesUsageGraphs();
let ctx = document.getElementById('chart_storage_pool').getContext('2d');
drawPieChartStorage(ctx, data.sp_stats);
} else {
if (data.result === 'error') {
alert(data.message);
} else {
alert("An error occurred. Check your logs for details.");
}
}
},
});
}
function loadTrashmanContent() {
trashmanDataForDir('.');
}
function loadActionsTrashContent() {
var $table = $('#trash-content');
var $loading_row = $table.find('.loading');
$loading_row.show();
$.ajax({
type: 'POST',
url: './?ajax=trash_content',
success: function(data, textStatus, jqXHR) {
if (data.result === 'success') {
$table.find('tr:not(.loading)').detach();
for (let row of data.sp_stats) {
let $tr = $('<tr/>');
$tr.append($('<td/>').append('<code/>').text(row.drive));
$tr.append($('<td/>').addClass('colorize').data('value', row.trash_size).html(row.trash_size_human));
$table.append($tr);
}
colorizeTrashContent();
} else {
if (data.result === 'error') {
alert(data.message);
} else {
alert("An error occurred. Check your logs for details.");
}
}
$loading_row.hide();
},
});
}