Merge branch 'feature/upload-progress' into 'dev'

Upload progress bar improvements

See merge request crafty-controller/crafty-4!546
This commit is contained in:
Iain Powrie 2023-02-11 18:19:19 +00:00
commit 70563f3554
8 changed files with 190 additions and 141 deletions

View File

@ -1,11 +1,12 @@
# Changelog # Changelog
## --- [4.0.21] - 2023/TBD ## --- [4.0.21] - 2023/TBD
### New features ### New features
TBD - Add better feedback for uploads with a progress bar ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/546))
### Bug fixes ### Bug fixes
- Fix exception related to page data on server start ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/544)) - Fix exception related to page data on server start ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/544))
### Tweaks ### Tweaks
- Cleanup authentication helpers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/545)) - Cleanup authentication helpers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/545))
- Optimize file upload progress WS ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/546))
### Lang ### Lang
- Add additional translations to backups page strings ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/543)) - Add additional translations to backups page strings ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/543))
<br><br> <br><br>

View File

@ -52,18 +52,19 @@ class UploadHandler(BaseHandler):
f"User with ID {user_id} attempted to upload a file that" f"User with ID {user_id} attempted to upload a file that"
f" exceeded the max body size." f" exceeded the max body size."
) )
self.helper.websocket_helper.broadcast_user(
user_id, return self.finish_json(
"send_start_error", 413,
{ {
"error": self.helper.translation.translate( "status": "error",
"error": "TOO LARGE",
"info": self.helper.translation.translate(
"error", "error",
"fileTooLarge", "fileTooLarge",
self.controller.users.get_user_lang_by_id(user_id), self.controller.users.get_user_lang_by_id(user_id),
), ),
}, },
) )
return
self.do_upload = True self.do_upload = True
if superuser: if superuser:
@ -141,48 +142,50 @@ class UploadHandler(BaseHandler):
f"User with ID {user_id} attempted to upload a file that" f"User with ID {user_id} attempted to upload a file that"
f" exceeded the max body size." f" exceeded the max body size."
) )
self.helper.websocket_helper.broadcast_user(
user_id, return self.finish_json(
"send_start_error", 413,
{ {
"error": self.helper.translation.translate( "status": "error",
"error": "TOO LARGE",
"info": self.helper.translation.translate(
"error", "error",
"fileTooLarge", "fileTooLarge",
self.controller.users.get_user_lang_by_id(user_id), self.controller.users.get_user_lang_by_id(user_id),
), ),
}, },
) )
return
self.do_upload = True self.do_upload = True
if not superuser: if not superuser:
self.helper.websocket_helper.broadcast_user( return self.finish_json(
user_id, 401,
"send_start_error",
{ {
"error": self.helper.translation.translate( "status": "error",
"error": "UNAUTHORIZED ACCESS",
"info": self.helper.translation.translate(
"error", "error",
"superError", "superError",
self.controller.users.get_user_lang_by_id(user_id), self.controller.users.get_user_lang_by_id(user_id),
), ),
}, },
) )
return
if not self.request.headers.get("X-Content-Type", None).startswith( if not self.request.headers.get("X-Content-Type", None).startswith(
"image/" "image/"
): ):
self.helper.websocket_helper.broadcast_user(
user_id, return self.finish_json(
"send_start_error", 415,
{ {
"error": self.helper.translation.translate( "status": "error",
"error": "TYPE ERROR",
"info": self.helper.translation.translate(
"error", "error",
"fileError", "fileError",
self.controller.users.get_user_lang_by_id(user_id), self.controller.users.get_user_lang_by_id(user_id),
), ),
}, },
) )
return
if user_id is None: if user_id is None:
logger.warning("User ID not found in upload handler call") logger.warning("User ID not found in upload handler call")
Console.warning("User ID not found in upload handler call") Console.warning("User ID not found in upload handler call")
@ -219,18 +222,19 @@ class UploadHandler(BaseHandler):
f"User with ID {user_id} attempted to upload a file that" f"User with ID {user_id} attempted to upload a file that"
f" exceeded the max body size." f" exceeded the max body size."
) )
self.helper.websocket_helper.broadcast_user(
user_id, return self.finish_json(
"send_start_error", 413,
{ {
"error": self.helper.translation.translate( "status": "error",
"error": "TOO LARGE",
"info": self.helper.translation.translate(
"error", "error",
"fileTooLarge", "fileTooLarge",
self.controller.users.get_user_lang_by_id(user_id), self.controller.users.get_user_lang_by_id(user_id),
), ),
}, },
) )
return
self.do_upload = True self.do_upload = True
if superuser: if superuser:

