WIP new WebAdmin: folders page

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino 2024-01-14 16:59:27 +01:00
parent 1a0f734a9c
commit 0722c4369b
No known key found for this signature in database
GPG key ID: 935D2952DEC4EECF
9 changed files with 551 additions and 365 deletions

View file

@ -1682,7 +1682,7 @@ func (s *httpdServer) setupWebAdminRoutes() {
router.With(s.checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie). router.With(s.checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie).
Get(webUsersPath, s.handleGetWebUsers) Get(webUsersPath, s.handleGetWebUsers)
router.With(s.checkPerm(dataprovider.PermAdminViewUsers), s.refreshCookie). router.With(s.checkPerm(dataprovider.PermAdminViewUsers), compressor.Handler, s.refreshCookie).
Get(webUsersPath+"/json", getAllUsers) Get(webUsersPath+"/json", getAllUsers)
router.With(s.checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie). router.With(s.checkPerm(dataprovider.PermAdminAddUsers), s.refreshCookie).
Get(webUserPath, s.handleWebAddUserGet) Get(webUserPath, s.handleWebAddUserGet)
@ -1693,7 +1693,7 @@ func (s *httpdServer) setupWebAdminRoutes() {
s.handleWebUpdateUserPost) s.handleWebUpdateUserPost)
router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie). router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie).
Get(webGroupsPath, s.handleWebGetGroups) Get(webGroupsPath, s.handleWebGetGroups)
router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie). router.With(s.checkPerm(dataprovider.PermAdminManageGroups), compressor.Handler, s.refreshCookie).
Get(webGroupsPath+"/json", getAllGroups) Get(webGroupsPath+"/json", getAllGroups)
router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie). router.With(s.checkPerm(dataprovider.PermAdminManageGroups), s.refreshCookie).
Get(webGroupPath, s.handleWebAddGroupGet) Get(webGroupPath, s.handleWebAddGroupGet)
@ -1708,6 +1708,8 @@ func (s *httpdServer) setupWebAdminRoutes() {
Get(webConnectionsPath, s.handleWebGetConnections) Get(webConnectionsPath, s.handleWebGetConnections)
router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie). router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie).
Get(webFoldersPath, s.handleWebGetFolders) Get(webFoldersPath, s.handleWebGetFolders)
router.With(s.checkPerm(dataprovider.PermAdminManageFolders), compressor.Handler, s.refreshCookie).
Get(webFoldersPath+"/json", getAllFolders)
router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie). router.With(s.checkPerm(dataprovider.PermAdminManageFolders), s.refreshCookie).
Get(webFolderPath, s.handleWebAddFolderGet) Get(webFolderPath, s.handleWebAddFolderGet)
router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(webFolderPath, s.handleWebAddFolderPost) router.With(s.checkPerm(dataprovider.PermAdminManageFolders)).Post(webFolderPath, s.handleWebAddFolderPost)

View file

