all lists on lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [RFC manager] fix #3248: GUI: storage: upload multiple files
@ 2022-06-29 12:23 Matthias Heiserer
  2022-07-01 14:21 ` Dominik Csapak
  0 siblings, 1 reply; 4+ messages in thread
From: Matthias Heiserer @ 2022-06-29 12:23 UTC (permalink / raw)
  To: pve-devel

Allows queueing multiple files for upload to the storage, which wasn't
possible with the old upload window.

Signed-off-by: Matthias Heiserer <m.heiserer@proxmox.com>
---
 www/manager6/window/UploadToStorage.js | 393 +++++++++++++------------
 1 file changed, 210 insertions(+), 183 deletions(-)

diff --git a/www/manager6/window/UploadToStorage.js b/www/manager6/window/UploadToStorage.js
index 0de6d89d..bf656164 100644
--- a/www/manager6/window/UploadToStorage.js
+++ b/www/manager6/window/UploadToStorage.js
@@ -1,3 +1,13 @@
+Ext.define('pve-multiupload', {
+	extend: 'Ext.data.Model',
+	fields: [
+	    'file', 'filename', 'progressWidget', 'hashsum', 'hashWidget',
+	    'xhr', 'mimetype', 'size',
+	    {
+		name: 'hash', defaultValue: '__default__',
+	    },
+	],
+});
 Ext.define('PVE.window.UploadToStorage', {
     extend: 'Ext.window.Window',
     alias: 'widget.pveStorageUpload',
@@ -27,93 +37,102 @@ Ext.define('PVE.window.UploadToStorage', {
 
     viewModel: {
 	data: {
-	    size: '-',
-	    mimetype: '-',
-	    filename: '',
+	    hasFiles: false,
+	    uploadInProgress: false,
 	},
     },
-
     controller: {
-	submit: function(button) {
-	    const view = this.getView();
-	    const form = this.lookup('formPanel').getForm();
-	    const abortBtn = this.lookup('abortBtn');
-	    const pbar = this.lookup('progressBar');
-
-	    const updateProgress = function(per, bytes) {
-		let text = (per * 100).toFixed(2) + '%';
-		if (bytes) {
-		    text += " (" + Proxmox.Utils.format_size(bytes) + ')';
-		}
-		pbar.updateProgress(per, text);
-	    };
 
+	addFile: function(input) {
+	    let me = this;
+	    let grid = me.lookup('grid');
+	    for (const file of input.fileInputEl.dom.files) {
+		grid.store.add({
+		    file: file,
+		    filename: file.name,
+		    mimetype: Proxmox.Utils.format_size(file.size),
+		    size: file.type,
+		});
+	    }
+	},
+
+	currentUploadIndex: 1,
+	startUpload: function() {
+	    const me = this;
+	    const view = me.getView();
+	    const grid = me.lookup('grid');
+	    view.taskDone();
+
+	    const last = grid.store.last();
+	    if (!last) {
+		me.getViewModel().set('uploadInProgress', false);
+		return;
+	    }
+	    const endId = parseInt(last.id.replace('pve-multiupload-', ''), 10);
+	    let record;
+	    while (!record && me.currentUploadIndex <= endId) {
+		record = grid.store.getById(`pve-multiupload-${me.currentUploadIndex++}`);
+	    }
+
+	    if (!record) {
+		me.getViewModel().set('uploadInProgress', false);
+		return;
+	    }
+
+	    const data = record.data;
 	    const fd = new FormData();
-
-	    button.setDisabled(true);
-	    abortBtn.setDisabled(false);
-
 	    fd.append("content", view.content);
-
-	    const fileField = form.findField('file');
-	    const file = fileField.fileInputEl.dom.files[0];
-	    fileField.setDisabled(true);
-
-	    const filenameField = form.findField('filename');
-	    const filename = filenameField.getValue();
-	    filenameField.setDisabled(true);
-
-	    const algorithmField = form.findField('checksum-algorithm');
-	    algorithmField.setDisabled(true);
-	    if (algorithmField.getValue() !== '__default__') {
-		fd.append("checksum-algorithm", algorithmField.getValue());
-
-		const checksumField = form.findField('checksum');
-		fd.append("checksum", checksumField.getValue()?.trim());
-		checksumField.setDisabled(true);
+	    if (data.hash !== '__default__') {
+		fd.append("checksum-algorithm", data.hash);
+		fd.append("checksum", data.hashsum.trim());
 	    }
+	    fd.append("filename", data.file, data.filename);
 
-	    fd.append("filename", file, filename);
-
-	    pbar.setVisible(true);
-	    updateProgress(0);
-
-	    const xhr = new XMLHttpRequest();
-	    view.xhr = xhr;
 
+	    const xhr = data.xhr = new XMLHttpRequest();
 	    xhr.addEventListener("load", function(e) {
 		if (xhr.status === 200) {
-		    view.hide();
-
 		    const result = JSON.parse(xhr.response);
 		    const upid = result.data;
 		    Ext.create('Proxmox.window.TaskViewer', {
 			autoShow: true,
 			upid: upid,
-			taskDone: view.taskDone,
+			taskDone: function(success) {
+			    if (success) {
+				this.close();
+			    } else {
+				const widget = record.get('progressWidget');
+				widget.updateProgress(0, "ERROR");
+				widget.setStyle('background-color', 'red');
+			    }
+			},
 			listeners: {
 			    destroy: function() {
-				view.close();
+				me?.startUpload?.();
 			    },
 			},
 		    });
+		} else {
+		    const widget = record.get('progressWidget');
+		    widget.updateProgress(0, `ERROR: ${xhr.status}`);
+		    widget.setStyle('background-color', 'red');
+		    me.getViewModel().set('uploadInProgress', false);
+		}
+	    });
 
-		    return;
+	    const updateProgress = function(per, bytes) {
+		let text = (per * 100).toFixed(2) + '%';
+		if (bytes) {
+		    text += " (" + Proxmox.Utils.format_size(bytes) + ')';
 		}
-		const err = Ext.htmlEncode(xhr.statusText);
-		let msg = `${gettext('Error')} ${xhr.status.toString()}: ${err}`;
-		if (xhr.responseText !== "") {
-		    const result = Ext.decode(xhr.responseText);
-		    result.message = msg;
-		    msg = Proxmox.Utils.extractRequestError(result, true);
-		}
-		Ext.Msg.alert(gettext('Error'), msg, btn => view.close());
-	    }, false);
+		let widget = record.get('progressWidget');
+		widget?.updateProgress(per, text);
+	    };
 
 	    xhr.addEventListener("error", function(e) {
 		const err = e.target.status.toString();
 		const msg = `Error '${err}' occurred while receiving the document.`;
-		Ext.Msg.alert(gettext('Error'), msg, btn => view.close());
+		Ext.Msg.alert(gettext('Error'), msg, _ => view.close());
 	    });
 
 	    xhr.upload.addEventListener("progress", function(evt) {
@@ -123,173 +142,181 @@ Ext.define('PVE.window.UploadToStorage', {
 		}
 	    }, false);
 
+	    me.getViewModel().set('uploadInProgress', true);
 	    xhr.open("POST", `/api2/json${view.url}`, true);
 	    xhr.send(fd);
 	},
 
-	validitychange: function(f, valid) {
-	    const submitBtn = this.lookup('submitBtn');
-	    submitBtn.setDisabled(!valid);
-	},
-
-	fileChange: function(input) {
-	    const vm = this.getViewModel();
-	    const name = input.value.replace(/^.*(\/|\\)/, '');
-	    const fileInput = input.fileInputEl.dom;
-	    vm.set('filename', name);
-	    vm.set('size', (fileInput.files[0] && Proxmox.Utils.format_size(fileInput.files[0].size)) || '-');
-	    vm.set('mimetype', (fileInput.files[0] && fileInput.files[0].type) || '-');
-	},
-
-	hashChange: function(field, value) {
-	    const checksum = this.lookup('downloadUrlChecksum');
-	    if (value === '__default__') {
-		checksum.setDisabled(true);
-		checksum.setValue("");
-	    } else {
-		checksum.setDisabled(false);
-	    }
+	removeFile: function(_view, _rowIndex, _colIndex, _item, _event, record) {
+	    let me = this;
+	    me.lookup('grid').store.remove(record);
+	    me.getViewModel().set('uploadInProgress', false);
 	},
     },
 
     items: [
 	{
-	    xtype: 'form',
-	    reference: 'formPanel',
-	    method: 'POST',
-	    waitMsgTarget: true,
-	    bodyPadding: 10,
-	    border: false,
-	    width: 400,
-	    fieldDefaults: {
-		labelWidth: 100,
-		anchor: '100%',
-            },
-	    items: [
-		{
-		    xtype: 'filefield',
-		    name: 'file',
-		    buttonText: gettext('Select File'),
-		    allowBlank: false,
-		    fieldLabel: gettext('File'),
-		    cbind: {
-			accept: '{extensions}',
-		    },
-		    listeners: {
-			change: 'fileChange',
+	    xtype: 'grid',
+	    reference: 'grid',
+	    height: 700,
+	    width: 1100,
+	    store: {
+		listeners: {
+		    remove: function(_store, records) {
+			records.forEach(record => {
+			    record.get('xhr')?.abort();
+			    record.progressWidget = null;
+			    record.hashWidget = null;
+			});
 		    },
 		},
+		model: 'pve-multiupload',
+	    },
+	    listeners: {
+		beforedestroy: function(grid) {
+		    grid.store.each(record => grid.store.remove(record));
+		},
+	    },
+	    columns: [
 		{
-		    xtype: 'textfield',
-		    name: 'filename',
-		    allowBlank: false,
-		    fieldLabel: gettext('File name'),
-		    bind: {
-			value: '{filename}',
-		    },
-		    cbind: {
-			regex: '{filenameRegex}',
-		    },
-		    regexText: gettext('Wrong file extension'),
+		    header: gettext('Source Name'),
+		    dataIndex: 'file',
+		    renderer: file => file.name,
+		    flex: 2,
 		},
 		{
-		    xtype: 'displayfield',
-		    name: 'size',
-		    fieldLabel: gettext('File size'),
-		    bind: {
-			value: '{size}',
+		    header: gettext('File Name'),
+		    dataIndex: 'filename',
+		    flex: 2,
+		    xtype: 'widgetcolumn',
+		    widget: {
+			xtype: 'textfield',
+			listeners: {
+			    change: function(widget, newValue, oldValue) {
+				const record = widget.getWidgetRecord();
+				record.set('filename', newValue);
+			    },
+			},
+			cbind: {
+			    regex: '{filenameRegex}',
+			},
+			regexText: gettext('Wrong file extension'),
 		    },
 		},
 		{
-		    xtype: 'displayfield',
-		    name: 'mimetype',
-		    fieldLabel: gettext('MIME type'),
-		    bind: {
-			value: '{mimetype}',
-		    },
+		    header: gettext('File size'),
+		    dataIndex: 'size',
 		},
 		{
-		    xtype: 'pveHashAlgorithmSelector',
-		    name: 'checksum-algorithm',
-		    fieldLabel: gettext('Hash algorithm'),
-		    allowBlank: true,
-		    hasNoneOption: true,
-		    value: '__default__',
-		    listeners: {
-			change: 'hashChange',
-		    },
+		    header: gettext('MIME type'),
+		    dataIndex: 'mimetype',
 		},
 		{
-		    xtype: 'textfield',
-		    name: 'checksum',
-		    fieldLabel: gettext('Checksum'),
-		    allowBlank: false,
-		    disabled: true,
-		    emptyText: gettext('none'),
-		    reference: 'downloadUrlChecksum',
+		    header: gettext('Hash'),
+		    dataIndex: 'hash',
+		    flex: 2,
+		    xtype: 'widgetcolumn',
+		    widget: {
+			xtype: 'pveHashAlgorithmSelector',
+			listeners: {
+			    change: function(widget, newValue, oldValue) {
+				const record = widget.getWidgetRecord();
+				record.set('hash', newValue);
+				let hashWidget = record.get('hashWidget');
+				if (newValue === '__default__') {
+					hashWidget?.setDisabled(true);
+					hashWidget?.setValue('');
+				} else {
+				    hashWidget?.setDisabled(false);
+				}
+			    },
+			},
+		    },
 		},
 		{
-		    xtype: 'progressbar',
-		    text: 'Ready',
-		    hidden: true,
-		    reference: 'progressBar',
+		    header: gettext('Hash Value'),
+		    dataIndex: 'hashsum',
+		    renderer: data => data || 'None',
+		    flex: 4,
+		    xtype: 'widgetcolumn',
+		    widget: {
+			xtype: 'textfield',
+			disabled: true,
+			listeners: {
+			    change: function(widget, newValue, oldValue) {
+				const record = widget.getWidgetRecord();
+				record.set('hashsum', newValue);
+			    },
+			},
+		    },
+		    onWidgetAttach: function(col, widget, record) {
+			record.set('hashWidget', widget);
+		    },
 		},
 		{
-		    xtype: 'hiddenfield',
-		    name: 'content',
-		    cbind: {
-			value: '{content}',
+		    header: gettext('Progress Bar'),
+		    xtype: 'widgetcolumn',
+		    widget: {
+			xtype: 'progressbar',
 		    },
+		    onWidgetAttach: function(col, widget, rec) {
+			rec.set('progressWidget', widget);
+			widget.updateProgress(0, "");
+		    },
+		    flex: 2,
+		},
+		{
+		    xtype: 'actioncolumn',
+		    items: [{
+			iconCls: 'fa critical fa-trash-o',
+			handler: 'removeFile',
+		    }],
+		    flex: 0.5,
 		},
 	    ],
-	   listeners: {
-		validitychange: 'validitychange',
-	   },
 	},
     ],
 
     buttons: [
+	{
+	    xtype: 'filefield',
+	    name: 'file',
+	    buttonText: gettext('Add File'),
+	    allowBlank: false,
+	    hideLabel: true,
+	    fieldStyle: 'display: none;',
+	    cbind: {
+		accept: '{extensions}',
+	    },
+	    listeners: {
+		change: 'addFile',
+		render: function(filefield) {
+		    filefield.fileInputEl.dom.multiple = true;
+		},
+	    },
+	},
 	{
 	    xtype: 'button',
 	    text: gettext('Abort'),
-	    reference: 'abortBtn',
-	    disabled: true,
 	    handler: function() {
 		const me = this;
 		me.up('pveStorageUpload').close();
 	    },
 	},
 	{
-	    text: gettext('Upload'),
-	    reference: 'submitBtn',
-	    disabled: true,
-	    handler: 'submit',
+	    text: gettext('Start upload'),
+	    handler: 'startUpload',
+	    bind: {
+		disabled: '{!hasFiles || uploadInProgress}',
+	    },
 	},
     ],
 
-    listeners: {
-	close: function() {
-	    const me = this;
-	    if (me.xhr) {
-		me.xhr.abort();
-		delete me.xhr;
-	    }
-	},
-    },
-
     initComponent: function() {
-        const me = this;
-
-	if (!me.nodename) {
-	    throw "no node name specified";
-	}
-	if (!me.storage) {
-	    throw "no storage ID specified";
-	}
-	if (!me.acceptedExtensions[me.content]) {
-	    throw "content type not supported";
-	}
-
-        me.callParent();
+	let me = this;
+	me.callParent();
+	me.lookup('grid').store.on('datachanged', function(store) {
+	    me.getViewModel().set('hasFiles', store.count() > 0);
+	});
     },
 });
-- 
2.30.2





^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2022-07-07 12:37 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2022-06-29 12:23 [pve-devel] [RFC manager] fix #3248: GUI: storage: upload multiple files Matthias Heiserer
2022-07-01 14:21 ` Dominik Csapak
2022-07-05  6:50   ` Thomas Lamprecht
2022-07-07 12:37   ` Matthias Heiserer

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal