sftpgo-mirror/templates/webclient/shares.html
Nicola Murino 32a4a753f9
Some checks are pending
Code scanning - action / CodeQL-Build (push) Waiting to run
CI / golangci-lint (push) Waiting to run
CI / Test and deploy (push) Waiting to run
CI / Test build flags (push) Waiting to run
CI / Test with PgSQL/MySQL/Cockroach (push) Waiting to run
CI / Build Linux packages (push) Waiting to run
Docker / Build (push) Waiting to run
WebClient: improve readability of upload progress
Fixes #1773

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
2024-10-03 20:31:46 +02:00

510 lines
No EOL
24 KiB
HTML

<!--
Copyright (C) 2023 Nicola Murino
This WebUI uses the KeenThemes Mega Bundle, a proprietary theme:
https://keenthemes.com/products/templates-mega-bundle
KeenThemes HTML/CSS/JS components are allowed for use only within the
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" .}}
{{- define "extra_css"}}
<link href="{{.StaticURL}}/assets/plugins/custom/datatables/datatables.bundle.css" rel="stylesheet" type="text/css"/>
{{- end}}
{{- define "page_body"}}
{{- template "errmsg" ""}}
<div class="card shadow-sm">
<div class="card-header bg-light">
<h3 data-i18n="share.view_manage" class="card-title section-title">View and manage shares</h3>
</div>
<div id="card_body" class="card-body">
<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>
<div class="d-flex justify-content-end my-2" data-table-toolbar="base">
<a href="{{.ShareURL}}" class="btn btn-primary">
<i class="ki-duotone ki-plus fs-2"></i>
<span data-i18n="general.add">Add</span>
</a>
</div>
</div>
<table id="dataTable" class="table align-middle table-row-dashed fs-6 gy-5">
<thead>
<tr class="text-start text-muted fw-bold fs-6 gs-0">
<th data-i18n="general.name">Name</th>
<th data-i18n="share.scope">Scope</th>
<th data-i18n="general.info">Info</th>
<th class="min-w-100px"></th>
</tr>
</thead>
<tbody id="table_body" class="text-gray-800 fw-semibold"></tbody>
</table>
</div>
</div>
</div>
{{- end}}
{{- define "modals"}}
<div class="modal fade" id="link_modal" tabindex="-1">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header border-0">
<h3 data-i18n="share.access_links_title" class="modal-title">
Share access links
</h3>
<div data-i18n="[aria-label]general.close" class="btn btn-icon btn-sm btn-active-light-primary" data-bs-dismiss="modal" aria-label="Close">
<i class="ki-solid ki-cross fs-2x text-gray-700"></i>
</div>
</div>
<div class="modal-body fs-5">
<div id="readShare" class="mb-5">
<div class="mb-3">
<h4 data-i18n="share.link_single_title">Single zip file</h4>
<p data-i18n="share.link_single_desc">You can download shared content as a single zip file</p>
<div class="d-flex">
<button id="readLinkCopy" type="button" class="btn btn-flex btn-light-primary btn-clipboard-copy me-3">
<i class="ki-duotone ki-fasten fs-2">
<span class="path1"></span>
<span class="path2"></span>
</i>
<span data-i18n="general.copy_link">Copy link</span>
</button>
<a id="readLink" href="#" target="_blank" rel="noopener noreferrer" type="button" class="btn btn-flex btn-primary">
<i class="ki-duotone ki-folder-down fs-2">
<span class="path1"></span>
<span class="path2"></span>
</i>
<span data-i18n="fs.download">Download</span>
</a>
</div>
</div>
<hr>
<div class="mb-3 mt-10">
<h4 data-i18n="share.link_dir_title">Single directory</h4>
<p data-i18n="share.link_dir_desc">If the share consists of a single directory you can browse and download files</p>
<button id="readBrowseLinkCopy" data-clipboard-target="#readBrowseLink" type="button" class="btn btn-flex btn-light-primary btn-clipboard-copy me-3">
<i class="ki-duotone ki-fasten fs-2">
<span class="path1"></span>
<span class="path2"></span>
</i>
<span data-i18n="general.copy_link">Copy link</span>
</button>
<a id="readBrowseLink" href="#" target="_blank" rel="noopener noreferrer" type="button" class="btn btn-flex btn-primary">
<i class="ki-duotone ki-arrow-up-right fs-2">
<span class="path1"></span>
<span class="path2"></span>
</i>
<span data-i18n="share.go">Go to share</span>
</a>
</div>
<hr>
<div class="mt-10">
<h4 data-i18n="share.link_uncompressed_title">Uncompressed file</h4>
<p data-i18n="share.link_uncompressed_desc">If the share consists of a single file you can download it uncompressed</p>
<button id="readUncompressedLinkCopy" data-clipboard-target="#readUncompressedLink" type="button" class="btn btn-flex btn-light-primary btn-clipboard-copy me-3">
<i class="ki-duotone ki-fasten fs-2">
<span class="path1"></span>
<span class="path2"></span>
</i>
<span data-i18n="general.copy_link">Copy link</span>
</button>
<a id="readUncompressedLink" href="#" target="_blank" rel="noopener noreferrer" type="button" class="btn btn-flex btn-primary">
<i class="ki-duotone ki-folder-down fs-2">
<span class="path1"></span>
<span class="path2"></span>
</i>
<span data-i18n="fs.download">Download</span>
</a>
</div>
</div>
<div id="writeShare" class="mb-5">
<p data-i18n="share.upload_desc">You can upload one or more files to the shared directory</p>
<button id="writePageLinkCopy" data-clipboard-target="#writePageLink" type="button" class="btn btn-flex btn-light-primary btn-clipboard-copy me-3">
<i class="ki-duotone ki-fasten fs-2">
<span class="path1"></span>
<span class="path2"></span>
</i>
<span data-i18n="general.copy_link">Copy link</span>
</button>
<a id="writePageLink" href="#" target="_blank" rel="noopener noreferrer" type="button" class="btn btn-flex btn-primary">
<i class="ki-duotone ki-folder-up fs-2">
<span class="path1"></span>
<span class="path2"></span>
</i>
<span data-i18n="fs.upload.text">Upload</span>
</a>
</div>
<div data-i18n="share.expired_desc" id="expiredShare" class="fw-semibold fs-4 mb-5">
This share is no longer accessible because it has expired
</div>
</div>
</div>
</div>
</div>
{{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(shareID) {
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){
clearLoading();
KTApp.showPageLoading();
let path = '{{.ShareURL}}' + "/" + encodeURIComponent(shareID);
axios.delete(path, {
timeout: 15000,
headers: {
'X-CSRF-TOKEN': '{{.CSRFToken}}'
},
validateStatus: function (status) {
return status == 200;
}
}).then(function(response){
location.reload();
}).catch(function(error){
KTApp.hidePageLoading();
let errorMessage;
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";
}
ModalAlert.fire({
text: $.t(errorMessage),
icon: "warning",
confirmButtonText: $.t('general.ok'),
customClass: {
confirmButton: "btn btn-primary"
}
});
});
}
});
}
function editAction(shareID) {
window.location.replace('{{.ShareURL}}' + "/" + encodeURIComponent(shareID));
}
function showShareLink(shareID, shareScope, expiresAt) {
if (expiresAt < Date.now()) {
$('#expiredShare').show();
$('#writeShare').hide();
$('#readShare').hide();
} else {
let shareURL = '{{.BasePublicSharesURL}}' + "/" + encodeURIComponent(shareID);
if (shareScope == '1') {
$('#expiredShare').hide();
$('#writeShare').hide();
$('#readShare').show();
$('#readLink').attr("href", shareURL + "/download");
$('#readLink').attr("title", shareURL + "/download");
$('#readLinkCopy').attr("data-clipboard-text",getCurrentURI()+shareURL + "/download");
$('#readUncompressedLink').attr("href", shareURL + "/download?compress=false");
$('#readUncompressedLink').attr("title", shareURL + "/download?compress=false");
$('#readUncompressedLinkCopy').attr("data-clipboard-text",getCurrentURI()+shareURL + "/download?compress=false");
$('#readBrowseLink').attr("href", shareURL + "/browse");
$('#readBrowseLink').attr("title", shareURL + "/browse");
$('#readBrowseLinkCopy').attr("data-clipboard-text",getCurrentURI()+shareURL + "/browse");
} else {
$('#expiredShare').hide();
$('#writeShare').show();
$('#readShare').hide();
$('#writePageLink').attr("href", shareURL + "/upload");
$('#writePageLink').attr("title", shareURL + "/upload");
$('#writePageLinkCopy').attr("data-clipboard-text",getCurrentURI()+shareURL + "/upload");
}
}
$('#link_modal').modal('show');
}
var sharesDatatable = function(){
var dt;
var initDatatable = function () {
dt = $('#dataTable').DataTable({
ajax: {
url: "{{.SharesURL}}/json",
dataSrc: "",
error: function ($xhr, textStatus, errorThrown) {
$(".dt-processing").hide();
$('#loader').addClass("d-none");
let txt = "";
if ($xhr) {
let json = $xhr.responseJSON;
if (json) {
if (json.message){
txt = json.message;
}
}
}
if (!txt){
txt = "general.error500";
}
setI18NData($('#errorTxt'), txt);
$('#errorMsg').removeClass("d-none");
}
},
columns: [
{
data: "name",
render: function(data, type, row) {
if (type === 'display') {
return escapeHTML(data);
}
return data;
}
},
{
data: "scope",
render: function (data, type, row) {
if (type === 'display') {
switch (data){
case 2:
return $.t('share.scope_write');
case 3:
return $.t('share.scope_read_write');
default:
return $.t('share.scope_read');
}
}
return data;
}
},
{
data: "expires_at",
defaultContent: 0,
searchable: false,
orderable: false,
render: function (data, type, row) {
if (type === 'display') {
let info = "";
if (row.expires_at && row.expires_at > 0){
info+= $.t('share.expiration_date', {
val: row.expires_at,
formatParams: {
val: { year: 'numeric', month: 'numeric', day: 'numeric' },
}
});
}
if (row.last_use_at && row.last_use_at > 0){
info+= $.t('share.last_use', {
val: row.last_use_at,
formatParams: {
val: { year: 'numeric', month: 'numeric', day: 'numeric' },
}
});
}
let used_tokens = 0;
if (row.used_tokens && row.used_tokens > 0){
used_tokens = row.used_tokens;
}
if (row.max_tokens && row.max_tokens > 0){
info+= $.t('share.usage', {used: used_tokens, total: row.max_tokens});
} else {
info+= $.t('share.used_tokens', {used: used_tokens});
}
if (row.password){
info+= $.t('share.password_protected')
}
return info;
}
return data;
}
},
{
data: "id",
searchable: false,
orderable: false,
className: 'text-end',
render: function (data, type, row) {
if (type === 'display') {
return `<div class="d-flex justify-content-end">
<div class="ms-2">
<a href="#" class="btn btn-sm btn-icon btn-light btn-active-light-primary" data-table-action="show_link">
<i class="ki-duotone ki-fasten fs-5 m-0">
<span class="path1"></span>
<span class="path2"></span>
</i>
</a>
</div>
<div class="ms-2">
<button type="button" class="btn btn-sm btn-icon btn-light btn-active-light-primary"
data-kt-menu-trigger="click" data-kt-menu-placement="bottom-end">
<i class="ki-duotone ki-dots-square fs-5 m-0">
<span class="path1"></span>
<span class="path2"></span>
<span class="path3"></span>
<span class="path4"></span>
</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-150px py-4" data-kt-menu="true">
<div class="menu-item px-3">
<a data-i18n="general.edit" href="#" class="menu-link px-3" data-table-action="edit_row">Edit</a>
</div>
<div class="menu-item px-3">
<a data-i18n="general.delete" href="#" class="menu-link text-danger px-3" data-table-action="delete_row">Delete</a>
</div>
</div>
</div>
</div>`;
}
return "";
}
}
],
deferRender: true,
stateSave: true,
stateDuration: 0,
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('share.no_share')
},
order: [[1, '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();
}
});
dt.on('draw', drawAction);
}
function drawAction() {
KTMenu.createInstances();
handleRowActions();
$('#table_body').localize();
}
var handleSearchDatatable = function () {
const filterSearch = $(document.querySelector('[data-table-filter="search"]'));
filterSearch.off("keyup");
filterSearch.on('keyup', function (e) {
dt.rows().deselect();
dt.search(e.target.value).draw();
});
}
function handleRowActions() {
const editButtons = document.querySelectorAll('[data-table-action="edit_row"]');
editButtons.forEach(d => {
let el = $(d);
el.off("click");
el.on("click", function(e){
e.preventDefault();
const parent = e.target.closest('tr');
editAction(dt.row(parent).data()["id"]);
});
});
const deleteButtons = document.querySelectorAll('[data-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()["id"]);
});
});
const showLinkButtons = document.querySelectorAll('[data-table-action="show_link"]');
showLinkButtons.forEach(d => {
let el = $(d);
el.off("click");
el.on("click", function(e){
e.preventDefault();
let rowData = dt.row(e.target.closest('tr')).data();
showShareLink(rowData["id"], rowData["scope"], rowData["expires_at"]);
});
});
}
return {
init: function () {
initDatatable();
handleSearchDatatable();
}
}
}();
$(document).on("i18nshow", function(){
sharesDatatable.init();
var clipboard = new ClipboardJS('.btn-clipboard-copy',{
container: document.getElementById('link_modal')
});
clipboard.on('success', function (e) {
e.trigger.querySelectorAll('span').forEach(spanEl => {
if (spanEl.getAttribute('data-i18n')){
e.trigger.classList.remove("btn-light-primary");
e.trigger.classList.add("btn-success");
setI18NData($(spanEl),"general.copied");
setTimeout(function(){
e.trigger.classList.remove("btn-success");
e.trigger.classList.add("btn-light-primary");
setI18NData($(spanEl),"general.copy_link");
}, 3000)
}
});
e.clearSelection();
});
});
</script>
{{end}}