@ -172,11 +172,6 @@ type adminsPage struct {
Admins []dataprovider.Admin Admins []dataprovider.Admin
} }
type foldersPage struct {
basePage
Folders []vfs.BaseVirtualFolder
}
type rolesPage struct { type rolesPage struct {
basePage basePage
Roles []dataprovider.Role Roles []dataprovider.Role
@ -428,7 +423,7 @@ func loadAdminTemplates(templatesPath string) {
filepath.Join(templatesPath, templateAdminDir, templateMessage), filepath.Join(templatesPath, templateAdminDir, templateMessage),
} }
foldersPaths := []string{ foldersPaths := []string{
filepath.Join(templatesPath, templateCommonDir, templateCommonCSS), filepath.Join(templatesPath, templateCommonDir, templateCommonBase),
filepath.Join(templatesPath, templateAdminDir, templateBase), filepath.Join(templatesPath, templateAdminDir, templateBase),
filepath.Join(templatesPath, templateAdminDir, templateFolders), filepath.Join(templatesPath, templateAdminDir, templateFolders),
} }
@ -3444,7 +3439,7 @@ func (s *httpdServer) handleWebUpdateFolderPost(w http.ResponseWriter, r *http.R
} }
func (s *httpdServer) getWebVirtualFolders(w http.ResponseWriter, r *http.Request, limit int, minimal bool) ([]vfs.BaseVirtualFolder, error) { func (s *httpdServer) getWebVirtualFolders(w http.ResponseWriter, r *http.Request, limit int, minimal bool) ([]vfs.BaseVirtualFolder, error) {
folders := make([]vfs.BaseVirtualFolder, 0, limit) folders := make([]vfs.BaseVirtualFolder, 0, 50)
for { for {
f, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC, minimal) f, err := dataprovider.GetFolders(limit, len(folders), dataprovider.OrderASC, minimal)
if err != nil { if err != nil {
@ -3459,25 +3454,27 @@ func (s *httpdServer) getWebVirtualFolders(w http.ResponseWriter, r *http.Reques
return folders, nil return folders, nil
} }
func (s *httpdServer) handleWebGetFolders(w http.ResponseWriter, r *http.Request) { func getAllFolders(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize) r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
limit := defaultQueryLimit folders := make([]vfs.BaseVirtualFolder, 0, 50)
if _, ok := r.URL.Query()["qlimit"]; ok { for {
var err error f, err := dataprovider.GetFolders(defaultQueryLimit, len(folders), dataprovider.OrderASC, false)
limit, err = strconv.Atoi(r.URL.Query().Get("qlimit"))
if err != nil {
limit = defaultQueryLimit
}
}
folders, err := s.getWebVirtualFolders(w, r, limit, false)
if err != nil { if err != nil {
sendAPIResponse(w, r, err, getI18NErrorString(err, util.I18nError500Message), http.StatusInternalServerError)
return return
} }
folders = append(folders, f...)
data := foldersPage{ if len(f) < defaultQueryLimit {
basePage: s.getBasePageData(util.I18nFoldersTitle, webFoldersPath, r), break
Folders: folders,
} }
}
render.JSON(w, r, folders)
}
func (s *httpdServer) handleWebGetFolders(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
data := s.getBasePageData(util.I18nFoldersTitle, webFoldersPath, r)
renderAdminTemplate(w, templateFolders, data) renderAdminTemplate(w, templateFolders, data)
} }

View file

@ -206,7 +206,8 @@
"zero_no_limit_help": "0 means no limit", "zero_no_limit_help": "0 means no limit",
"global_settings": "Global settings", "global_settings": "Global settings",
"mandatory_encryption": "Mandatory encryption", "mandatory_encryption": "Mandatory encryption",
"name_invalid": "The specified username is not valid, the following characters are allowed: a-zA-Z0-9-_.~" "name_invalid": "The specified username is not valid, the following characters are allowed: a-zA-Z0-9-_.~",
"associations": "Associations"
}, },
"fs": { "fs": {
"view_file": "View file \"{{- path}}\"", "view_file": "View file \"{{- path}}\"",
@ -474,10 +475,12 @@
"members_summary": "Users: {{users}}. Admins: {{admins}}" "members_summary": "Users: {{users}}. Admins: {{admins}}"
}, },
"virtual_folders": { "virtual_folders": {
"view_manage": "View and manage virtual folders",
"mount_path": "mount path, i.e. /vfolder", "mount_path": "mount path, i.e. /vfolder",
"quota_size": "Quota size", "quota_size": "Quota size",
"quota_size_help": "0 means no limit. You can use MB/GB/TB suffix", "quota_size_help": "0 means no limit. You can use MB/GB/TB suffix",
"quota_files": "Quota files" "quota_files": "Quota files",
"associations_summary": "Users: {{users}}. Groups: {{groups}}"
}, },
"storage": { "storage": {
"title": "File system", "title": "File system",

View file

@ -206,7 +206,8 @@
"zero_no_limit_help": "0 significa nessun limite", "zero_no_limit_help": "0 significa nessun limite",
"global_settings": "Impostazioni globali", "global_settings": "Impostazioni globali",
"mandatory_encryption": "Crittografia obbligatoria", "mandatory_encryption": "Crittografia obbligatoria",
"name_invalid": "Il nome specificato non è valido, sono consentiti i seguenti caratteri: a-zA-Z0-9-_.~" "name_invalid": "Il nome specificato non è valido, sono consentiti i seguenti caratteri: a-zA-Z0-9-_.~",
"associations": "Associazioni"
}, },
"fs": { "fs": {
"view_file": "Visualizza file \"{{- path}}\"", "view_file": "Visualizza file \"{{- path}}\"",
@ -474,10 +475,12 @@
"members_summary": "Utenti: {{users}}. Amministratori: {{admins}}" "members_summary": "Utenti: {{users}}. Amministratori: {{admins}}"
}, },
"virtual_folders": { "virtual_folders": {
"view_manage": "Visualizza e gestisci cartelle virtuali",
"mount_path": "percorso, es. /vfolder", "mount_path": "percorso, es. /vfolder",
"quota_size": "Quota (dimensione)", "quota_size": "Quota (dimensione)",
"quota_size_help": "0 significa nessun limite. E' possibile utilizzare il suffisso MB/GB/TB", "quota_size_help": "0 significa nessun limite. E' possibile utilizzare il suffisso MB/GB/TB",
"quota_files": "Quota (numero file)" "quota_files": "Quota (numero file)",
"associations_summary": "Utenti: {{users}}. Gruppi: {{groups}}"
}, },
"storage": { "storage": {
"title": "File system", "title": "File system",

View file

@ -1,361 +1,409 @@
<!-- <!--
Copyright (C) 2019 Nicola Murino Copyright (C) 2024 Nicola Murino
This program is free software: you can redistribute it and/or modify This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, version 3.
This program is distributed in the hope that it will be useful, https://keenthemes.com/products/templates-mega-bundle
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License KeenThemes HTML/CSS/JS components are allowed for use only within the
along with this program. If not, see <https://www.gnu.org/licenses/>. SFTPGo product and restricted to be used in a resealable HTML template
that can compete with KeenThemes products anyhow.
This WebUI is allowed for use only within the SFTPGo product and
therefore cannot be used in derivative works/products without an
explicit grant from the SFTPGo Team (support@sftpgo.com).
--> -->
{{template "base" .}} {{template "base" .}}
{{define "title"}}{{.Title}}{{end}} {{- define "extra_css"}}
<link href="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css" rel="stylesheet" type="text/css"/>
{{- end}}
{{define "extra_css"}} {{- define "page_body"}}
<link href="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.css" rel="stylesheet"> {{- template "errmsg" ""}}
<link href="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.css" rel="stylesheet"> <div class="card shadow-sm">
<link href="{{.StaticURL}}/vendor/datatables/fixedHeader.bootstrap4.min.css" rel="stylesheet"> <div class="card-header bg-light">
<link href="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.css" rel="stylesheet"> <h3 data-i18n="virtual_folders.view_manage" class="card-title section-title">View and manage folders</h3>
<link href="{{.StaticURL}}/vendor/datatables/select.bootstrap4.min.css" rel="stylesheet"> </div>
<link href="{{.StaticURL}}/vendor/datatables/colReorder.bootstrap4.min.css" rel="stylesheet"> <div id="card_body" class="card-body">
{{end}} <div id="loader" class="align-items-center text-center my-10">
<span class="spinner-border w-15px h-15px text-muted align-middle me-2"></span>
<span data-i18n="general.loading" class="text-gray-700">Loading...</span>
</div>
<div id="card_content" class="d-none">
<div class="d-flex flex-stack flex-wrap mb-5">
<div class="d-flex align-items-center position-relative my-2">
<i class="ki-solid ki-magnifier fs-1 position-absolute ms-6"></i>
<input name="search" data-i18n="[placeholder]general.search" type="text" data-table-filter="search"
class="form-control rounded-1 w-250px ps-15 me-5" placeholder="Search" />
</div>
{{define "page_body"}} <div class="d-flex justify-content-end my-2" data-table-toolbar="base">
<div id="errorMsg" class="alert alert-warning fade show" style="display: none;" role="alert"> <button type="button" class="btn btn-light-primary rotate" data-kt-menu-trigger="click" data-kt-menu-placement="bottom" data-kt-menu-permanent="true">
<span id="errorTxt"></span> <span data-i18n="general.colvis">Column visibility</span>
<button type="button" class="close" aria-label="Close" onclick="dismissErrorMsg();"> <i class="ki-duotone ki-down fs-3 rotate-180 ms-3 me-0"></i>
<span aria-hidden="true">&times;</span>
</button> </button>
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-800 menu-state-bg-light-primary fw-semibold w-auto min-w-200 mw-300px py-4" data-kt-menu="true">
<div class="menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid">
<input type="checkbox" class="form-check-input" value="" id="checkColStorage" />
<label class="form-check-label" for="checkColStorage">
<span data-i18n="storage.label" class="text-gray-800 fs-6">Storage</span>
</label>
</div>
<div class="menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid">
<input type="checkbox" class="form-check-input" value="" id="checkColQuota" />
<label class="form-check-label" for="checkColQuota">
<span data-i18n="fs.quota_usage.disk" class="text-gray-800 fs-6">Disk quota</span>
</label>
</div>
<div class="menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid">
<input type="checkbox" class="form-check-input" value="" id="checkColAssociations" />
<label class="form-check-label" for="checkColAssociations">
<span data-i18n="general.associations" class="text-gray-800 fs-6">Associations</span>
</label>
</div>
<div class="menu-item px-3 py-2 form-check form-check-sm form-check-custom form-check-solid">
<input type="checkbox" class="form-check-input" value="" id="checkColDesc" />
<label class="form-check-label" for="checkColDesc">
<span data-i18n="general.description" class="text-gray-800 fs-6">Description</span>
</label>
</div>
</div>
{{- if .LoggedUser.HasPermission "manage_folders"}}
<a href="{{.FolderURL}}" class="btn btn-primary ms-5">
<i class="ki-duotone ki-plus fs-2"></i>
<span data-i18n="general.add">Add</span>
</a>
{{- end}}
</div>
</div> </div>
<script type="text/javascript">
function dismissErrorMsg(){
$('#errorMsg').hide();
}
</script>
<div id="successMsg" class="card mb-4 border-left-success" style="display: none;">
<div id="successTxt" class="card-body"></div>
</div> </div>
<div class="card shadow mb-4"> <table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">View and manage folders</h6>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover nowrap" id="dataTable" width="100%" cellspacing="0">
<thead> <thead>
<tr> <tr class="text-start text-muted fw-bold fs-6 gs-0">
<th></th> <th data-i18n="general.name">Name</th>
<th>Name</th> <th data-i18n="storage.label">Storage</th>
<th>Storage</th> <th data-i18n="fs.quota_usage.disk">Disk quota</th>
<th>Description</th> <th data-i18n="general.associations">Associations</th>
<th>Associated users</th> <th data-i18n="general.description">Description</th>
<th>Associated groups</th> <th class="min-w-100px"></th>
<th>Quota</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="table_body" class="text-gray-800 fw-semibold"></tbody>
{{range .Folders}}
<tr>
<td>{{.GetLastQuotaUpdateAsString}}</td>
<td>{{.Name}}</td>
<td>{{.GetStorageDescrition}}</td>
<td>{{.Description}}</td>
<td>{{.GetUsersAsString}}</td>
<td>{{.GetGroupsAsString}}</td>
<td>{{.GetQuotaSummary}}</td>
</tr>
{{end}}
</tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
{{end}} {{- end}}
{{- define "extra_js"}}
<script {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}} src="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.js"></script>
<script type="text/javascript" {{- if .CSPNonce}} nonce="{{.CSPNonce}}"{{- end}}>
function deleteAction(name) {
ModalAlert.fire({
text: $.t('general.delete_confirm_generic'),
icon: "warning",
confirmButtonText: $.t('general.delete_confirm_btn'),
cancelButtonText: $.t('general.cancel'),
customClass: {
confirmButton: "btn btn-danger",
cancelButton: 'btn btn-secondary'
}
}).then((result) => {
if (result.isConfirmed){
$('#loading_message').text("");
KTApp.showPageLoading();
let path = '{{.FolderURL}}' + "/" + encodeURIComponent(name);
{{define "dialog"}} axios.delete(path, {
<div class="modal fade" id="deleteModal" tabindex="-1" role="dialog" aria-labelledby="deleteModalLabel"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">
Confirmation required
</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">Do you want to delete the selected virtual folder and any users mapping?</div>
<div class="modal-footer">
<button class="btn btn-secondary" type="button" data-dismiss="modal">
Cancel
</button>
<a class="btn btn-warning" href="#" onclick="deleteAction()">
Delete
</a>
</div>
</div>
</div>
</div>
{{end}}
{{define "extra_js"}}
<script src="{{.StaticURL}}/vendor/datatables/jquery.dataTables.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.buttons.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/buttons.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/buttons.colVis.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.fixedHeader.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.responsive.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/responsive.bootstrap4.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.select.min.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/ellipsis.js"></script>
<script src="{{.StaticURL}}/vendor/datatables/dataTables.colReorder.min.js"></script>
<script type="text/javascript">
function deleteAction() {
let table = $('#dataTable').DataTable();
table.button('delete:name').enable(false);
let folderName = table.row({ selected: true }).data()[1];
let path = '{{.FolderURL}}' + "/" + fixedEncodeURIComponent(folderName);
$('#deleteModal').modal('hide');
$('#errorMsg').hide();
$.ajax({
url: path,
type: 'DELETE',
dataType: 'json',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
timeout: 15000, timeout: 15000,
success: function (result) { headers: {
window.location.href = '{{.FoldersURL}}'; 'X-CSRF-TOKEN': '{{.CSRFToken}}'
}, },
error: function ($xhr, textStatus, errorThrown) { validateStatus: function (status) {
var txt = "Unable to delete the selected folder"; return status == 200;
if ($xhr) { }
var json = $xhr.responseJSON; }).then(function(response){
if (json) { location.reload();
if (json.message){ }).catch(function(error){
txt += ": " + json.message; KTApp.hidePageLoading();
} else { let errorMessage;
txt += ": " + json.error; if (error && error.response) {
switch (error.response.status) {
case 403:
errorMessage = "general.delete_error_403";
break;
case 404:
errorMessage = "general.delete_error_404";
break;
} }
} }
if (!errorMessage){
errorMessage = "general.delete_error_generic";
} }
$('#errorTxt').text(txt); ModalAlert.fire({
$('#errorMsg').show(); text: $.t(errorMessage),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
});
} }
}); });
} }
$(document).ready(function () { var datatable = function(){
$.fn.dataTable.ext.buttons.add = { var dt;
text: '<i class="fas fa-plus"></i>',
name: 'add',
titleAttr: "Add",
action: function (e, dt, node, config) {
window.location.href = '{{.FolderURL}}';
}
};
$.fn.dataTable.ext.buttons.edit = { var initDatatable = function () {
text: '<i class="fas fa-pen"></i>', $('#errorMsg').addClass("d-none");
name: 'edit', dt = $('#dataTable').DataTable({
titleAttr: "Edit", ajax: {
action: function (e, dt, node, config) { url: "{{.FoldersURL}}/json",
var folderName = table.row({ selected: true }).data()[1]; dataSrc: "",
var path = '{{.FolderURL}}' + "/" + fixedEncodeURIComponent(folderName);
window.location.href = path;
},
enabled: false
};
$.fn.dataTable.ext.buttons.template = {
text: '<i class="fas fa-clone"></i>',
name: 'template',
titleAttr: "Template",
action: function (e, dt, node, config) {
var selectedRows = table.rows({ selected: true }).count();
if (selectedRows == 1){
var folderName = table.row({ selected: true }).data()[1];
var path = '{{.FolderTemplateURL}}' + "?from=" + encodeURIComponent(folderName);
window.location.href = path;
} else {
window.location.href = '{{.FolderTemplateURL}}';
}
}
};
$.fn.dataTable.ext.buttons.delete = {
text: '<i class="fas fa-trash"></i>',
name: 'delete',
titleAttr: "Delete",
action: function (e, dt, node, config) {
$('#deleteModal').modal('show');
},
enabled: false
};
$.fn.dataTable.ext.buttons.quota_scan = {
text: '<i class="fas fa-redo-alt"></i>',
name: 'quota_scan',
titleAttr: 'Quota Scan',
action: function (e, dt, node, config) {
dt.button('quota_scan:name').enable(false);
let folderName = dt.row({ selected: true }).data()[1];
let path = '{{.FolderQuotaScanURL}}'+ "/" + fixedEncodeURIComponent(folderName);
$.ajax({
url: path,
type: 'POST',
headers: {'X-CSRF-TOKEN' : '{{.CSRFToken}}'},
timeout: 15000,
success: function (result) {
//dt.button('quota_scan:name').enable(true);
$('#successTxt').text("Quota scan started for the selected folder. Please reload the folders page to check when the scan ends");
$('#successMsg').show();
setTimeout(function () {
$('#successMsg').hide();
}, 15000);
},
error: function ($xhr, textStatus, errorThrown) { error: function ($xhr, textStatus, errorThrown) {
dt.button('quota_scan:name').enable(true); $(".dataTables_processing").hide();
var txt = "Unable to update quota for the selected folder"; let txt = "";
if ($xhr) { if ($xhr) {
var json = $xhr.responseJSON; let json = $xhr.responseJSON;
if (json) { if (json) {
if (json.message){ if (json.message){
txt += ": " + json.message; txt = json.message;
} else if (json.error) {
txt += ": " + json.error;
} }
} }
} }
$('#errorTxt').text(txt); if (!txt){
$('#errorMsg').show(); txt = "general.error500";
} }
}); setI18NData($('#errorTxt'), txt);
}, $('#errorMsg').removeClass("d-none");
enabled: false
};
let dateFn = $.fn.dataTable.render.datetime();
let table = $('#dataTable').DataTable({
"select": {
"style": "single",
"blurable": true
},
"colReorder": {
"enable": true,
"fixedColumnsLeft": 2
},
"stateSave": true,
"stateDuration": 0,
"buttons": [
{
"text": "Column visibility",
"extend": "colvis",
"columns": ":not(.noVis)"
} }
],
"columnDefs": [
{
"targets": [0],
"visible": false,
"className": "noVis"
}, },
columns: [
{ {
"targets": [1], data: "name",
"className": "noVis" render: function(data, type, row) {
}, if (type === 'display') {
{ return escapeHTML(data);
"targets": [2], }
"render": $.fn.dataTable.render.ellipsis(50, true)
},
{
"targets": [3],
"visible": false
},
{
"targets": [4],
"render": $.fn.dataTable.render.ellipsis(40, true)
},
{
"targets": [5],
"visible": false,
"render": $.fn.dataTable.render.ellipsis(40, true)
},
{
"targets": [6],
"visible": false,
"render": function ( data, type, row, meta ) {
if (type !== 'display') {
return data; return data;
} }
if (row[0] !== ""){
let formattedDate = dateFn(row[0], type);
data = `${data}. Updated at: ${formattedDate}`;
}
let ellipsisFn = $.fn.dataTable.render.ellipsis(60, true);
return ellipsisFn(data, type);
}
}
],
"scrollX": false,
"scrollY": false,
"responsive": true,
"language": {
"emptyTable": "No folder defined"
}, },
"order": [[1, 'asc']] {
data: "filesystem.provider",
defaultContent: "",
render: function(data, type, row) {
if (type === 'display') {
switch (data){
case 1:
return $.t('storage.s3');
case 2:
return $.t('storage.gcs');
case 3:
return $.t('storage.azblob');
case 4:
return $.t('storage.encrypted');
case 5:
return $.t('storage.sftp');
case 6:
return $.t('storage.http');
default:
return $.t('storage.local');
}
}
return data;
}
},
{
data: "used_quota_size",
visible: false,
searchable: false,
orderable: false,
render: function(data, type, row) {
if (type === 'display') {
let val = "";
if (row.used_quota_size) {
let usage = fileSizeIEC(row.used_quota_size);
val += $.t('fs.quota_usage.size', {val: usage})+". ";
}
if (row.used_quota_files){
val += $.t('fs.quota_usage.files', {val: row.used_quota_files});
}
return val
}
return data;
}
},
{
data: "users",
defaultContent: "",
visible: false,
searchable: false,
orderable: false,
render: function(data, type, row) {
if (type === 'display') {
let users = 0;
if (row.users){
users = row.users.length;
}
let groups = 0;
if (row.groups){
groups = row.groups.length;
}
return $.t('virtual_folders.associations_summary', {users: users, groups: groups});
}
return "";
}
},
{
data: "description",
visible: false,
defaultContent: "",
render: function(data, type, row) {
if (type === 'display') {
if (data){
return escapeHTML(data);
}
return ""
}
return data;
}
},
{
data: "id",
searchable: false,
orderable: false,
className: 'text-end',
render: function (data, type, row) {
if (type === 'display') {
let numActions = 0;
let actions = `<button class="btn btn-light btn-active-light-primary btn-flex btn-center btn-sm rotate" data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end">
<span data-i18n="general.actions" class="fs-6">Actions</span>
<i class="ki-duotone ki-down fs-5 ms-1 rotate-180"></i>
</button>
<div class="menu menu-sub menu-sub-dropdown menu-column menu-rounded menu-gray-700 menu-state-bg-light-primary fw-semibold fs-6 w-200px py-4" data-kt-menu="true">`;
//{{- if .LoggedUser.HasPermission "manage_folders"}}
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-share-table-action="edit_row">Edit</a>
</div>`
numActions++;
actions+=`<div class="menu-item px-3">
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-share-table-action="delete_row">Delete</a>
</div>`
//{{- end}}
if (numActions > 0){
actions+=`</div>`;
return actions;
}
}
return "";
}
},
],
deferRender: true,
stateSave: true,
stateDuration: 0,
colReorder: {
enable: true,
fixedColumnsLeft: 1
},
stateLoadParams: function (settings, data) {
if (data.search.search){
const filterSearch = document.querySelector('[data-table-filter="search"]');
filterSearch.value = data.search.search;
}
},
language: {
info: $.t('datatable.info'),
infoEmpty: $.t('datatable.info_empty'),
infoFiltered: $.t('datatable.info_filtered'),
loadingRecords: "",
processing: $.t('datatable.processing'),
zeroRecords: "",
emptyTable: $.t('datatable.no_records')
},
order: [[0, 'asc']],
initComplete: function(settings, json) {
$('#loader').addClass("d-none");
$('#card_content').removeClass("d-none");
let api = $.fn.dataTable.Api(settings);
api.columns.adjust().draw("page");
drawAction();
}
}); });
new $.fn.dataTable.FixedHeader( table ); dt.on('draw', drawAction);
dt.on('column-reorder', function(e, settings, details){
drawAction();
});
}
{{if .LoggedAdmin.HasPermission "quota_scans"}} function drawAction() {
table.button().add(0,'quota_scan'); KTMenu.createInstances();
{{end}} handleRowActions();
$('#table_body').localize();
}
{{if .LoggedAdmin.HasPermission "del_users"}} function handleColVisibilityCheckbox(el, index) {
table.button().add(0,'delete'); el.off("change");
{{end}} el.prop('checked', dt.column(index).visible());
el.on("change", function(e){
dt.column(index).visible($(this).is(':checked'));
dt.draw('page');
});
}
{{if .LoggedAdmin.HasPermission "add_users"}} var handleDatatableActions = function () {
table.button().add(0,'template'); const filterSearch = $(document.querySelector('[data-table-filter="search"]'));
{{end}} filterSearch.off("keyup");
filterSearch.on('keyup', function (e) {
dt.rows().deselect();
dt.search(e.target.value, true, false).draw();
});
handleColVisibilityCheckbox($('#checkColStorage'), 1);
handleColVisibilityCheckbox($('#checkColQuota'), 2);
handleColVisibilityCheckbox($('#checkColAssociations'), 3);
handleColVisibilityCheckbox($('#checkColDesc'), 4);
}
{{if .LoggedAdmin.HasPermission "edit_users"}} function handleRowActions() {
table.button().add(0,'edit'); const editButtons = document.querySelectorAll('[data-share-table-action="edit_row"]');
{{end}} editButtons.forEach(d => {
let el = $(d);
{{if .LoggedAdmin.HasPermission "add_users"}} el.off("click");
table.button().add(0,'add'); el.on("click", function(e){
{{end}} e.preventDefault();
let rowData = dt.row(e.target.closest('tr')).data();
table.buttons().container().appendTo('.col-md-6:eq(0)', table.table().container()); window.location.replace('{{.FolderURL}}' + "/" + encodeURIComponent(rowData['name']));
});
table.on('select deselect', function () {
var selectedRows = table.rows({ selected: true }).count();
{{if .LoggedAdmin.HasPermission "del_users"}}
table.button('delete:name').enable(selectedRows == 1);
{{end}}
{{if .LoggedAdmin.HasPermission "edit_users"}}
table.button('edit:name').enable(selectedRows == 1);
{{end}}
{{if .LoggedAdmin.HasPermission "quota_scans"}}
table.button('quota_scan:name').enable(selectedRows == 1);
{{end}}
}); });
const deleteButtons = document.querySelectorAll('[data-share-table-action="delete_row"]');
deleteButtons.forEach(d => {
let el = $(d);
el.off("click");
el.on("click", function(e){
e.preventDefault();
const parent = e.target.closest('tr');
deleteAction(dt.row(parent).data()['name']);
}); });
});
}
return {
init: function () {
initDatatable();
handleDatatableActions();
}
}
}();
$(document).on("i18nshow", function(){
datatable.init();
});
</script> </script>
{{end}} {{- end}}

View file

@ -126,7 +126,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (!errorMessage){ if (!errorMessage){
errorMessage = "general.delete_error_generic"; errorMessage = "general.delete_error_generic";
} }
showToast(2, errorMessage); ModalAlert.fire({
text: $.t(errorMessage),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
}); });
} }
}); });

View file

@ -175,7 +175,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (!errorMessage){ if (!errorMessage){
errorMessage = "general.delete_error_generic"; errorMessage = "general.delete_error_generic";
} }
showToast(2, errorMessage); ModalAlert.fire({
text: $.t(errorMessage),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
}); });
} }
}); });
@ -209,7 +216,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (!errorMessage) { if (!errorMessage) {
errorMessage = "general.quota_scan_error"; errorMessage = "general.quota_scan_error";
} }
showToast(2, errorMessage); ModalAlert.fire({
text: $.t(errorMessage),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
}); });
} }
@ -364,7 +378,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (!used){ if (!used){
used = 0; used = 0;
} }
let usage = fileSizeIEC(used)+"/"+fileSizeIEC(row.quota_size) let usage = fileSizeIEC(used)+"/"+fileSizeIEC(row.quota_size);
val += $.t('fs.quota_usage.size', {val: usage})+". "; val += $.t('fs.quota_usage.size', {val: usage})+". ";
} else if (row.used_quota_size && row.used_quota_size > 0) { } else if (row.used_quota_size && row.used_quota_size > 0) {
val += $.t('fs.quota_usage.size', {val: fileSizeIEC(row.used_quota_size)})+". "; val += $.t('fs.quota_usage.size', {val: fileSizeIEC(row.used_quota_size)})+". ";

View file

@ -1224,7 +1224,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
items = checkMoveCopyItems(items) items = checkMoveCopyItems(items)
if (items.length == 0){ if (items.length == 0){
showToast(2, "fs.invalid_name"); ModalAlert.fire({
text: $.t('fs.invalid_name'),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
return; return;
} }
keepAlive(); keepAlive();
@ -1296,7 +1303,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (!errorMessage){ if (!errorMessage){
errorMessage = "fs.copy.err_generic"; errorMessage = "fs.copy.err_generic";
} }
showToast(2, errorMessage); ModalAlert.fire({
text: $.t(errorMessage),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
copyItem(); copyItem();
}); });
} }
@ -1315,10 +1329,24 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}).then((result)=>{ }).then((result)=>{
if (result.error) { if (result.error) {
hasError = true; hasError = true;
showToast(2, "fs.copy.err_generic"); ModalAlert.fire({
text: $.t("fs.copy.err_generic"),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
} else if (result.data.length > 0){ } else if (result.data.length > 0){
hasError = true; hasError = true;
showToast(2, "fs.copy.err_exists"); ModalAlert.fire({
text: $.t("fs.copy.err_exists"),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
} }
copyItem(); copyItem();
}); });
@ -1332,7 +1360,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
} }
items = checkMoveCopyItems(items) items = checkMoveCopyItems(items)
if (items.length == 0){ if (items.length == 0){
showToast(2, "fs.invalid_name"); ModalAlert.fire({
text: $.t("fs.invalid_name"),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
return; return;
} }
keepAlive(); keepAlive();
@ -1404,7 +1439,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (!errorMessage){ if (!errorMessage){
errorMessage = "fs.move.err_generic"; errorMessage = "fs.move.err_generic";
} }
showToast(2, errorMessage); ModalAlert.fire({
text: $.t(errorMessage),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
moveItem(); moveItem();
}); });
} }
@ -1423,10 +1465,24 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}).then((result)=>{ }).then((result)=>{
if (result.error) { if (result.error) {
hasError = true; hasError = true;
showToast(2, "fs.move.err_generic"); ModalAlert.fire({
text: $.t("fs.move.err_generic"),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
} else if (result.data.length > 0){ } else if (result.data.length > 0){
hasError = true; hasError = true;
showToast(2, "fs.move.err_exists"); ModalAlert.fire({
text: $.t("fs.move.err_exists"),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
} }
moveItem(); moveItem();
}); });
@ -1493,7 +1549,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (!errorMessage){ if (!errorMessage){
errorMessage = "fs.delete.err_generic"; errorMessage = "fs.delete.err_generic";
} }
showToast(2, errorMessage, {name: itemName}); ModalAlert.fire({
text: $.t(errorMessage, {name: itemName}),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
}); });
} }
}); });
@ -1523,15 +1586,36 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
let oldName = getNameFromMeta(meta); let oldName = getNameFromMeta(meta);
let newName = $('#rename_new_name').val(); let newName = $('#rename_new_name').val();
if (!newName){ if (!newName){
showToast(2, "general.name_required"); ModalAlert.fire({
text: $.t('general.name_required'),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
return; return;
} }
if (newName == oldName){ if (newName == oldName){
showToast(2, "general.name_different"); ModalAlert.fire({
text: $.t('general.name_different'),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
return; return;
} }
if (newName.includes("/")){ if (newName.includes("/")){
showToast(2, "fs.invalid_name"); ModalAlert.fire({
text: $.t('fs.invalid_name'),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
return; return;
} }
@ -1567,7 +1651,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (!errorMessage) { if (!errorMessage) {
errorMessage = "fs.rename.err_generic"; errorMessage = "fs.rename.err_generic";
} }
showToast(2, errorMessage, { name: oldName }); ModalAlert.fire({
text: $.t(errorMessage, {name: oldName}),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
}); });
} }
@ -1578,12 +1669,26 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
}).then((result)=>{ }).then((result)=>{
if (result.error) { if (result.error) {
KTApp.hidePageLoading(); KTApp.hidePageLoading();
showToast(2, "fs.rename.err_generic", { name: oldName }); ModalAlert.fire({
text: $.t('fs.rename.err_generic', { name: oldName }),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
return; return;
} }
if (result.data.length > 0){ if (result.data.length > 0){
KTApp.hidePageLoading(); KTApp.hidePageLoading();
showToast(2, "fs.rename.err_exists", { name: oldName }); ModalAlert.fire({
text: $.t('fs.rename.err_exists', { name: oldName }),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
return; return;
} }
executeRename(); executeRename();

View file

@ -205,7 +205,14 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
if (!errorMessage){ if (!errorMessage){
errorMessage = "general.delete_error_generic"; errorMessage = "general.delete_error_generic";
} }
showToast(2, errorMessage); ModalAlert.fire({
text: $.t(errorMessage),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
}); });
} }
}); });