View File

@ -6,7 +6,8 @@
{% block title %}Crafty Controller - {{ translate('panelConfig', 'pageTitle', data['lang']) }}{% end %} {% block title %}Crafty Controller - {{ translate('panelConfig', 'pageTitle', data['lang']) }}{% end %}
{% block content %} {% block content %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/css/bootstrap-select.min.css"> <link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/css/bootstrap-select.min.css">
<div class="content-wrapper"> <div class="content-wrapper">
@ -71,7 +72,8 @@
<button type="button" class="btn btn-outline-default custom-picker" onclick="$('option', $('#lang_select')).each(function(element) { <button type="button" class="btn btn-outline-default custom-picker" onclick="$('option', $('#lang_select')).each(function(element) {
$(this).removeAttr('selected').prop('selected', false); $('.selectpicker').selectpicker('refresh') $(this).removeAttr('selected').prop('selected', false); $('.selectpicker').selectpicker('refresh')
});">Enable all Languages</button> });">Enable all Languages</button>
<select id="lang_select" class="form-control selectpicker show-tick" data-icon-base="fas" data-tick-icon="fa-check" multiple data-style="custom-picker"> <select id="lang_select" class="form-control selectpicker show-tick" data-icon-base="fas"
data-tick-icon="fa-check" multiple data-style="custom-picker">
{% for lang in data['all_languages'] %} {% for lang in data['all_languages'] %}
{% if lang in item[1] %} {% if lang in item[1] %}
<option selected>{{lang}}</option> <option selected>{{lang}}</option>
@ -80,10 +82,13 @@
{% end %} {% end %}
{% end %} {% end %}
</select> </select>
<textarea id="disabled_lang" name="{{item[0]}}" class="form-control list hidden" rows="{{ len(data['all_languages']) }}" value="{{','.join(item[1])}}" hidden>{{','.join(item[1])}}</textarea> <textarea id="disabled_lang" name="{{item[0]}}" class="form-control list hidden"
rows="{{ len(data['all_languages']) }}" value="{{','.join(item[1])}}"
hidden>{{','.join(item[1])}}</textarea>
</div> </div>
{% elif isinstance(item[1], list) %} {% elif isinstance(item[1], list) %}
<textarea value="{{','.join(item[1])}}" type="text" name="{{item[0]}}" class="form-control list">{{','.join(item[1])}}</textarea> <textarea value="{{','.join(item[1])}}" type="text" name="{{item[0]}}"
class="form-control list">{{','.join(item[1])}}</textarea>
{% elif isinstance(item[1], bool) %} {% elif isinstance(item[1], bool) %}
<div style="margin-left: 30px;"> <div style="margin-left: 30px;">
{% if item[1] == True %} {% if item[1] == True %}
@ -99,9 +104,11 @@
{% end %} {% end %}
</div> </div>
{% elif isinstance(item[1], int) %} {% elif isinstance(item[1], int) %}
<input type="number" class="form-control" name="{{item[0]}}" id="{{item[0]}}" value="{{ item[1] }}" step="1" min="0" required> <input type="number" class="form-control" name="{{item[0]}}" id="{{item[0]}}" value="{{ item[1] }}"
step="1" min="0" required>
{% else %} {% else %}
<input type="text" class="form-control" name="{{item[0]}}" id="{{item[0]}}" value="{{ item[1] }}" step="2" min="0" required> <input type="text" class="form-control" name="{{item[0]}}" id="{{item[0]}}" value="{{ item[1] }}"
step="2" min="0" required>
{% end %} {% end %}
</div> </div>
{% end %} {% end %}
@ -276,44 +283,6 @@
}, },
}); });
}) })
var file;
function sendFile() {
file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>'
let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf")
let fileName = file.name
let target = '/upload'
let mimeType = file.type
let size = file.size
let type = 'background'
xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
xmlHttpRequest.setRequestHeader('X-Content-Length', size);
xmlHttpRequest.setRequestHeader('X-Content-Disposition', 'attachment; filename="' + fileName + '"');
xmlHttpRequest.setRequestHeader('X-Content-Upload-Type', type);
xmlHttpRequest.setRequestHeader('X-FileName', fileName);
xmlHttpRequest.addEventListener('load', (event) => {
if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!')
document.getElementById("upload_input").innerHTML = '<div class="card-header header-sm d-flex justify-content-between align-items-center"><span id="file-uploaded" style="color: gray;">' + fileName + '</span> 🔒</div>';
setTimeout(function () {
window.location.reload();
}, 2000);
}
else {
alert('Upload failed with response: ' + event.target.responseText);
doUpload = false;
}
}, false);
xmlHttpRequest.addEventListener('error', (e) => {
console.error('Error while uploading file', file.name + '.', 'Event:', e)
}, false);
xmlHttpRequest.send(file);
}
</script> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/js/bootstrap-select.min.js"> <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.13.10/js/bootstrap-select.min.js">
</script> </script>

View File

@ -352,7 +352,7 @@
var file; var file;
function sendFile() { function sendFile() {
file = $("#file")[0].files[0] file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>'; document.getElementById("upload_input").innerHTML = '<div class="progress"><div id="upload-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>';
let xmlHttpRequest = new XMLHttpRequest(); let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf") let token = getCookie("_xsrf")
let fileName = file.name let fileName = file.name
@ -361,6 +361,15 @@
let size = file.size let size = file.size
let type = 'background' let type = 'background'
xmlHttpRequest.upload.addEventListener('progress', function (e) {
if (e.loaded <= size) {
var percent = Math.round(e.loaded / size * 100);
$(`#upload-progress-bar`).css('width', percent + '%');
$(`#upload-progress-bar`).html(percent + '%');
}
});
xmlHttpRequest.open('POST', target, true); xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType); xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token); xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
@ -377,7 +386,15 @@
}, 2000); }, 2000);
} }
else { else {
alert('Upload failed with response: ' + event.target.responseText); let response_text = JSON.parse(event.target.responseText);
var x = document.querySelector('.bootbox');
console.log(JSON.parse(event.target.responseText).info)
bootbox.alert({
message: JSON.parse(event.target.responseText).info,
callback: function () {
window.location.reload();
}
});
doUpload = false; doUpload = false;
} }
}, false); }, false);

View File

@ -705,6 +705,15 @@
let mimeType = file.type let mimeType = file.type
let size = file.size let size = file.size
xmlHttpRequest.upload.addEventListener('progress', function (e) {
if (e.loaded <= size) {
var percent = Math.round(e.loaded / size * 100);
$(`#upload-progress-bar-${i + 1}`).css('width', percent + '%');
$(`#upload-progress-bar-${i + 1}`).html(percent + '%');
}
});
xmlHttpRequest.open('POST', target, true); xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType); xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token); xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
@ -751,7 +760,22 @@
$(`#upload-progress-bar-${i + 1}`).html('<i style="color: black;" class="fas fa-box-check"></i>') $(`#upload-progress-bar-${i + 1}`).html('<i style="color: black;" class="fas fa-box-check"></i>')
} }
else { else {
alert('Upload failed with response: ' + event.target.responseText); let response_text = JSON.parse(event.target.responseText);
var x = document.querySelector('.bootbox');
if (x) {
x.remove()
}
var x = document.querySelector('.modal-content');
if (x) {
x.remove()
}
console.log(JSON.parse(event.target.responseText).info)
bootbox.alert({
message: JSON.parse(event.target.responseText).info,
callback: function () {
window.location.reload();
}
});
doUpload = false; doUpload = false;
} }
}, false); }, false);
@ -788,7 +812,7 @@
var height = files.files.length * 50; var height = files.files.length * 50;
var waitMessage = '<p class="text-center mb-0">' + var waitMessage = '<p class="text-center mb-0">' +
'<i class="fa fa-spin fa-cog"></i>' + '<i class="fa fa-spin fa-cog"></i>&nbsp;' +
"{{ translate('serverFiles', 'waitUpload', data['lang']) }}" + '<br>' + "{{ translate('serverFiles', 'waitUpload', data['lang']) }}" + '<br>' +
'<strong>' + "{{ translate('serverFiles', 'stayHere', data['lang']) }}" + '</strong>' + '<strong>' + "{{ translate('serverFiles', 'stayHere', data['lang']) }}" + '</strong>' +
'</p>' + '</p>' +
@ -826,7 +850,7 @@
await sendFile(files.files[i], path, serverId, nFiles - i - 1, i, (progress) => { await sendFile(files.files[i], path, serverId, nFiles - i - 1, i, (progress) => {
$(`#upload-progress-bar-${i + 1}`).attr('aria-valuenow', progress) $(`#upload-progress-bar-${i + 1}`).attr('aria-valuenow', progress)
$(`#upload-progress-bar-${i + 1}`).css('width', progress + '%') $(`#upload-progress-bar-${i + 1}`).css('width', progress + '%');
}); });
} }
hideUploadBox(); hideUploadBox();

View File

@ -562,7 +562,7 @@
var file; var file;
function sendFile() { function sendFile() {
file = $("#file")[0].files[0] file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>' document.getElementById("upload_input").innerHTML = '<div class="progress"><div id="upload-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>'
let xmlHttpRequest = new XMLHttpRequest(); let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf") let token = getCookie("_xsrf")
let fileName = encodeURIComponent(file.name) let fileName = encodeURIComponent(file.name)
@ -571,6 +571,15 @@
let size = file.size let size = file.size
let type = 'server_import' let type = 'server_import'
xmlHttpRequest.upload.addEventListener('progress', function (e) {
if (e.loaded <= size) {
var percent = Math.round(e.loaded / size * 100);
$(`#upload-progress-bar`).css('width', percent + '%');
$(`#upload-progress-bar`).html(percent + '%');
}
});
xmlHttpRequest.open('POST', target, true); xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType); xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token); xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
@ -585,7 +594,15 @@
document.getElementById("lower_half").style.visibility = "visible"; document.getElementById("lower_half").style.visibility = "visible";
} }
else { else {
alert('Upload failed with response: ' + event.target.responseText); let response_text = JSON.parse(event.target.responseText);
var x = document.querySelector('.bootbox');
console.log(JSON.parse(event.target.responseText).info)
bootbox.alert({
message: JSON.parse(event.target.responseText).info,
callback: function () {
window.location.reload();
}
});
doUpload = false; doUpload = false;
} }
}, false); }, false);

View File

@ -804,7 +804,7 @@
var file; var file;
function sendFile() { function sendFile() {
file = $("#file")[0].files[0] file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>' document.getElementById("upload_input").innerHTML = '<div class="progress"><div id="upload-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>'
let xmlHttpRequest = new XMLHttpRequest(); let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf") let token = getCookie("_xsrf")
let fileName = file.name let fileName = file.name
@ -813,6 +813,15 @@
let size = file.size let size = file.size
let type = 'server_import' let type = 'server_import'
xmlHttpRequest.upload.addEventListener('progress', function (e) {
if (e.loaded <= size) {
var percent = Math.round(e.loaded / size * 100);
$(`#upload-progress-bar`).css('width', percent + '%');
$(`#upload-progress-bar`).html(percent + '%');
}
});
xmlHttpRequest.open('POST', target, true); xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType); xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token); xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
@ -827,7 +836,15 @@
document.getElementById("lower_half").style.visibility = "visible"; document.getElementById("lower_half").style.visibility = "visible";
} }
else { else {
alert('Upload failed with response: ' + event.target.responseText); let response_text = JSON.parse(event.target.responseText);
var x = document.querySelector('.bootbox');
console.log(JSON.parse(event.target.responseText).info)
bootbox.alert({
message: JSON.parse(event.target.responseText).info,
callback: function () {
window.location.reload();
}
});
doUpload = false; doUpload = false;
} }
}, false); }, false);