]> git.proxmox.com Git - proxmox-widget-toolkit.git/commitdiff
cleanly separate sources from package build, move to own folder
authorThomas Lamprecht <t.lamprecht@proxmox.com>
Sat, 6 Jun 2020 15:35:28 +0000 (17:35 +0200)
committerThomas Lamprecht <t.lamprecht@proxmox.com>
Sat, 6 Jun 2020 15:43:03 +0000 (17:43 +0200)
compared result with `diffoscope`, saw now difference

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
110 files changed:
Logo.js [deleted file]
Makefile
Toolkit.js [deleted file]
button/Button.js [deleted file]
button/HelpButton.js [deleted file]
css/Makefile [deleted file]
css/ext6-pmx.css [deleted file]
data/DiffStore.js [deleted file]
data/ObjectStore.js [deleted file]
data/ProxmoxProxy.js [deleted file]
data/RRDStore.js [deleted file]
data/TimezoneStore.js [deleted file]
data/UpdateStore.js [deleted file]
data/model/Realm.js [deleted file]
data/reader/JsonObject.js [deleted file]
debian/control
defines.mk [deleted file]
form/BondModeSelector.js [deleted file]
form/Checkbox.js [deleted file]
form/ComboGrid.js [deleted file]
form/DateTimeField.js [deleted file]
form/DisplayEdit.js [deleted file]
form/ExpireDate.js [deleted file]
form/IntegerField.js [deleted file]
form/KVComboBox.js [deleted file]
form/LanguageSelector.js [deleted file]
form/NetworkSelector.js [deleted file]
form/RRDTypeSelector.js [deleted file]
form/RealmComboBox.js [deleted file]
form/RoleSelector.js [deleted file]
form/TextField.js [deleted file]
grid/ObjectGrid.js [deleted file]
grid/PendingObjectGrid.js [deleted file]
images/Makefile [deleted file]
images/pmx-clear-trigger.png [deleted file]
mixin/CBind.js [deleted file]
node/APT.js [deleted file]
node/DNSEdit.js [deleted file]
node/DNSView.js [deleted file]
node/HostsView.js [deleted file]
node/NetworkEdit.js [deleted file]
node/NetworkView.js [deleted file]
node/ServiceView.js [deleted file]
node/Tasks.js [deleted file]
node/TimeEdit.js [deleted file]
node/TimeView.js [deleted file]
panel/GaugeWidget.js [deleted file]
panel/InputPanel.js [deleted file]
panel/JournalView.js [deleted file]
panel/LogView.js [deleted file]
panel/RRDChart.js [deleted file]
src/Logo.js [new file with mode: 0644]
src/Makefile [new file with mode: 0644]
src/Toolkit.js [new file with mode: 0644]
src/Utils.js [new file with mode: 0644]
src/button/Button.js [new file with mode: 0644]
src/button/HelpButton.js [new file with mode: 0644]
src/css/Makefile [new file with mode: 0644]
src/css/ext6-pmx.css [new file with mode: 0644]
src/data/DiffStore.js [new file with mode: 0644]
src/data/ObjectStore.js [new file with mode: 0644]
src/data/ProxmoxProxy.js [new file with mode: 0644]
src/data/RRDStore.js [new file with mode: 0644]
src/data/TimezoneStore.js [new file with mode: 0644]
src/data/UpdateStore.js [new file with mode: 0644]
src/data/model/Realm.js [new file with mode: 0644]
src/data/reader/JsonObject.js [new file with mode: 0644]
src/defines.mk [new file with mode: 0644]
src/form/BondModeSelector.js [new file with mode: 0644]
src/form/Checkbox.js [new file with mode: 0644]
src/form/ComboGrid.js [new file with mode: 0644]
src/form/DateTimeField.js [new file with mode: 0644]
src/form/DisplayEdit.js [new file with mode: 0644]
src/form/ExpireDate.js [new file with mode: 0644]
src/form/IntegerField.js [new file with mode: 0644]
src/form/KVComboBox.js [new file with mode: 0644]
src/form/LanguageSelector.js [new file with mode: 0644]
src/form/NetworkSelector.js [new file with mode: 0644]
src/form/RRDTypeSelector.js [new file with mode: 0644]
src/form/RealmComboBox.js [new file with mode: 0644]
src/form/RoleSelector.js [new file with mode: 0644]
src/form/TextField.js [new file with mode: 0644]
src/grid/ObjectGrid.js [new file with mode: 0644]
src/grid/PendingObjectGrid.js [new file with mode: 0644]
src/images/Makefile [new file with mode: 0644]
src/images/pmx-clear-trigger.png [new file with mode: 0644]
src/mixin/CBind.js [new file with mode: 0644]
src/node/APT.js [new file with mode: 0644]
src/node/DNSEdit.js [new file with mode: 0644]
src/node/DNSView.js [new file with mode: 0644]
src/node/HostsView.js [new file with mode: 0644]
src/node/NetworkEdit.js [new file with mode: 0644]
src/node/NetworkView.js [new file with mode: 0644]
src/node/ServiceView.js [new file with mode: 0644]
src/node/Tasks.js [new file with mode: 0644]
src/node/TimeEdit.js [new file with mode: 0644]
src/node/TimeView.js [new file with mode: 0644]
src/panel/GaugeWidget.js [new file with mode: 0644]
src/panel/InputPanel.js [new file with mode: 0644]
src/panel/JournalView.js [new file with mode: 0644]
src/panel/LogView.js [new file with mode: 0644]
src/panel/RRDChart.js [new file with mode: 0644]
src/window/Edit.js [new file with mode: 0644]
src/window/LanguageEdit.js [new file with mode: 0644]
src/window/PasswordEdit.js [new file with mode: 0644]
src/window/TaskViewer.js [new file with mode: 0644]
window/Edit.js [deleted file]
window/LanguageEdit.js [deleted file]
window/PasswordEdit.js [deleted file]
window/TaskViewer.js [deleted file]

diff --git a/Logo.js b/Logo.js
deleted file mode 100644 (file)
index 78d9aad..0000000
--- a/Logo.js
+++ /dev/null
@@ -1,21 +0,0 @@
-Ext.define('PMX.image.Logo', {
-    extend: 'Ext.Img',
-    xtype: 'proxmoxlogo',
-
-    height: 30,
-    width: 172,
-    src: '/images/proxmox_logo.png',
-    alt: 'Proxmox',
-    autoEl: {
-       tag: 'a',
-       href: 'https://www.proxmox.com',
-       target: '_blank',
-    },
-
-    initComponent: function() {
-       let me = this;
-       let prefix = me.prefix !== undefined ? me.prefix : '/pve2';
-       me.src = prefix + me.src;
-       me.callParent();
-    },
-});
index 1d1dafbfe385a5a5e739428138f8e569fae9dd71..f3bd2cf5046d9b07c92245b9261add3850683c11 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,67 +1,18 @@
 include /usr/share/dpkg/pkg-info.mk
 
 include /usr/share/dpkg/pkg-info.mk
 
-include defines.mk
+export PACKAGE=proxmox-widget-toolkit
+BUILDDIR ?= ${PACKAGE}-${DEB_VERSION_UPSTREAM}
+DEB=${PACKAGE}_${DEB_VERSION_UPSTREAM_REVISION}_all.deb
+DSC=${PACKAGE}_${DEB_VERSION_UPSTREAM_REVISION}.dsc
 
 
-SUBDIRS= css images
-
-JSSRC=                                 \
-       Utils.js                        \
-       Toolkit.js                      \
-       Logo.js                         \
-       mixin/CBind.js                  \
-       data/reader/JsonObject.js       \
-       data/ProxmoxProxy.js            \
-       data/UpdateStore.js             \
-       data/DiffStore.js               \
-       data/ObjectStore.js             \
-       data/RRDStore.js                \
-       data/TimezoneStore.js           \
-       data/model/Realm.js             \
-       form/DisplayEdit.js             \
-       form/ExpireDate.js              \
-       form/IntegerField.js            \
-       form/TextField.js               \
-       form/DateTimeField.js           \
-       form/Checkbox.js                \
-       form/KVComboBox.js              \
-       form/LanguageSelector.js        \
-       form/ComboGrid.js               \
-       form/RRDTypeSelector.js         \
-       form/BondModeSelector.js        \
-       form/NetworkSelector.js         \
-       form/RealmComboBox.js           \
-       form/RoleSelector.js            \
-       button/Button.js                \
-       button/HelpButton.js            \
-       grid/ObjectGrid.js              \
-       grid/PendingObjectGrid.js       \
-       panel/InputPanel.js             \
-       panel/LogView.js                \
-       panel/JournalView.js            \
-       panel/RRDChart.js               \
-       panel/GaugeWidget.js            \
-       window/Edit.js                  \
-       window/PasswordEdit.js          \
-       window/TaskViewer.js            \
-       window/LanguageEdit.js          \
-       node/APT.js                     \
-       node/NetworkEdit.js             \
-       node/NetworkView.js             \
-       node/DNSEdit.js                 \
-       node/HostsView.js               \
-       node/DNSView.js                 \
-       node/Tasks.js                   \
-       node/ServiceView.js             \
-       node/TimeEdit.js                \
-       node/TimeView.js
-
-all: ${SUBDIRS}
-       set -e && for i in ${SUBDIRS}; do ${MAKE} -C $$i; done
+GITVERSION:=$(shell git rev-parse HEAD)
 
 ${BUILDDIR}:
 
 ${BUILDDIR}:
-       rm -rf ${BUILDDIR}
-       rsync -a * ${BUILDDIR}
-       echo "git clone git://git.proxmox.com/git/proxmox-widget-toolkit.git\\ngit checkout ${GITVERSION}" >  ${BUILDDIR}/debian/SOURCE
+       rm -rf ${BUILDDIR} ${BUILDDIR}.tmp
+       cp -a src/ ${BUILDDIR}.tmp
+       cp -a debian ${BUILDDIR}.tmp/
+       echo "git clone git://git.proxmox.com/git/proxmox-widget-toolkit.git\\ngit checkout ${GITVERSION}" >  ${BUILDDIR}.tmp/debian/SOURCE
+       mv ${BUILDDIR}.tmp/ ${BUILDDIR}
 
 .PHONY: deb
 deb: ${DEB}
 
 .PHONY: deb
 deb: ${DEB}
@@ -76,20 +27,8 @@ ${DSC}: ${BUILDDIR}
        lintian ${DSC}
 
 .PHONY: lint
        lintian ${DSC}
 
 .PHONY: lint
-check: lint
 lint: ${JSSRC}
 lint: ${JSSRC}
-       eslint ${JSSRC}
-
-proxmoxlib.js: ${JSSRC}
-       # add the version as comment in the file
-       echo "// ${DEB_VERSION_UPSTREAM_REVISION}" > $@.tmp
-       cat ${JSSRC} >> $@.tmp
-       mv $@.tmp $@
-
-install: proxmoxlib.js
-       install -d -m 755 ${WWWBASEDIR}
-       install -m 0644 proxmoxlib.js ${WWWBASEDIR}
-       set -e && for i in ${SUBDIRS}; do ${MAKE} -C $$i $@; done
+       ${MAKE} -C src lint
 
 .PHONY: upload
 upload: ${DEB}
 
 .PHONY: upload
 upload: ${DEB}
@@ -97,7 +36,7 @@ upload: ${DEB}
 
 distclean: clean
 clean:
 
 distclean: clean
 clean:
-       rm -rf ${BUILDDIR} *.tar.gz *.dsc *.deb *.changes *.buildinfo proxmoxlib.js
+       rm -rf ${BUILDDIR} ${BUILDDIR}.tmp *.tar.gz *.dsc *.deb *.changes *.buildinfo
        find . -name '*~' -exec rm {} ';'
 
 .PHONY: dinstall
        find . -name '*~' -exec rm {} ';'
 
 .PHONY: dinstall
diff --git a/Toolkit.js b/Toolkit.js
deleted file mode 100644 (file)
index d528ea7..0000000
+++ /dev/null
@@ -1,673 +0,0 @@
-// ExtJS related things
-
- // do not send '_dc' parameter
-Ext.Ajax.disableCaching = false;
-
-// FIXME: HACK! Makes scrolling in number spinner work again. fixed in ExtJS >= 6.1
-if (Ext.isFirefox) {
-    Ext.$eventNameMap.DOMMouseScroll = 'DOMMouseScroll';
-}
-
-// custom Vtypes
-Ext.apply(Ext.form.field.VTypes, {
-    IPAddress: function(v) {
-       return Proxmox.Utils.IP4_match.test(v);
-    },
-    IPAddressText: gettext('Example') + ': 192.168.1.1',
-    IPAddressMask: /[\d.]/i,
-
-    IPCIDRAddress: function(v) {
-       let result = Proxmox.Utils.IP4_cidr_match.exec(v);
-       // limits according to JSON Schema see
-       // pve-common/src/PVE/JSONSchema.pm
-       return result !== null && result[1] >= 8 && result[1] <= 32;
-    },
-    IPCIDRAddressText: gettext('Example') + ': 192.168.1.1/24<br>' + gettext('Valid CIDR Range') + ': 8-32',
-    IPCIDRAddressMask: /[\d./]/i,
-
-    IP6Address: function(v) {
-        return Proxmox.Utils.IP6_match.test(v);
-    },
-    IP6AddressText: gettext('Example') + ': 2001:DB8::42',
-    IP6AddressMask: /[A-Fa-f0-9:]/,
-
-    IP6CIDRAddress: function(v) {
-       let result = Proxmox.Utils.IP6_cidr_match.exec(v);
-       // limits according to JSON Schema see
-       // pve-common/src/PVE/JSONSchema.pm
-       return result !== null && result[1] >= 8 && result[1] <= 128;
-    },
-    IP6CIDRAddressText: gettext('Example') + ': 2001:DB8::42/64<br>' + gettext('Valid CIDR Range') + ': 8-128',
-    IP6CIDRAddressMask: /[A-Fa-f0-9:/]/,
-
-    IP6PrefixLength: function(v) {
-       return v >= 0 && v <= 128;
-    },
-    IP6PrefixLengthText: gettext('Example') + ': X, where 0 <= X <= 128',
-    IP6PrefixLengthMask: /[0-9]/,
-
-    IP64Address: function(v) {
-        return Proxmox.Utils.IP64_match.test(v);
-    },
-    IP64AddressText: gettext('Example') + ': 192.168.1.1 2001:DB8::42',
-    IP64AddressMask: /[A-Fa-f0-9.:]/,
-
-    IP64CIDRAddress: function(v) {
-       let result = Proxmox.Utils.IP64_cidr_match.exec(v);
-       if (result === null) {
-           return false;
-       }
-       if (result[1] !== undefined) {
-           return result[1] >= 8 && result[1] <= 128;
-       } else if (result[2] !== undefined) {
-           return result[2] >= 8 && result[2] <= 32;
-       } else {
-           return false;
-       }
-    },
-    IP64CIDRAddressText: gettext('Example') + ': 192.168.1.1/24 2001:DB8::42/64',
-    IP64CIDRAddressMask: /[A-Fa-f0-9.:/]/,
-
-    MacAddress: function(v) {
-       return (/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/).test(v);
-    },
-    MacAddressMask: /[a-fA-F0-9:]/,
-    MacAddressText: gettext('Example') + ': 01:23:45:67:89:ab',
-
-    MacPrefix: function(v) {
-       return (/^[a-f0-9][02468ace](?::[a-f0-9]{2}){0,2}:?$/i).test(v);
-    },
-    MacPrefixMask: /[a-fA-F0-9:]/,
-    MacPrefixText: gettext('Example') + ': 02:8f - ' + gettext('only unicast addresses are allowed'),
-
-    BridgeName: function(v) {
-        return (/^vmbr\d{1,4}$/).test(v);
-    },
-    VlanName: function(v) {
-       if (Proxmox.Utils.VlanInterface_match.test(v)) {
-         return true;
-       } else if (Proxmox.Utils.Vlan_match.test(v)) {
-         return true;
-       }
-       return true;
-    },
-    BridgeNameText: gettext('Format') + ': vmbr<b>N</b>, where 0 <= <b>N</b> <= 9999',
-
-    BondName: function(v) {
-        return (/^bond\d{1,4}$/).test(v);
-    },
-    BondNameText: gettext('Format') + ': bond<b>N</b>, where 0 <= <b>N</b> <= 9999',
-
-    InterfaceName: function(v) {
-        return (/^[a-z][a-z0-9_]{1,20}$/).test(v);
-    },
-    InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'<br />" +
-                      gettext("Minimum characters") + ": 2<br />" +
-                      gettext("Maximum characters") + ": 21<br />" +
-                      gettext("Must start with") + ": 'a-z'",
-
-    StorageId: function(v) {
-        return (/^[a-z][a-z0-9\-_.]*[a-z0-9]$/i).test(v);
-    },
-    StorageIdText: gettext("Allowed characters") + ":  'A-Z', 'a-z', '0-9', '-', '_', '.'<br />" +
-                  gettext("Minimum characters") + ": 2<br />" +
-                  gettext("Must start with") + ": 'A-Z', 'a-z'<br />" +
-                  gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'<br />",
-
-    ConfigId: function(v) {
-        return (/^[a-z][a-z0-9_]+$/i).test(v);
-    },
-    ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'<br />" +
-                 gettext("Minimum characters") + ": 2<br />" +
-                 gettext("Must start with") + ": " + gettext("letter"),
-
-    HttpProxy: function(v) {
-        return (/^http:\/\/.*$/).test(v);
-    },
-    HttpProxyText: gettext('Example') + ": http://username:password&#64;host:port/",
-
-    DnsName: function(v) {
-       return Proxmox.Utils.DnsName_match.test(v);
-    },
-    DnsNameText: gettext('This is not a valid DNS name'),
-
-    // workaround for https://www.sencha.com/forum/showthread.php?302150
-    proxmoxMail: function(v) {
-        return (/^(\w+)([-+.][\w]+)*@(\w[-\w]*\.){1,5}([A-Za-z]){2,63}$/).test(v);
-    },
-    proxmoxMailText: gettext('Example') + ": user@example.com",
-
-    DnsOrIp: function(v) {
-       if (!Proxmox.Utils.DnsName_match.test(v) &&
-           !Proxmox.Utils.IP64_match.test(v)) {
-           return false;
-       }
-
-       return true;
-    },
-    DnsOrIpText: gettext('Not a valid DNS name or IP address.'),
-
-    HostList: function(v) {
-       let list = v.split(/[ ,;]+/);
-       let i;
-       for (i = 0; i < list.length; i++) {
-           if (list[i] === '') {
-               continue;
-           }
-
-           if (!Proxmox.Utils.HostPort_match.test(list[i]) &&
-               !Proxmox.Utils.HostPortBrackets_match.test(list[i]) &&
-               !Proxmox.Utils.IP6_dotnotation_match.test(list[i])) {
-               return false;
-           }
-       }
-
-       return true;
-    },
-    HostListText: gettext('Not a valid list of hosts'),
-
-    password: function(val, field) {
-        if (field.initialPassField) {
-            let pwd = field.up('form').down(`[name=${field.initialPassField}]`);
-            return val === pwd.getValue();
-        }
-        return true;
-    },
-
-    passwordText: gettext('Passwords do not match'),
-});
-
-// Firefox 52+ Touchscreen bug
-// see https://www.sencha.com/forum/showthread.php?336762-Examples-don-t-work-in-Firefox-52-touchscreen/page2
-// and https://bugzilla.proxmox.com/show_bug.cgi?id=1223
-Ext.define('EXTJS_23846.Element', {
-    override: 'Ext.dom.Element',
-}, function(Element) {
-    let supports = Ext.supports,
-        proto = Element.prototype,
-        eventMap = proto.eventMap,
-        additiveEvents = proto.additiveEvents;
-
-    if (Ext.os.is.Desktop && supports.TouchEvents && !supports.PointerEvents) {
-        eventMap.touchstart = 'mousedown';
-        eventMap.touchmove = 'mousemove';
-        eventMap.touchend = 'mouseup';
-        eventMap.touchcancel = 'mouseup';
-
-        additiveEvents.mousedown = 'mousedown';
-        additiveEvents.mousemove = 'mousemove';
-        additiveEvents.mouseup = 'mouseup';
-        additiveEvents.touchstart = 'touchstart';
-        additiveEvents.touchmove = 'touchmove';
-        additiveEvents.touchend = 'touchend';
-        additiveEvents.touchcancel = 'touchcancel';
-
-        additiveEvents.pointerdown = 'mousedown';
-        additiveEvents.pointermove = 'mousemove';
-        additiveEvents.pointerup = 'mouseup';
-        additiveEvents.pointercancel = 'mouseup';
-    }
-});
-
-Ext.define('EXTJS_23846.Gesture', {
-    override: 'Ext.event.publisher.Gesture',
-}, function(Gesture) {
-    let gestures = Gesture.instance;
-
-    if (Ext.supports.TouchEvents && !Ext.isWebKit && Ext.os.is.Desktop) {
-        gestures.handledDomEvents.push('mousedown', 'mousemove', 'mouseup');
-        gestures.registerEvents();
-    }
-});
-
-Ext.define('EXTJS_18900.Pie', {
-    override: 'Ext.chart.series.Pie',
-
-    // from 6.0.2
-    betweenAngle: function(x, a, b) {
-        let pp = Math.PI * 2,
-            offset = this.rotationOffset;
-
-        if (a === b) {
-            return false;
-        }
-
-        if (!this.getClockwise()) {
-            x *= -1;
-            a *= -1;
-            b *= -1;
-            a -= offset;
-            b -= offset;
-        } else {
-            a += offset;
-            b += offset;
-        }
-
-        x -= a;
-        b -= a;
-
-        // Normalize, so that both x and b are in the [0,360) interval.
-        x %= pp;
-        b %= pp;
-        x += pp;
-        b += pp;
-        x %= pp;
-        b %= pp;
-
-        // Because 360 * n angles will be normalized to 0,
-        // we need to treat b === 0 as a special case.
-        return x < b || b === 0;
-    },
-});
-
-// we always want the number in x.y format and never in, e.g., x,y
-Ext.define('PVE.form.field.Number', {
-    override: 'Ext.form.field.Number',
-    submitLocaleSeparator: false,
-});
-
-// ExtJs 5-6 has an issue with caching
-// see https://www.sencha.com/forum/showthread.php?308989
-Ext.define('Proxmox.UnderlayPool', {
-    override: 'Ext.dom.UnderlayPool',
-
-    checkOut: function() {
-        let cache = this.cache,
-            len = cache.length,
-            el;
-
-        // do cleanup because some of the objects might have been destroyed
-       while (len--) {
-            if (cache[len].destroyed) {
-                cache.splice(len, 1);
-            }
-        }
-        // end do cleanup
-
-       el = cache.shift();
-
-        if (!el) {
-            el = Ext.Element.create(this.elementConfig);
-            el.setVisibilityMode(2);
-            //<debug>
-            // tell the spec runner to ignore this element when checking if the dom is clean
-           el.dom.setAttribute('data-sticky', true);
-            //</debug>
-       }
-
-        return el;
-    },
-});
-
-// 'Enter' in Textareas and aria multiline fields should not activate the
-// defaultbutton, fixed in extjs 6.0.2
-Ext.define('PVE.panel.Panel', {
-    override: 'Ext.panel.Panel',
-
-    fireDefaultButton: function(e) {
-       if (e.target.getAttribute('aria-multiline') === 'true' ||
-           e.target.tagName === "TEXTAREA") {
-           return true;
-       }
-       return this.callParent(arguments);
-    },
-});
-
-// if the order of the values are not the same in originalValue and value
-// extjs will not overwrite value, but marks the field dirty and thus
-// the reset button will be enabled (but clicking it changes nothing)
-// so if the arrays are not the same after resetting, we
-// clear and set it
-Ext.define('Proxmox.form.ComboBox', {
-    override: 'Ext.form.field.ComboBox',
-
-    reset: function() {
-       // copied from combobox
-       let me = this;
-       me.callParent();
-
-       // clear and set when not the same
-       let value = me.getValue();
-       if (Ext.isArray(me.originalValue) && Ext.isArray(value) &&
-           !Ext.Array.equals(value, me.originalValue)) {
-           me.clearValue();
-           me.setValue(me.originalValue);
-       }
-    },
-
-    // we also want to open the trigger on editable comboboxes by default
-    initComponent: function() {
-       let me = this;
-       me.callParent();
-
-       if (me.editable) {
-           // The trigger.picker causes first a focus event on the field then
-           // toggles the selection picker. Thus skip expanding in this case,
-           // else our focus listener expands and the picker.trigger then
-           // collapses it directly afterwards.
-           Ext.override(me.triggers.picker, {
-               onMouseDown: function(e) {
-                   // copied "should we focus" check from Ext.form.trigger.Trigger
-                   if (e.pointerType !== 'touch' && !this.field.owns(Ext.Element.getActiveElement())) {
-                       me.skip_expand_on_focus = true;
-                   }
-                   this.callParent(arguments);
-               },
-           });
-
-           me.on("focus", function(combobox) {
-               if (!combobox.isExpanded && !combobox.skip_expand_on_focus) {
-                   combobox.expand();
-               }
-               combobox.skip_expand_on_focus = false;
-           });
-       }
-    },
-});
-
-// when refreshing a grid/tree view, restoring the focus moves the view back to
-// the previously focused item. Save scroll position before refocusing.
-Ext.define(null, {
-    override: 'Ext.view.Table',
-
-    jumpToFocus: false,
-
-    saveFocusState: function() {
-       let me = this,
-           store = me.dataSource,
-           actionableMode = me.actionableMode,
-           navModel = me.getNavigationModel(),
-           focusPosition = actionableMode ? me.actionPosition : navModel.getPosition(true),
-           refocusRow, refocusCol;
-
-       if (focusPosition) {
-           // Separate this from the instance that the nav model is using.
-           focusPosition = focusPosition.clone();
-
-           // Exit actionable mode.
-           // We must inform any Actionables that they must relinquish control.
-           // Tabbability must be reset.
-           if (actionableMode) {
-               me.ownerGrid.setActionableMode(false);
-           }
-
-           // Blur the focused descendant, but do not trigger focusLeave.
-           me.el.dom.focus();
-
-           // Exiting actionable mode navigates to the owning cell, so in either focus mode we must
-           // clear the navigation position
-           navModel.setPosition();
-
-           // The following function will attempt to refocus back in the same mode to the same cell
-           // as it was at before based upon the previous record (if it's still inthe store), or the row index.
-           return function() {
-               // If we still have data, attempt to refocus in the same mode.
-               if (store.getCount()) {
-                   // Adjust expectations of where we are able to refocus according to what kind of destruction
-                   // might have been wrought on this view's DOM during focus save.
-                   refocusRow = Math.min(focusPosition.rowIdx, me.all.getCount() - 1);
-                   refocusCol = Math.min(focusPosition.colIdx,
-                                         me.getVisibleColumnManager().getColumns().length - 1);
-                   refocusRow = store.contains(focusPosition.record) ? focusPosition.record : refocusRow;
-                   focusPosition = new Ext.grid.CellContext(me).setPosition(refocusRow, refocusCol);
-
-                   if (actionableMode) {
-                       me.ownerGrid.setActionableMode(true, focusPosition);
-                   } else {
-                       me.cellFocused = true;
-
-                       // we sometimes want to scroll back to where we were
-                       let x = me.getScrollX();
-                       let y = me.getScrollY();
-
-                       // Pass "preventNavigation" as true so that that does not cause selection.
-                       navModel.setPosition(focusPosition, null, null, null, true);
-
-                       if (!me.jumpToFocus) {
-                           me.scrollTo(x, y);
-                       }
-                   }
-               } else { // No rows - focus associated column header
-                   focusPosition.column.focus();
-               }
-           };
-       }
-       return Ext.emptyFn;
-    },
-});
-
-// should be fixed with ExtJS 6.0.2, see:
-// https://www.sencha.com/forum/showthread.php?307244-Bug-with-datefield-in-window-with-scroll
-Ext.define('Proxmox.Datepicker', {
-    override: 'Ext.picker.Date',
-    hideMode: 'visibility',
-});
-
-// ExtJS 6.0.1 has no setSubmitValue() (although you find it in the docs).
-// Note: this.submitValue is a boolean flag, whereas getSubmitValue() returns
-// data to be submitted.
-Ext.define('Proxmox.form.field.Text', {
-    override: 'Ext.form.field.Text',
-
-    setSubmitValue: function(v) {
-       this.submitValue = v;
-    },
-});
-
-// this should be fixed with ExtJS 6.0.2
-// make mousescrolling work in firefox in the containers overflowhandler
-Ext.define(null, {
-    override: 'Ext.layout.container.boxOverflow.Scroller',
-
-    createWheelListener: function() {
-       let me = this;
-       if (Ext.isFirefox) {
-           me.wheelListener = me.layout.innerCt.on('wheel', me.onMouseWheelFirefox, me, { destroyable: true });
-       } else {
-           me.wheelListener = me.layout.innerCt.on('mousewheel', me.onMouseWheel, me, { destroyable: true });
-       }
-    },
-
-    // special wheel handler for firefox. differs from the default onMouseWheel
-    // handler by using deltaY instead of wheelDeltaY and no normalizing,
-    // because it is already
-    onMouseWheelFirefox: function(e) {
-       e.stopEvent();
-       let delta = e.browserEvent.deltaY || 0;
-       this.scrollBy(delta * this.wheelIncrement, false);
-    },
-
-});
-
-// add '@' to the valid id
-Ext.define('Proxmox.validIdReOverride', {
-    override: 'Ext.Component',
-    validIdRe: /^[a-z_][a-z0-9\-_@]*$/i,
-});
-
-// force alert boxes to be rendered with an Error Icon
-// since Ext.Msg is an object and not a prototype, we need to override it
-// after the framework has been initiated
-Ext.onReady(function() {
-/*jslint confusion: true */
-    Ext.override(Ext.Msg, {
-       alert: function(title, message, fn, scope) { // eslint-disable-line consistent-return
-           if (Ext.isString(title)) {
-               let config = {
-                   title: title,
-                   message: message,
-                   icon: this.ERROR,
-                   buttons: this.OK,
-                   fn: fn,
-                   scope: scope,
-                   minWidth: this.minWidth,
-               };
-           return this.show(config);
-           }
-       },
-    });
-/*jslint confusion: false */
-});
-Ext.define('Ext.ux.IFrame', {
-    extend: 'Ext.Component',
-
-    alias: 'widget.uxiframe',
-
-    loadMask: 'Loading...',
-
-    src: 'about:blank',
-
-    renderTpl: [
-        '<iframe src="{src}" id="{id}-iframeEl" data-ref="iframeEl" name="{frameName}" width="100%" height="100%" frameborder="0" allowfullscreen="true"></iframe>',
-    ],
-    childEls: ['iframeEl'],
-
-    initComponent: function() {
-        this.callParent();
-
-        this.frameName = this.frameName || this.id + '-frame';
-    },
-
-    initEvents: function() {
-        let me = this;
-        me.callParent();
-        me.iframeEl.on('load', me.onLoad, me);
-    },
-
-    initRenderData: function() {
-        return Ext.apply(this.callParent(), {
-            src: this.src,
-            frameName: this.frameName,
-        });
-    },
-
-    getBody: function() {
-        let doc = this.getDoc();
-        return doc.body || doc.documentElement;
-    },
-
-    getDoc: function() {
-        try {
-            return this.getWin().document;
-        } catch (ex) {
-            return null;
-        }
-    },
-
-    getWin: function() {
-        let me = this,
-            name = me.frameName,
-            win = Ext.isIE
-                ? me.iframeEl.dom.contentWindow
-                : window.frames[name];
-        return win;
-    },
-
-    getFrame: function() {
-        let me = this;
-        return me.iframeEl.dom;
-    },
-
-    beforeDestroy: function() {
-        this.cleanupListeners(true);
-        this.callParent();
-    },
-
-    cleanupListeners: function(destroying) {
-        let doc, prop;
-
-        if (this.rendered) {
-            try {
-                doc = this.getDoc();
-                if (doc) {
-                    Ext.get(doc).un(this._docListeners);
-                    if (destroying && doc.hasOwnProperty) {
-                        for (prop in doc) {
-                            if (Object.prototype.hasOwnProperty.call(doc, prop)) {
-                                delete doc[prop];
-                            }
-                        }
-                    }
-                }
-            } catch (e) {
-                // do nothing
-            }
-        }
-    },
-
-    onLoad: function() {
-        let me = this,
-            doc = me.getDoc(),
-            fn = me.onRelayedEvent;
-
-        if (doc) {
-            try {
-                // These events need to be relayed from the inner document (where they stop
-                // bubbling) up to the outer document. This has to be done at the DOM level so
-                // the event reaches listeners on elements like the document body. The effected
-                // mechanisms that depend on this bubbling behavior are listed to the right
-                // of the event.
-               /*jslint nomen: true*/
-                Ext.get(doc).on(
-                    me._docListeners = {
-                        mousedown: fn, // menu dismisal (MenuManager) and Window onMouseDown (toFront)
-                        mousemove: fn, // window resize drag detection
-                        mouseup: fn, // window resize termination
-                        click: fn, // not sure, but just to be safe
-                        dblclick: fn, // not sure again
-                        scope: me,
-                    },
-                );
-               /*jslint nomen: false*/
-            } catch (e) {
-                // cannot do this xss
-            }
-
-            // We need to be sure we remove all our events from the iframe on unload or we're going to LEAK!
-            Ext.get(this.getWin()).on('beforeunload', me.cleanupListeners, me);
-
-            this.el.unmask();
-            this.fireEvent('load', this);
-        } else if (me.src) {
-            this.el.unmask();
-            this.fireEvent('error', this);
-        }
-    },
-
-    onRelayedEvent: function(event) {
-        // relay event from the iframe's document to the document that owns the iframe...
-
-        let iframeEl = this.iframeEl,
-
-            // Get the left-based iframe position
-            iframeXY = iframeEl.getTrueXY(),
-            originalEventXY = event.getXY(),
-
-            // Get the left-based XY position.
-            // This is because the consumer of the injected event will
-            // perform its own RTL normalization.
-            eventXY = event.getTrueXY();
-
-        // the event from the inner document has XY relative to that document's origin,
-        // so adjust it to use the origin of the iframe in the outer document:
-        event.xy = [iframeXY[0] + eventXY[0], iframeXY[1] + eventXY[1]];
-
-        event.injectEvent(iframeEl); // blame the iframe for the event...
-
-        event.xy = originalEventXY; // restore the original XY (just for safety)
-    },
-
-    load: function(src) {
-        let me = this,
-            text = me.loadMask,
-            frame = me.getFrame();
-
-        if (me.fireEvent('beforeload', me, src) !== false) {
-            if (text && me.el) {
-                me.el.mask(text);
-            }
-
-            frame.src = me.src = src || me.src;
-        }
-    },
-});
diff --git a/button/Button.js b/button/Button.js
deleted file mode 100644 (file)
index 93c773c..0000000
+++ /dev/null
@@ -1,171 +0,0 @@
-/* Button features:
- * - observe selection changes to enable/disable the button using enableFn()
- * - pop up confirmation dialog using confirmMsg()
- */
-Ext.define('Proxmox.button.Button', {
-    extend: 'Ext.button.Button',
-    alias: 'widget.proxmoxButton',
-
-    // the selection model to observe
-    selModel: undefined,
-
-    // if 'false' handler will not be called (button disabled)
-    enableFn: function(record) {
-       // return undefined by default
-    },
-
-    // function(record) or text
-    confirmMsg: false,
-
-    // take special care in confirm box (select no as default).
-    dangerous: false,
-
-    // is used to get the parent container for its selection model
-    parentXType: 'grid',
-
-    initComponent: function() {
-        let me = this;
-
-       if (me.handler) {
-           // Note: me.realHandler may be a string (see named scopes)
-           let realHandler = me.handler;
-
-           me.handler = function(button, event) {
-               let rec, msg;
-               if (me.selModel) {
-                   rec = me.selModel.getSelection()[0];
-                   if (!rec || me.enableFn(rec) === false) {
-                       return;
-                   }
-               }
-
-               if (me.confirmMsg) {
-                   msg = me.confirmMsg;
-                   if (Ext.isFunction(me.confirmMsg)) {
-                       msg = me.confirmMsg(rec);
-                   }
-                   Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1;
-                   Ext.Msg.show({
-                       title: gettext('Confirm'),
-                       icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
-                       message: msg,
-                       buttons: Ext.Msg.YESNO,
-                       defaultFocus: me.dangerous ? 'no' : 'yes',
-                       callback: function(btn) {
-                           if (btn !== 'yes') {
-                               return;
-                           }
-                           Ext.callback(realHandler, me.scope, [button, event, rec], 0, me);
-                       },
-                   });
-               } else {
-                   Ext.callback(realHandler, me.scope, [button, event, rec], 0, me);
-               }
-           };
-       }
-
-       me.callParent();
-
-       let grid;
-       if (!me.selModel && me.selModel !== null && me.selModel !== false) {
-           let parent = me.up(me.parentXType);
-           if (parent && parent.selModel) {
-               me.selModel = parent.selModel;
-           }
-       }
-
-       if (me.waitMsgTarget === true) {
-           grid = me.up('grid');
-           if (grid) {
-               me.waitMsgTarget = grid;
-           } else {
-               throw "unable to find waitMsgTarget";
-           }
-       }
-
-       if (me.selModel) {
-           me.mon(me.selModel, "selectionchange", function() {
-               let rec = me.selModel.getSelection()[0];
-               if (!rec || me.enableFn(rec) === false) {
-                   me.setDisabled(true);
-               } else {
-                   me.setDisabled(false);
-               }
-           });
-       }
-    },
-});
-
-
-Ext.define('Proxmox.button.StdRemoveButton', {
-    extend: 'Proxmox.button.Button',
-    alias: 'widget.proxmoxStdRemoveButton',
-
-    text: gettext('Remove'),
-
-    disabled: true,
-
-    // time to wait for removal task to finish
-    delay: undefined,
-
-    config: {
-       baseurl: undefined,
-    },
-
-    getUrl: function(rec) {
-       let me = this;
-
-       if (me.selModel) {
-           return me.baseurl + '/' + rec.getId();
-       } else {
-           return me.baseurl;
-       }
-    },
-
-    // also works with names scopes
-    callback: function(options, success, response) {
-       // do nothing by default
-    },
-
-    getRecordName: (rec) => rec.getId(),
-
-    confirmMsg: function(rec) {
-       let me = this;
-
-       let name = me.getRecordName(rec);
-       return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), `'${name}'`);
-    },
-
-    handler: function(btn, event, rec) {
-       let me = this;
-
-       let url = me.getUrl(rec);
-
-       if (typeof me.delay !== 'undefined' && me.delay >= 0) {
-           url += "?delay=" + me.delay;
-       }
-
-       Proxmox.Utils.API2Request({
-           url: url,
-           method: 'DELETE',
-           waitMsgTarget: me.waitMsgTarget,
-           callback: function(options, success, response) {
-               Ext.callback(me.callback, me.scope, [options, success, response], 0, me);
-           },
-           failure: function(response, opts) {
-               Ext.Msg.alert(gettext('Error'), response.htmlStatus);
-           },
-       });
-    },
-    initComponent: function() {
-       let me = this;
-
-       // enable by default if no seleModel is there and disabled not set
-       if (me.initialConfig.disabled === undefined &&
-           (me.selModel === null || me.selModel === false)) {
-           me.disabled = false;
-       }
-
-       me.callParent();
-    },
-});
diff --git a/button/HelpButton.js b/button/HelpButton.js
deleted file mode 100644 (file)
index d97a6c4..0000000
+++ /dev/null
@@ -1,86 +0,0 @@
-/* help button pointing to an online documentation
-   for components contained in a modal window
-*/
-Ext.define('Proxmox.button.Help', {
-    extend: 'Ext.button.Button',
-    xtype: 'proxmoxHelpButton',
-
-    text: gettext('Help'),
-
-    // make help button less flashy by styling it like toolbar buttons
-    iconCls: ' x-btn-icon-el-default-toolbar-small fa fa-question-circle',
-    cls: 'x-btn-default-toolbar-small proxmox-inline-button',
-
-    hidden: true,
-
-    listenToGlobalEvent: true,
-
-    controller: {
-       xclass: 'Ext.app.ViewController',
-       listen: {
-           global: {
-               proxmoxShowHelp: 'onProxmoxShowHelp',
-               proxmoxHideHelp: 'onProxmoxHideHelp',
-           },
-       },
-       onProxmoxShowHelp: function(helpLink) {
-           let view = this.getView();
-           if (view.listenToGlobalEvent === true) {
-               view.setOnlineHelp(helpLink);
-               view.show();
-           }
-       },
-       onProxmoxHideHelp: function() {
-           let view = this.getView();
-           if (view.listenToGlobalEvent === true) {
-               view.hide();
-           }
-       },
-    },
-
-    // this sets the link and the tooltip text
-    setOnlineHelp: function(blockid) {
-       let me = this;
-
-       let info = Proxmox.Utils.get_help_info(blockid);
-       if (info) {
-           me.onlineHelp = blockid;
-           let title = info.title;
-           if (info.subtitle) {
-               title += ' - ' + info.subtitle;
-           }
-           me.setTooltip(title);
-       }
-    },
-
-    // helper to set the onlineHelp via a config object
-    setHelpConfig: function(config) {
-       let me = this;
-       me.setOnlineHelp(config.onlineHelp);
-    },
-
-    handler: function() {
-       let me = this;
-       let docsURI;
-
-       if (me.onlineHelp) {
-           docsURI = Proxmox.Utils.get_help_link(me.onlineHelp);
-       }
-
-       if (docsURI) {
-           window.open(docsURI);
-       } else {
-           Ext.Msg.alert(gettext('Help'), gettext('No Help available'));
-       }
-    },
-
-    initComponent: function() {
-       let me = this;
-
-       me.callParent();
-
-       if (me.onlineHelp) {
-           me.setOnlineHelp(me.onlineHelp); // set tooltip
-       }
-    },
-});
diff --git a/css/Makefile b/css/Makefile
deleted file mode 100644 (file)
index 5054b55..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-include ../defines.mk
-
-CSS=ext6-pmx.css
-
-all:
-
-.PHONY: install
-install: ${CSS}
-       install -d ${WWWCSSDIR}
-       for i in ${CSS}; do install -m 0755 $$i ${WWWCSSDIR}/$$i; done
-
-.PHONY: clean
-clean:
diff --git a/css/ext6-pmx.css b/css/ext6-pmx.css
deleted file mode 100644 (file)
index a7d446b..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-.pmx-clear-trigger {
-    background-image: url(../images/pmx-clear-trigger.png);
-}
-
-.pmx-hint {
-    background-color: LightYellow;
-}
-
-.x-mask-msg-text {
-    text-align: center;
-}
-
-.proxmox-invalid-row {
-    background-color: #f3d6d7;
-}
-
-/* some icons have to be color manually */
-.black {
-    color: #000;
-}
-
-.normal {
-    color: #c2ddf2;
-}
-
-.faded {
-    color: #cfcfcf;
-}
-
-.good {
-    color: #21BF4B;
-}
-
-.warning {
-    color: #fc0;
-}
-
-.critical {
-    color: #FF6C59;
-}
-
-/* reduce chart legend space usage to something more sane */
-.x-legend-item {
-       padding: 0.4em 0.8em 0.4em 1.8em;
-}
-
-.x-legend-item-marker {
-       left: 0.5em;
-       top: 0.6em;
-}
diff --git a/data/DiffStore.js b/data/DiffStore.js
deleted file mode 100644 (file)
index e4143f5..0000000
+++ /dev/null
@@ -1,140 +0,0 @@
-/*
- * The DiffStore is a in-memory store acting as proxy between a real store
- * instance and a component.
- * Its purpose is to redisplay the component *only* if the data has been changed
- * inside the real store, to avoid the annoying visual flickering of using
- * the real store directly.
- *
- * Implementation:
- * The DiffStore monitors via mon() the 'load' events sent by the real store.
- * On each 'load' event, the DiffStore compares its own content with the target
- * store (call to cond_add_item()) and then fires a 'refresh' event.
- * The 'refresh' event will automatically trigger a view refresh on the component
- * who binds to this store.
- */
-
-/* Config properties:
- * rstore: the realstore which will autorefresh its content from the API
- * Only works if rstore has a model and use 'idProperty'
- * sortAfterUpdate: sort the diffstore before rendering the view
- */
-Ext.define('Proxmox.data.DiffStore', {
-    extend: 'Ext.data.Store',
-    alias: 'store.diff',
-
-    sortAfterUpdate: false,
-
-    // if true, destroy rstore on destruction. Defaults to true if a rstore
-    // config is passed instead of an existing rstore instance
-    autoDestroyRstore: false,
-
-    onDestroy: function() {
-       let me = this;
-       if (me.autoDestroyRstore) {
-           if (Ext.isFunction(me.rstore.destroy)) {
-               me.rstore.destroy();
-           }
-           delete me.rstore;
-       }
-       me.callParent();
-    },
-
-    constructor: function(config) {
-       let me = this;
-
-       config = config || {};
-
-       if (!config.rstore) {
-           throw "no rstore specified";
-       }
-
-       if (!config.rstore.model) {
-           throw "no rstore model specified";
-       }
-
-       let rstore;
-       if (config.rstore.isInstance) {
-           rstore = config.rstore;
-       } else if (config.rstore.type) {
-           Ext.applyIf(config.rstore, {
-               autoDestroyRstore: true,
-           });
-           rstore = Ext.create(`store.${config.rstore.type}`, config.rstore);
-       } else {
-           throw 'rstore is not an instance, and cannot autocreate without "type"';
-       }
-
-       Ext.apply(config, {
-           model: rstore.model,
-           proxy: { type: 'memory' },
-       });
-
-       me.callParent([config]);
-
-       me.rstore = rstore;
-
-       let first_load = true;
-
-       let cond_add_item = function(data, id) {
-           let olditem = me.getById(id);
-           if (olditem) {
-               olditem.beginEdit();
-               Ext.Array.each(me.model.prototype.fields, function(field) {
-                   if (olditem.data[field.name] !== data[field.name]) {
-                       olditem.set(field.name, data[field.name]);
-                   }
-               });
-               olditem.endEdit(true);
-               olditem.commit();
-           } else {
-               let newrec = Ext.create(me.model, data);
-               let pos = me.appendAtStart && !first_load ? 0 : me.data.length;
-               me.insert(pos, newrec);
-           }
-       };
-
-       let loadFn = function(s, records, success) {
-           if (!success) {
-               return;
-           }
-
-           me.suspendEvents();
-
-           // getSource returns null if data is not filtered
-           // if it is filtered it returns all records
-           let allItems = me.getData().getSource() || me.getData();
-
-           // remove vanished items
-           allItems.each(function(olditem) {
-               let item = me.rstore.getById(olditem.getId());
-               if (!item) {
-                   me.remove(olditem);
-               }
-           });
-
-           me.rstore.each(function(item) {
-               cond_add_item(item.data, item.getId());
-           });
-
-           me.filter();
-
-           if (me.sortAfterUpdate) {
-               me.sort();
-           }
-
-           first_load = false;
-
-           me.resumeEvents();
-           me.fireEvent('refresh', me);
-           me.fireEvent('datachanged', me);
-       };
-
-       if (me.rstore.isLoaded()) {
-           // if store is already loaded,
-           // insert items instantly
-           loadFn(me.rstore, [], true);
-       }
-
-       me.mon(me.rstore, 'load', loadFn);
-    },
-});
diff --git a/data/ObjectStore.js b/data/ObjectStore.js
deleted file mode 100644 (file)
index 860cbfd..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-/* This store encapsulates data items which are organized as an Array of key-values Objects
- * ie data[0] contains something like {key: "keyboard", value: "da"}
-*
-* Designed to work with the KeyValue model and the JsonObject data reader
-*/
-Ext.define('Proxmox.data.ObjectStore', {
-    extend: 'Proxmox.data.UpdateStore',
-
-    getRecord: function() {
-       let me = this;
-       let record = Ext.create('Ext.data.Model');
-       me.getData().each(function(item) {
-           record.set(item.data.key, item.data.value);
-       });
-       record.commit(true);
-       return record;
-    },
-
-    constructor: function(config) {
-       let me = this;
-
-        config = config || {};
-
-       if (!config.storeid) {
-           config.storeid = 'proxmox-store-' + ++Ext.idSeed;
-       }
-
-        Ext.applyIf(config, {
-           model: 'KeyValue',
-            proxy: {
-                type: 'proxmox',
-               url: config.url,
-               extraParams: config.extraParams,
-                reader: {
-                   type: 'jsonobject',
-                   rows: config.rows,
-                   readArray: config.readArray,
-                   rootProperty: config.root || 'data',
-               },
-            },
-        });
-
-        me.callParent([config]);
-    },
-});
diff --git a/data/ProxmoxProxy.js b/data/ProxmoxProxy.js
deleted file mode 100644 (file)
index 53e92f3..0000000
+++ /dev/null
@@ -1,74 +0,0 @@
-Ext.define('Proxmox.RestProxy', {
-    extend: 'Ext.data.RestProxy',
-    alias: 'proxy.proxmox',
-
-    pageParam: null,
-    startParam: null,
-    limitParam: null,
-    groupParam: null,
-    sortParam: null,
-    filterParam: null,
-    noCache: false,
-
-    afterRequest: function(request, success) {
-       this.fireEvent('afterload', this, request, success);
-    },
-
-    constructor: function(config) {
-       Ext.applyIf(config, {
-           reader: {
-               type: 'json',
-               rootProperty: config.root || 'data',
-           },
-       });
-
-       this.callParent([config]);
-    },
-}, function() {
-    Ext.define('KeyValue', {
-       extend: "Ext.data.Model",
-       fields: ['key', 'value'],
-       idProperty: 'key',
-    });
-
-    Ext.define('KeyValuePendingDelete', {
-       extend: "Ext.data.Model",
-       fields: ['key', 'value', 'pending', 'delete'],
-       idProperty: 'key',
-    });
-
-    Ext.define('proxmox-tasks', {
-       extend: 'Ext.data.Model',
-       fields: [
-           { name: 'starttime', type: 'date', dateFormat: 'timestamp' },
-           { name: 'endtime', type: 'date', dateFormat: 'timestamp' },
-           { name: 'pid', type: 'int' },
-           'node', 'upid', 'user', 'status', 'type', 'id',
-       ],
-       idProperty: 'upid',
-    });
-
-    Ext.define('proxmox-cluster-log', {
-       extend: 'Ext.data.Model',
-       fields: [
-           { name: 'uid', type: 'int' },
-           { name: 'time', type: 'date', dateFormat: 'timestamp' },
-           { name: 'pri', type: 'int' },
-           { name: 'pid', type: 'int' },
-           'node', 'user', 'tag', 'msg',
-           {
-               name: 'id',
-               convert: function(value, record) {
-                   let info = record.data;
-
-                   if (value) {
-                       return value;
-                   }
-                   // compute unique ID
-                   return info.uid + ':' + info.node;
-               },
-           },
-       ],
-       idProperty: 'id',
-    });
-});
diff --git a/data/RRDStore.js b/data/RRDStore.js
deleted file mode 100644 (file)
index 67ffb57..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-/* Extends the Proxmox.data.UpdateStore type
- *
- *
- */
-Ext.define('Proxmox.data.RRDStore', {
-    extend: 'Proxmox.data.UpdateStore',
-    alias: 'store.proxmoxRRDStore',
-
-    setRRDUrl: function(timeframe, cf) {
-       let me = this;
-       if (!timeframe) {
-           timeframe = me.timeframe;
-       }
-
-       if (!cf) {
-           cf = me.cf;
-       }
-
-       me.proxy.url = me.rrdurl + "?timeframe=" + timeframe + "&cf=" + cf;
-    },
-
-    proxy: {
-       type: 'proxmox',
-    },
-
-    timeframe: 'hour',
-
-    cf: 'AVERAGE',
-
-    constructor: function(config) {
-       let me = this;
-
-       config = config || {};
-
-       // set default interval to 30seconds
-       if (!config.interval) {
-           config.interval = 30000;
-       }
-
-       // set a new storeid
-       if (!config.storeid) {
-           config.storeid = 'rrdstore-' + ++Ext.idSeed;
-       }
-
-       // rrdurl is required
-       if (!config.rrdurl) {
-           throw "no rrdurl specified";
-       }
-
-       let stateid = 'proxmoxRRDTypeSelection';
-       let sp = Ext.state.Manager.getProvider();
-       let stateinit = sp.get(stateid);
-
-        if (stateinit) {
-           if (stateinit.timeframe !== me.timeframe || stateinit.cf !== me.rrdcffn) {
-               me.timeframe = stateinit.timeframe;
-               me.rrdcffn = stateinit.cf;
-           }
-       }
-
-       me.callParent([config]);
-
-       me.setRRDUrl();
-       me.mon(sp, 'statechange', function(prov, key, state) {
-           if (key === stateid) {
-               if (state && state.id) {
-                   if (state.timeframe !== me.timeframe || state.cf !== me.cf) {
-                       me.timeframe = state.timeframe;
-                       me.cf = state.cf;
-                       me.setRRDUrl();
-                       me.reload();
-                   }
-               }
-           }
-       });
-    },
-});
diff --git a/data/TimezoneStore.js b/data/TimezoneStore.js
deleted file mode 100644 (file)
index a67ad8b..0000000
+++ /dev/null
@@ -1,419 +0,0 @@
-Ext.define('Timezone', {
-    extend: 'Ext.data.Model',
-    fields: ['zone'],
-});
-
-Ext.define('Proxmox.data.TimezoneStore', {
-    extend: 'Ext.data.Store',
-    model: 'Timezone',
-    data: [
-           ['Africa/Abidjan'],
-           ['Africa/Accra'],
-           ['Africa/Addis_Ababa'],
-           ['Africa/Algiers'],
-           ['Africa/Asmara'],
-           ['Africa/Bamako'],
-           ['Africa/Bangui'],
-           ['Africa/Banjul'],
-           ['Africa/Bissau'],
-           ['Africa/Blantyre'],
-           ['Africa/Brazzaville'],
-           ['Africa/Bujumbura'],
-           ['Africa/Cairo'],
-           ['Africa/Casablanca'],
-           ['Africa/Ceuta'],
-           ['Africa/Conakry'],
-           ['Africa/Dakar'],
-           ['Africa/Dar_es_Salaam'],
-           ['Africa/Djibouti'],
-           ['Africa/Douala'],
-           ['Africa/El_Aaiun'],
-           ['Africa/Freetown'],
-           ['Africa/Gaborone'],
-           ['Africa/Harare'],
-           ['Africa/Johannesburg'],
-           ['Africa/Kampala'],
-           ['Africa/Khartoum'],
-           ['Africa/Kigali'],
-           ['Africa/Kinshasa'],
-           ['Africa/Lagos'],
-           ['Africa/Libreville'],
-           ['Africa/Lome'],
-           ['Africa/Luanda'],
-           ['Africa/Lubumbashi'],
-           ['Africa/Lusaka'],
-           ['Africa/Malabo'],
-           ['Africa/Maputo'],
-           ['Africa/Maseru'],
-           ['Africa/Mbabane'],
-           ['Africa/Mogadishu'],
-           ['Africa/Monrovia'],
-           ['Africa/Nairobi'],
-           ['Africa/Ndjamena'],
-           ['Africa/Niamey'],
-           ['Africa/Nouakchott'],
-           ['Africa/Ouagadougou'],
-           ['Africa/Porto-Novo'],
-           ['Africa/Sao_Tome'],
-           ['Africa/Tripoli'],
-           ['Africa/Tunis'],
-           ['Africa/Windhoek'],
-           ['America/Adak'],
-           ['America/Anchorage'],
-           ['America/Anguilla'],
-           ['America/Antigua'],
-           ['America/Araguaina'],
-           ['America/Argentina/Buenos_Aires'],
-           ['America/Argentina/Catamarca'],
-           ['America/Argentina/Cordoba'],
-           ['America/Argentina/Jujuy'],
-           ['America/Argentina/La_Rioja'],
-           ['America/Argentina/Mendoza'],
-           ['America/Argentina/Rio_Gallegos'],
-           ['America/Argentina/Salta'],
-           ['America/Argentina/San_Juan'],
-           ['America/Argentina/San_Luis'],
-           ['America/Argentina/Tucuman'],
-           ['America/Argentina/Ushuaia'],
-           ['America/Aruba'],
-           ['America/Asuncion'],
-           ['America/Atikokan'],
-           ['America/Bahia'],
-           ['America/Bahia_Banderas'],
-           ['America/Barbados'],
-           ['America/Belem'],
-           ['America/Belize'],
-           ['America/Blanc-Sablon'],
-           ['America/Boa_Vista'],
-           ['America/Bogota'],
-           ['America/Boise'],
-           ['America/Cambridge_Bay'],
-           ['America/Campo_Grande'],
-           ['America/Cancun'],
-           ['America/Caracas'],
-           ['America/Cayenne'],
-           ['America/Cayman'],
-           ['America/Chicago'],
-           ['America/Chihuahua'],
-           ['America/Costa_Rica'],
-           ['America/Cuiaba'],
-           ['America/Curacao'],
-           ['America/Danmarkshavn'],
-           ['America/Dawson'],
-           ['America/Dawson_Creek'],
-           ['America/Denver'],
-           ['America/Detroit'],
-           ['America/Dominica'],
-           ['America/Edmonton'],
-           ['America/Eirunepe'],
-           ['America/El_Salvador'],
-           ['America/Fortaleza'],
-           ['America/Glace_Bay'],
-           ['America/Godthab'],
-           ['America/Goose_Bay'],
-           ['America/Grand_Turk'],
-           ['America/Grenada'],
-           ['America/Guadeloupe'],
-           ['America/Guatemala'],
-           ['America/Guayaquil'],
-           ['America/Guyana'],
-           ['America/Halifax'],
-           ['America/Havana'],
-           ['America/Hermosillo'],
-           ['America/Indiana/Indianapolis'],
-           ['America/Indiana/Knox'],
-           ['America/Indiana/Marengo'],
-           ['America/Indiana/Petersburg'],
-           ['America/Indiana/Tell_City'],
-           ['America/Indiana/Vevay'],
-           ['America/Indiana/Vincennes'],
-           ['America/Indiana/Winamac'],
-           ['America/Inuvik'],
-           ['America/Iqaluit'],
-           ['America/Jamaica'],
-           ['America/Juneau'],
-           ['America/Kentucky/Louisville'],
-           ['America/Kentucky/Monticello'],
-           ['America/La_Paz'],
-           ['America/Lima'],
-           ['America/Los_Angeles'],
-           ['America/Maceio'],
-           ['America/Managua'],
-           ['America/Manaus'],
-           ['America/Marigot'],
-           ['America/Martinique'],
-           ['America/Matamoros'],
-           ['America/Mazatlan'],
-           ['America/Menominee'],
-           ['America/Merida'],
-           ['America/Mexico_City'],
-           ['America/Miquelon'],
-           ['America/Moncton'],
-           ['America/Monterrey'],
-           ['America/Montevideo'],
-           ['America/Montreal'],
-           ['America/Montserrat'],
-           ['America/Nassau'],
-           ['America/New_York'],
-           ['America/Nipigon'],
-           ['America/Nome'],
-           ['America/Noronha'],
-           ['America/North_Dakota/Center'],
-           ['America/North_Dakota/New_Salem'],
-           ['America/Ojinaga'],
-           ['America/Panama'],
-           ['America/Pangnirtung'],
-           ['America/Paramaribo'],
-           ['America/Phoenix'],
-           ['America/Port-au-Prince'],
-           ['America/Port_of_Spain'],
-           ['America/Porto_Velho'],
-           ['America/Puerto_Rico'],
-           ['America/Rainy_River'],
-           ['America/Rankin_Inlet'],
-           ['America/Recife'],
-           ['America/Regina'],
-           ['America/Resolute'],
-           ['America/Rio_Branco'],
-           ['America/Santa_Isabel'],
-           ['America/Santarem'],
-           ['America/Santiago'],
-           ['America/Santo_Domingo'],
-           ['America/Sao_Paulo'],
-           ['America/Scoresbysund'],
-           ['America/Shiprock'],
-           ['America/St_Barthelemy'],
-           ['America/St_Johns'],
-           ['America/St_Kitts'],
-           ['America/St_Lucia'],
-           ['America/St_Thomas'],
-           ['America/St_Vincent'],
-           ['America/Swift_Current'],
-           ['America/Tegucigalpa'],
-           ['America/Thule'],
-           ['America/Thunder_Bay'],
-           ['America/Tijuana'],
-           ['America/Toronto'],
-           ['America/Tortola'],
-           ['America/Vancouver'],
-           ['America/Whitehorse'],
-           ['America/Winnipeg'],
-           ['America/Yakutat'],
-           ['America/Yellowknife'],
-           ['Antarctica/Casey'],
-           ['Antarctica/Davis'],
-           ['Antarctica/DumontDUrville'],
-           ['Antarctica/Macquarie'],
-           ['Antarctica/Mawson'],
-           ['Antarctica/McMurdo'],
-           ['Antarctica/Palmer'],
-           ['Antarctica/Rothera'],
-           ['Antarctica/South_Pole'],
-           ['Antarctica/Syowa'],
-           ['Antarctica/Vostok'],
-           ['Arctic/Longyearbyen'],
-           ['Asia/Aden'],
-           ['Asia/Almaty'],
-           ['Asia/Amman'],
-           ['Asia/Anadyr'],
-           ['Asia/Aqtau'],
-           ['Asia/Aqtobe'],
-           ['Asia/Ashgabat'],
-           ['Asia/Baghdad'],
-           ['Asia/Bahrain'],
-           ['Asia/Baku'],
-           ['Asia/Bangkok'],
-           ['Asia/Beirut'],
-           ['Asia/Bishkek'],
-           ['Asia/Brunei'],
-           ['Asia/Choibalsan'],
-           ['Asia/Chongqing'],
-           ['Asia/Colombo'],
-           ['Asia/Damascus'],
-           ['Asia/Dhaka'],
-           ['Asia/Dili'],
-           ['Asia/Dubai'],
-           ['Asia/Dushanbe'],
-           ['Asia/Gaza'],
-           ['Asia/Harbin'],
-           ['Asia/Ho_Chi_Minh'],
-           ['Asia/Hong_Kong'],
-           ['Asia/Hovd'],
-           ['Asia/Irkutsk'],
-           ['Asia/Jakarta'],
-           ['Asia/Jayapura'],
-           ['Asia/Jerusalem'],
-           ['Asia/Kabul'],
-           ['Asia/Kamchatka'],
-           ['Asia/Karachi'],
-           ['Asia/Kashgar'],
-           ['Asia/Kathmandu'],
-           ['Asia/Kolkata'],
-           ['Asia/Krasnoyarsk'],
-           ['Asia/Kuala_Lumpur'],
-           ['Asia/Kuching'],
-           ['Asia/Kuwait'],
-           ['Asia/Macau'],
-           ['Asia/Magadan'],
-           ['Asia/Makassar'],
-           ['Asia/Manila'],
-           ['Asia/Muscat'],
-           ['Asia/Nicosia'],
-           ['Asia/Novokuznetsk'],
-           ['Asia/Novosibirsk'],
-           ['Asia/Omsk'],
-           ['Asia/Oral'],
-           ['Asia/Phnom_Penh'],
-           ['Asia/Pontianak'],
-           ['Asia/Pyongyang'],
-           ['Asia/Qatar'],
-           ['Asia/Qyzylorda'],
-           ['Asia/Rangoon'],
-           ['Asia/Riyadh'],
-           ['Asia/Sakhalin'],
-           ['Asia/Samarkand'],
-           ['Asia/Seoul'],
-           ['Asia/Shanghai'],
-           ['Asia/Singapore'],
-           ['Asia/Taipei'],
-           ['Asia/Tashkent'],
-           ['Asia/Tbilisi'],
-           ['Asia/Tehran'],
-           ['Asia/Thimphu'],
-           ['Asia/Tokyo'],
-           ['Asia/Ulaanbaatar'],
-           ['Asia/Urumqi'],
-           ['Asia/Vientiane'],
-           ['Asia/Vladivostok'],
-           ['Asia/Yakutsk'],
-           ['Asia/Yekaterinburg'],
-           ['Asia/Yerevan'],
-           ['Atlantic/Azores'],
-           ['Atlantic/Bermuda'],
-           ['Atlantic/Canary'],
-           ['Atlantic/Cape_Verde'],
-           ['Atlantic/Faroe'],
-           ['Atlantic/Madeira'],
-           ['Atlantic/Reykjavik'],
-           ['Atlantic/South_Georgia'],
-           ['Atlantic/St_Helena'],
-           ['Atlantic/Stanley'],
-           ['Australia/Adelaide'],
-           ['Australia/Brisbane'],
-           ['Australia/Broken_Hill'],
-           ['Australia/Currie'],
-           ['Australia/Darwin'],
-           ['Australia/Eucla'],
-           ['Australia/Hobart'],
-           ['Australia/Lindeman'],
-           ['Australia/Lord_Howe'],
-           ['Australia/Melbourne'],
-           ['Australia/Perth'],
-           ['Australia/Sydney'],
-           ['Europe/Amsterdam'],
-           ['Europe/Andorra'],
-           ['Europe/Athens'],
-           ['Europe/Belgrade'],
-           ['Europe/Berlin'],
-           ['Europe/Bratislava'],
-           ['Europe/Brussels'],
-           ['Europe/Bucharest'],
-           ['Europe/Budapest'],
-           ['Europe/Chisinau'],
-           ['Europe/Copenhagen'],
-           ['Europe/Dublin'],
-           ['Europe/Gibraltar'],
-           ['Europe/Guernsey'],
-           ['Europe/Helsinki'],
-           ['Europe/Isle_of_Man'],
-           ['Europe/Istanbul'],
-           ['Europe/Jersey'],
-           ['Europe/Kaliningrad'],
-           ['Europe/Kiev'],
-           ['Europe/Lisbon'],
-           ['Europe/Ljubljana'],
-           ['Europe/London'],
-           ['Europe/Luxembourg'],
-           ['Europe/Madrid'],
-           ['Europe/Malta'],
-           ['Europe/Mariehamn'],
-           ['Europe/Minsk'],
-           ['Europe/Monaco'],
-           ['Europe/Moscow'],
-           ['Europe/Oslo'],
-           ['Europe/Paris'],
-           ['Europe/Podgorica'],
-           ['Europe/Prague'],
-           ['Europe/Riga'],
-           ['Europe/Rome'],
-           ['Europe/Samara'],
-           ['Europe/San_Marino'],
-           ['Europe/Sarajevo'],
-           ['Europe/Simferopol'],
-           ['Europe/Skopje'],
-           ['Europe/Sofia'],
-           ['Europe/Stockholm'],
-           ['Europe/Tallinn'],
-           ['Europe/Tirane'],
-           ['Europe/Uzhgorod'],
-           ['Europe/Vaduz'],
-           ['Europe/Vatican'],
-           ['Europe/Vienna'],
-           ['Europe/Vilnius'],
-           ['Europe/Volgograd'],
-           ['Europe/Warsaw'],
-           ['Europe/Zagreb'],
-           ['Europe/Zaporozhye'],
-           ['Europe/Zurich'],
-           ['Indian/Antananarivo'],
-           ['Indian/Chagos'],
-           ['Indian/Christmas'],
-           ['Indian/Cocos'],
-           ['Indian/Comoro'],
-           ['Indian/Kerguelen'],
-           ['Indian/Mahe'],
-           ['Indian/Maldives'],
-           ['Indian/Mauritius'],
-           ['Indian/Mayotte'],
-           ['Indian/Reunion'],
-           ['Pacific/Apia'],
-           ['Pacific/Auckland'],
-           ['Pacific/Chatham'],
-           ['Pacific/Chuuk'],
-           ['Pacific/Easter'],
-           ['Pacific/Efate'],
-           ['Pacific/Enderbury'],
-           ['Pacific/Fakaofo'],
-           ['Pacific/Fiji'],
-           ['Pacific/Funafuti'],
-           ['Pacific/Galapagos'],
-           ['Pacific/Gambier'],
-           ['Pacific/Guadalcanal'],
-           ['Pacific/Guam'],
-           ['Pacific/Honolulu'],
-           ['Pacific/Johnston'],
-           ['Pacific/Kiritimati'],
-           ['Pacific/Kosrae'],
-           ['Pacific/Kwajalein'],
-           ['Pacific/Majuro'],
-           ['Pacific/Marquesas'],
-           ['Pacific/Midway'],
-           ['Pacific/Nauru'],
-           ['Pacific/Niue'],
-           ['Pacific/Norfolk'],
-           ['Pacific/Noumea'],
-           ['Pacific/Pago_Pago'],
-           ['Pacific/Palau'],
-           ['Pacific/Pitcairn'],
-           ['Pacific/Pohnpei'],
-           ['Pacific/Port_Moresby'],
-           ['Pacific/Rarotonga'],
-           ['Pacific/Saipan'],
-           ['Pacific/Tahiti'],
-           ['Pacific/Tarawa'],
-           ['Pacific/Tongatapu'],
-           ['Pacific/Wake'],
-           ['Pacific/Wallis'],
-           ['UTC'],
-       ],
-});
diff --git a/data/UpdateStore.js b/data/UpdateStore.js
deleted file mode 100644 (file)
index be85e4f..0000000
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Extends the Ext.data.Store type with  startUpdate() and stopUpdate() methods
- * to refresh the store data in the background.
- * Components using this store directly will flicker due to the redisplay of
- * the element ater 'config.interval' ms.
- *
- * Note that you have to set 'autoStart' or call startUpdate() once yourself
- * for the background load to begin.
- */
-Ext.define('Proxmox.data.UpdateStore', {
-    extend: 'Ext.data.Store',
-    alias: 'store.update',
-
-    config: {
-       interval: 3000,
-
-       isStopped: true,
-
-       autoStart: false,
-    },
-
-    destroy: function() {
-       let me = this;
-       me.stopUpdate();
-       me.callParent();
-    },
-
-    constructor: function(config) {
-       let me = this;
-
-       config = config || {};
-       if (config.interval === undefined) {
-           delete config.interval;
-       }
-
-       if (!config.storeid) {
-           throw "no storeid specified";
-       }
-
-       let load_task = new Ext.util.DelayedTask();
-
-       let run_load_task = function() {
-           if (me.getIsStopped()) {
-               return;
-           }
-
-           if (Proxmox.Utils.authOK()) {
-               let start = new Date();
-               me.load(function() {
-                   let runtime = new Date() - start;
-                   let interval = me.getInterval() + runtime*2;
-                   load_task.delay(interval, run_load_task);
-               });
-           } else {
-               load_task.delay(200, run_load_task);
-           }
-       };
-
-       Ext.apply(config, {
-           startUpdate: function() {
-               me.setIsStopped(false);
-               // run_load_task(); this makes problems with chrome
-               load_task.delay(1, run_load_task);
-           },
-           stopUpdate: function() {
-               me.setIsStopped(true);
-               load_task.cancel();
-           },
-       });
-
-       me.callParent([config]);
-
-       me.load_task = load_task;
-
-       if (me.getAutoStart()) {
-           me.startUpdate();
-       }
-    },
-});
diff --git a/data/model/Realm.js b/data/model/Realm.js
deleted file mode 100644 (file)
index dce270d..0000000
+++ /dev/null
@@ -1,29 +0,0 @@
-Ext.define('pmx-domains', {
-    extend: "Ext.data.Model",
-    fields: [
-       'realm', 'type', 'comment', 'default',
-       {
-           name: 'tfa',
-           allowNull: true,
-       },
-       {
-           name: 'descr',
-           convert: function(value, { data={} }) {
-               if (value) return Ext.String.htmlEncode(value);
-
-               let text = data.comment || data.realm;
-
-               if (data.tfa) {
-                   text += ` (+ ${data.tfa})`;
-               }
-
-               return Ext.String.htmlEncode(text);
-           },
-       },
-    ],
-    idProperty: 'realm',
-    proxy: {
-       type: 'proxmox',
-       url: "/api2/json/access/domains",
-    },
-});
diff --git a/data/reader/JsonObject.js b/data/reader/JsonObject.js
deleted file mode 100644 (file)
index 0489df8..0000000
+++ /dev/null
@@ -1,123 +0,0 @@
-/* A reader to store a single JSON Object (hash) into a storage.
- * Also accepts an array containing a single hash.
- *
- * So it can read:
- *
- * example1: {data1: "xyz", data2: "abc"}
- * returns [{key: "data1", value: "xyz"}, {key: "data2", value: "abc"}]
- *
- * example2: [ {data1: "xyz", data2: "abc"} ]
- * returns [{key: "data1", value: "xyz"}, {key: "data2", value: "abc"}]
- *
- * If you set 'readArray', the reader expexts the object as array:
- *
- * example3: [ { key: "data1", value: "xyz", p2: "cde" },  { key: "data2", value: "abc", p2: "efg" }]
- * returns [{key: "data1", value: "xyz", p2: "cde}, {key: "data2", value: "abc", p2: "efg"}]
- *
- * Note: The records can contain additional properties (like 'p2' above) when you use 'readArray'
- *
- * Additional feature: specify allowed properties with default values with 'rows' object
- *
- * let rows = {
- *   memory: {
- *     required: true,
- *     defaultValue: 512
- *   }
- * }
- *
- */
-
-Ext.define('Proxmox.data.reader.JsonObject', {
-    extend: 'Ext.data.reader.Json',
-    alias: 'reader.jsonobject',
-
-    readArray: false,
-
-    rows: undefined,
-
-    constructor: function(config) {
-        let me = this;
-
-        Ext.apply(me, config || {});
-
-       me.callParent([config]);
-    },
-
-    getResponseData: function(response) {
-       let me = this;
-
-       let data = [];
-        try {
-        let result = Ext.decode(response.responseText);
-        // get our data items inside the server response
-        let root = result[me.getRootProperty()];
-
-           if (me.readArray) {
-               let rec_hash = {};
-               Ext.Array.each(root, function(rec) {
-                   if (Ext.isDefined(rec.key)) {
-                       rec_hash[rec.key] = rec;
-                   }
-               });
-
-               if (me.rows) {
-                   Ext.Object.each(me.rows, function(key, rowdef) {
-                       let rec = rec_hash[key];
-                       if (Ext.isDefined(rec)) {
-                           if (!Ext.isDefined(rec.value)) {
-                               rec.value = rowdef.defaultValue;
-                           }
-                           data.push(rec);
-                       } else if (Ext.isDefined(rowdef.defaultValue)) {
-                           data.push({ key: key, value: rowdef.defaultValue });
-                       } else if (rowdef.required) {
-                           data.push({ key: key, value: undefined });
-                       }
-                   });
-               } else {
-                   Ext.Array.each(root, function(rec) {
-                       if (Ext.isDefined(rec.key)) {
-                           data.push(rec);
-                       }
-                   });
-               }
-           } else {
-               let org_root = root;
-
-               if (Ext.isArray(org_root)) {
-                   if (root.length === 1) {
-                       root = org_root[0];
-                   } else {
-                       root = {};
-                   }
-               }
-
-               if (me.rows) {
-                   Ext.Object.each(me.rows, function(key, rowdef) {
-                       if (Ext.isDefined(root[key])) {
-                           data.push({ key: key, value: root[key] });
-                       } else if (Ext.isDefined(rowdef.defaultValue)) {
-                           data.push({ key: key, value: rowdef.defaultValue });
-                       } else if (rowdef.required) {
-                           data.push({ key: key, value: undefined });
-                       }
-                   });
-               } else {
-                   Ext.Object.each(root, function(key, value) {
-                       data.push({ key: key, value: value });
-                   });
-               }
-           }
-       } catch (ex) {
-            Ext.Error.raise({
-                response: response,
-                json: response.responseText,
-                parseError: ex,
-                msg: 'Unable to parse the JSON returned by the server: ' + ex.toString(),
-            });
-        }
-
-       return data;
-    },
-});
-
index 800192db9c89564c56460087be607dc57b2e1718..1f9f20636039a9c650c48b8f82ba637690ef255c 100644 (file)
@@ -3,7 +3,8 @@ Section: web
 Priority: optional
 Maintainer: Proxmox Support Team <support@proxmox.com>
 Build-Depends: debhelper (>= 10~),
 Priority: optional
 Maintainer: Proxmox Support Team <support@proxmox.com>
 Build-Depends: debhelper (>= 10~),
-               rsync
+               pve-eslint,
+               rsync,
 Standards-Version: 3.9.8
 Homepage: http://www.proxmox.com
 
 Standards-Version: 3.9.8
 Homepage: http://www.proxmox.com
 
diff --git a/defines.mk b/defines.mk
deleted file mode 100644 (file)
index 99461e1..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-PACKAGE=proxmox-widget-toolkit
-
-BUILDDIR ?= ${PACKAGE}-${DEB_VERSION_UPSTREAM}
-GITVERSION:=$(shell git rev-parse HEAD)
-
-DEB=${PACKAGE}_${DEB_VERSION_UPSTREAM_REVISION}_all.deb
-DSC=${PACKAGE}_${DEB_VERSION_UPSTREAM_REVISION}.dsc
-
-DESTDIR=
-
-DOCDIR=${DESTDIR}/usr/share/doc/${PACKAGE}
-
-WWWBASEDIR=${DESTDIR}/usr/share/javascript/${PACKAGE}
-WWWCSSDIR=${WWWBASEDIR}/css
-WWWIMAGESDIR=${WWWBASEDIR}/images
diff --git a/form/BondModeSelector.js b/form/BondModeSelector.js
deleted file mode 100644 (file)
index 1fe45b9..0000000
+++ /dev/null
@@ -1,42 +0,0 @@
-Ext.define('Proxmox.form.BondModeSelector', {
-    extend: 'Proxmox.form.KVComboBox',
-    alias: ['widget.bondModeSelector'],
-
-    openvswitch: false,
-
-    initComponent: function() {
-       let me = this;
-
-       if (me.openvswitch) {
-           me.comboItems = Proxmox.Utils.bond_mode_array([
-              'active-backup',
-              'balance-slb',
-              'lacp-balance-slb',
-              'lacp-balance-tcp',
-           ]);
-       } else {
-           me.comboItems = Proxmox.Utils.bond_mode_array([
-               'balance-rr',
-               'active-backup',
-               'balance-xor',
-               'broadcast',
-               '802.3ad',
-               'balance-tlb',
-               'balance-alb',
-           ]);
-       }
-
-       me.callParent();
-    },
-});
-
-Ext.define('Proxmox.form.BondPolicySelector', {
-    extend: 'Proxmox.form.KVComboBox',
-    alias: ['widget.bondPolicySelector'],
-    comboItems: [
-           ['layer2', 'layer2'],
-           ['layer2+3', 'layer2+3'],
-           ['layer3+4', 'layer3+4'],
-    ],
-});
-
diff --git a/form/Checkbox.js b/form/Checkbox.js
deleted file mode 100644 (file)
index 2b29e1c..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-Ext.define('Proxmox.form.Checkbox', {
-    extend: 'Ext.form.field.Checkbox',
-    alias: ['widget.proxmoxcheckbox'],
-
-    config: {
-       defaultValue: undefined,
-       deleteDefaultValue: false,
-       deleteEmpty: false,
-    },
-
-    inputValue: '1',
-
-    getSubmitData: function() {
-        let me = this,
-            data = null,
-            val;
-        if (!me.disabled && me.submitValue) {
-            val = me.getSubmitValue();
-            if (val !== null) {
-                data = {};
-               if (val === me.getDefaultValue() && me.getDeleteDefaultValue()) {
-                   data.delete = me.getName();
-               } else {
-                    data[me.getName()] = val;
-               }
-            } else if (me.getDeleteEmpty()) {
-               data = {};
-               data.delete = me.getName();
-           }
-        }
-        return data;
-    },
-
-    // also accept integer 1 as true
-    setRawValue: function(value) {
-       let me = this;
-
-       if (value === 1) {
-            me.callParent([true]);
-       } else {
-            me.callParent([value]);
-       }
-    },
-
-});
diff --git a/form/ComboGrid.js b/form/ComboGrid.js
deleted file mode 100644 (file)
index e5a1920..0000000
+++ /dev/null
@@ -1,457 +0,0 @@
-/*
- * ComboGrid component: a ComboBox where the dropdown menu (the
- * "Picker") is a Grid with Rows and Columns expects a listConfig
- * object with a columns property roughly based on the GridPicker from
- * https://www.sencha.com/forum/showthread.php?299909
- *
-*/
-
-Ext.define('Proxmox.form.ComboGrid', {
-    extend: 'Ext.form.field.ComboBox',
-    alias: ['widget.proxmoxComboGrid'],
-
-    // this value is used as default value after load()
-    preferredValue: undefined,
-
-    // hack: allow to select empty value
-    // seems extjs does not allow that when 'editable == false'
-    onKeyUp: function(e, t) {
-        let me = this;
-        let key = e.getKey();
-
-        if (!me.editable && me.allowBlank && !me.multiSelect &&
-           (key === e.BACKSPACE || key === e.DELETE)) {
-           me.setValue('');
-       }
-
-        me.callParent(arguments);
-    },
-
-    config: {
-       skipEmptyText: false,
-       notFoundIsValid: false,
-       deleteEmpty: false,
-    },
-
-    // needed to trigger onKeyUp etc.
-    enableKeyEvents: true,
-
-    editable: false,
-
-    triggers: {
-       clear: {
-           cls: 'pmx-clear-trigger',
-           weight: -1,
-           hidden: true,
-           handler: function() {
-               let me = this;
-               me.setValue('');
-           },
-       },
-    },
-
-    setValue: function(value) {
-       let me = this;
-       let empty = Ext.isArray(value) ? !value.length : !value;
-       me.triggers.clear.setVisible(!empty && me.allowBlank);
-       return me.callParent([value]);
-    },
-
-    // override ExtJS method
-    // if the field has multiSelect enabled, the store is not loaded, and
-    // the displayfield == valuefield, it saves the rawvalue as an array
-    // but the getRawValue method is only defined in the textfield class
-    // (which has not to deal with arrays) an returns the string in the
-    // field (not an array)
-    //
-    // so if we have multiselect enabled, return the rawValue (which
-    // should be an array) and else we do callParent so
-    // it should not impact any other use of the class
-    getRawValue: function() {
-       let me = this;
-       if (me.multiSelect) {
-           return me.rawValue;
-       } else {
-           return me.callParent();
-       }
-    },
-
-    getSubmitData: function() {
-       let me = this;
-
-       let data = null;
-       if (!me.disabled && me.submitValue) {
-           let val = me.getSubmitValue();
-           if (val !== null) {
-               data = {};
-               data[me.getName()] = val;
-           } else if (me.getDeleteEmpty()) {
-               data = {};
-               data.delete = me.getName();
-           }
-       }
-       return data;
-   },
-
-    getSubmitValue: function() {
-       let me = this;
-
-       let value = me.callParent();
-       if (value !== '') {
-           return value;
-       }
-
-       return me.getSkipEmptyText() ? null: value;
-    },
-
-    setAllowBlank: function(allowBlank) {
-       this.allowBlank = allowBlank;
-       this.validate();
-    },
-
-// override ExtJS protected method
-    onBindStore: function(store, initial) {
-       let me = this,
-           picker = me.picker,
-           extraKeySpec,
-           valueCollectionConfig;
-
-       // We're being bound, not unbound...
-       if (store) {
-           // If store was created from a 2 dimensional array with generated field names 'field1' and 'field2'
-           if (store.autoCreated) {
-               me.queryMode = 'local';
-               me.valueField = me.displayField = 'field1';
-               if (!store.expanded) {
-                   me.displayField = 'field2';
-               }
-
-               // displayTpl config will need regenerating with the autogenerated displayField name 'field1'
-               me.setDisplayTpl(null);
-           }
-           if (!Ext.isDefined(me.valueField)) {
-               me.valueField = me.displayField;
-           }
-
-           // Add a byValue index to the store so that we can efficiently look up records by the value field
-           // when setValue passes string value(s).
-           // The two indices (Ext.util.CollectionKeys) are configured unique: false, so that if duplicate keys
-           // are found, they are all returned by the get call.
-           // This is so that findByText and findByValue are able to return the *FIRST* matching value. By default,
-           // if unique is true, CollectionKey keeps the *last* matching value.
-           extraKeySpec = {
-               byValue: {
-                   rootProperty: 'data',
-                   unique: false,
-               },
-           };
-           extraKeySpec.byValue.property = me.valueField;
-           store.setExtraKeys(extraKeySpec);
-
-           if (me.displayField === me.valueField) {
-               store.byText = store.byValue;
-           } else {
-               extraKeySpec.byText = {
-                   rootProperty: 'data',
-                   unique: false,
-               };
-               extraKeySpec.byText.property = me.displayField;
-               store.setExtraKeys(extraKeySpec);
-           }
-
-           // We hold a collection of the values which have been selected, keyed by this field's valueField.
-           // This collection also functions as the selected items collection for the BoundList's selection model
-           valueCollectionConfig = {
-               rootProperty: 'data',
-               extraKeys: {
-                   byInternalId: {
-                       property: 'internalId',
-                   },
-                   byValue: {
-                       property: me.valueField,
-                       rootProperty: 'data',
-                   },
-               },
-               // Whenever this collection is changed by anyone, whether by this field adding to it,
-               // or the BoundList operating, we must refresh our value.
-               listeners: {
-                   beginupdate: me.onValueCollectionBeginUpdate,
-                   endupdate: me.onValueCollectionEndUpdate,
-                   scope: me,
-               },
-           };
-
-           // This becomes our collection of selected records for the Field.
-           me.valueCollection = new Ext.util.Collection(valueCollectionConfig);
-
-           // We use the selected Collection as our value collection and the basis
-           // for rendering the tag list.
-
-           //proxmox override: since the picker is represented by a grid panel,
-           // we changed here the selection to RowModel
-           me.pickerSelectionModel = new Ext.selection.RowModel({
-               mode: me.multiSelect ? 'SIMPLE' : 'SINGLE',
-               // There are situations when a row is selected on mousedown but then the mouse is
-               // dragged to another row and released.  In these situations, the event target for
-               // the click event won't be the row where the mouse was released but the boundview.
-               // The view will then determine that it should fire a container click, and the
-               // DataViewModel will then deselect all prior selections. Setting
-               // `deselectOnContainerClick` here will prevent the model from deselecting.
-               deselectOnContainerClick: false,
-               enableInitialSelection: false,
-               pruneRemoved: false,
-               selected: me.valueCollection,
-               store: store,
-               listeners: {
-                   scope: me,
-                   lastselectedchanged: me.updateBindSelection,
-               },
-           });
-
-           if (!initial) {
-               me.resetToDefault();
-           }
-
-           if (picker) {
-               picker.setSelectionModel(me.pickerSelectionModel);
-               if (picker.getStore() !== store) {
-                   picker.bindStore(store);
-               }
-           }
-       }
-    },
-
-    // copied from ComboBox
-    createPicker: function() {
-        let me = this;
-        let picker;
-
-        let pickerCfg = Ext.apply({
-                // proxmox overrides: display a grid for selection
-                xtype: 'gridpanel',
-                id: me.pickerId,
-                pickerField: me,
-                floating: true,
-                hidden: true,
-                store: me.store,
-                displayField: me.displayField,
-                preserveScrollOnRefresh: true,
-                pageSize: me.pageSize,
-                tpl: me.tpl,
-                selModel: me.pickerSelectionModel,
-                focusOnToFront: false,
-            }, me.listConfig, me.defaultListConfig);
-
-        picker = me.picker || Ext.widget(pickerCfg);
-
-        if (picker.getStore() !== me.store) {
-            picker.bindStore(me.store);
-        }
-
-        if (me.pageSize) {
-            picker.pagingToolbar.on('beforechange', me.onPageChange, me);
-        }
-
-        // proxmox overrides: pass missing method in gridPanel to its view
-        picker.refresh = function() {
-            picker.getSelectionModel().select(me.valueCollection.getRange());
-            picker.getView().refresh();
-        };
-        picker.getNodeByRecord = function() {
-            picker.getView().getNodeByRecord(arguments);
-        };
-
-        // We limit the height of the picker to fit in the space above
-        // or below this field unless the picker has its own ideas about that.
-        if (!picker.initialConfig.maxHeight) {
-            picker.on({
-                beforeshow: me.onBeforePickerShow,
-                scope: me,
-            });
-        }
-        picker.getSelectionModel().on({
-            beforeselect: me.onBeforeSelect,
-            beforedeselect: me.onBeforeDeselect,
-            focuschange: me.onFocusChange,
-            selectionChange: function(sm, selectedRecords) {
-                if (selectedRecords.length) {
-                    this.setValue(selectedRecords);
-                    this.fireEvent('select', me, selectedRecords);
-                }
-            },
-            scope: me,
-        });
-
-       // hack for extjs6
-       // when the clicked item is the same as the previously selected,
-       // it does not select the item
-       // instead we hide the picker
-       if (!me.multiSelect) {
-           picker.on('itemclick', function(sm, record) {
-               if (picker.getSelection()[0] === record) {
-                   picker.hide();
-               }
-           });
-       }
-
-       // when our store is not yet loaded, we increase
-       // the height of the gridpanel, so that we can see
-       // the loading mask
-       //
-       // we save the minheight to reset it after the load
-       picker.on('show', function() {
-           if (me.enableLoadMask) {
-               me.savedMinHeight = picker.getMinHeight();
-               picker.setMinHeight(100);
-           }
-       });
-
-        picker.getNavigationModel().navigateOnSpace = false;
-
-        return picker;
-    },
-
-    clearLocalFilter: function() {
-        let me = this,
-            filter = me.queryFilter;
-
-        if (filter) {
-            me.queryFilter = null;
-            me.changingFilters = true;
-            me.store.removeFilter(filter, true);
-            me.changingFilters = false;
-        }
-    },
-
-    isValueInStore: function(value) {
-       let me = this;
-       let store = me.store;
-       let found = false;
-
-       if (!store) {
-           return found;
-       }
-
-       // Make sure the current filter is removed before checking the store
-       // to prevent false negative results when iterating over a filtered store.
-       // All store.find*() method's operate on the filtered store.
-       if (me.queryFilter && me.queryMode === 'local' && me.clearFilterOnBlur) {
-           me.clearLocalFilter();
-       }
-
-       if (Ext.isArray(value)) {
-           Ext.Array.each(value, function(v) {
-               if (store.findRecord(me.valueField, v)) {
-                   found = true;
-                   return false; // break
-               }
-               return true;
-           });
-       } else {
-           found = !!store.findRecord(me.valueField, value);
-       }
-
-       return found;
-    },
-
-    validator: function(value) {
-       let me = this;
-
-       if (!value) {
-           return true; // handled later by allowEmpty in the getErrors call chain
-       }
-
-       // we normally get here the displayField as value, but if a valueField
-       // is configured we need to get the "actual" value, to ensure it is in
-       // the store. Below check is copied from ExtJS 6.0.2 ComboBox source
-       //
-       // we also have to get the 'real' value if the we have a mulitSelect
-       // Field but got a non array value
-       if ((me.valueField && me.valueField !== me.displayField) ||
-           (me.multiSelect && !Ext.isArray(value))) {
-           value = me.getValue();
-       }
-
-       if (!(me.notFoundIsValid || me.isValueInStore(value))) {
-           return gettext('Invalid Value');
-       }
-
-       return true;
-    },
-
-    // validate after enabling a field, otherwise blank fields with !allowBlank
-    // are sometimes not marked as invalid
-    setDisabled: function(value) {
-       this.callParent([value]);
-       this.validate();
-    },
-
-    initComponent: function() {
-       let me = this;
-
-       Ext.apply(me, {
-           queryMode: 'local',
-           matchFieldWidth: false,
-       });
-
-       Ext.applyIf(me, { value: '' }); // hack: avoid ExtJS validate() bug
-
-       Ext.applyIf(me.listConfig, { width: 400 });
-
-       me.callParent();
-
-       // Create the picker at an early stage, so it is available to store the previous selection
-       if (!me.picker) {
-           me.createPicker();
-       }
-
-       me.mon(me.store, 'beforeload', function() {
-           if (!me.isDisabled()) {
-               me.enableLoadMask = true;
-           }
-       });
-
-       // hack: autoSelect does not work
-       me.mon(me.store, 'load', function(store, r, success, o) {
-           if (success) {
-               me.clearInvalid();
-
-               if (me.enableLoadMask) {
-                   delete me.enableLoadMask;
-
-                   // if the picker exists,
-                   // we reset its minheight to the saved let/0
-                   // we have to update the layout, otherwise the height
-                   // gets not recalculated
-                   if (me.picker) {
-                       me.picker.setMinHeight(me.savedMinHeight || 0);
-                       delete me.savedMinHeight;
-                       me.picker.updateLayout();
-                   }
-               }
-
-               let def = me.getValue() || me.preferredValue;
-               if (def) {
-                   me.setValue(def, true); // sync with grid
-               }
-               let found = false;
-               if (def) {
-                   found = me.isValueInStore(def);
-               }
-
-               if (!found) {
-                   let rec = me.store.first();
-                   if (me.autoSelect && rec && rec.data) {
-                       def = rec.data[me.valueField];
-                       me.setValue(def, true);
-                   } else if (!me.allowBlank && !(Ext.isArray(def) ? def.length : def)) {
-                       me.setValue(def);
-                       if (!me.notFoundIsValid && !me.isDisabled()) {
-                           me.markInvalid(me.blankText);
-                       }
-                   }
-               }
-           }
-       });
-    },
-});
diff --git a/form/DateTimeField.js b/form/DateTimeField.js
deleted file mode 100644 (file)
index a061e15..0000000
+++ /dev/null
@@ -1,151 +0,0 @@
-Ext.define('Proxmox.DateTimeField', {
-    extend: 'Ext.form.FieldContainer',
-    xtype: 'promxoxDateTimeField',
-
-    layout: 'hbox',
-
-    referenceHolder: true,
-
-    submitFormat: 'U',
-
-    getValue: function() {
-       let me = this;
-       let d = me.lookupReference('dateentry').getValue();
-
-       if (d === undefined || d === null) { return null; }
-
-       let t = me.lookupReference('timeentry').getValue();
-
-       if (t === undefined || t === null) { return null; }
-
-       let offset = (t.getHours() * 3600 + t.getMinutes() * 60) * 1000;
-
-       return new Date(d.getTime() + offset);
-    },
-
-    getSubmitValue: function() {
-        let me = this;
-        let format = me.submitFormat;
-        let value = me.getValue();
-
-        return value ? Ext.Date.format(value, format) : null;
-    },
-
-    items: [
-       {
-           xtype: 'datefield',
-           editable: false,
-           reference: 'dateentry',
-           flex: 1,
-           format: 'Y-m-d',
-       },
-       {
-           xtype: 'timefield',
-           reference: 'timeentry',
-           format: 'H:i',
-           width: 80,
-           value: '00:00',
-           increment: 60,
-       },
-    ],
-
-    setMinValue: function(value) {
-       let me = this;
-       let current = me.getValue();
-       if (!value || !current) {
-           return;
-       }
-
-       let minhours = value.getHours();
-       let minminutes = value.getMinutes();
-
-       let hours = current.getHours();
-       let minutes = current.getMinutes();
-
-       value.setHours(0);
-       value.setMinutes(0);
-       value.setSeconds(0);
-       current.setHours(0);
-       current.setMinutes(0);
-       current.setSeconds(0);
-
-       let time = new Date();
-       if (current-value > 0) {
-           time.setHours(0);
-           time.setMinutes(0);
-           time.setSeconds(0);
-           time.setMilliseconds(0);
-       } else {
-           time.setHours(minhours);
-           time.setMinutes(minminutes);
-       }
-       me.lookup('timeentry').setMinValue(time);
-
-       // current time is smaller than the time part of the new minimum
-       // so we have to add 1 to the day
-       if (minhours*60+minminutes > hours*60+minutes) {
-           value.setDate(value.getDate()+1);
-       }
-       me.lookup('dateentry').setMinValue(value);
-    },
-
-    setMaxValue: function(value) {
-       let me = this;
-       let current = me.getValue();
-       if (!value || !current) {
-           return;
-       }
-
-       let maxhours = value.getHours();
-       let maxminutes = value.getMinutes();
-
-       let hours = current.getHours();
-       let minutes = current.getMinutes();
-
-       value.setHours(0);
-       value.setMinutes(0);
-       current.setHours(0);
-       current.setMinutes(0);
-
-       let time = new Date();
-       if (value-current > 0) {
-           time.setHours(23);
-           time.setMinutes(59);
-           time.setSeconds(59);
-       } else {
-           time.setHours(maxhours);
-           time.setMinutes(maxminutes);
-       }
-       me.lookup('timeentry').setMaxValue(time);
-
-       // current time is biger than the time part of the new maximum
-       // so we have to subtract 1 to the day
-       if (maxhours*60+maxminutes < hours*60+minutes) {
-           value.setDate(value.getDate()-1);
-       }
-
-       me.lookup('dateentry').setMaxValue(value);
-    },
-
-    initComponent: function() {
-       let me = this;
-
-       me.callParent();
-
-       let value = me.value || new Date();
-
-       me.lookupReference('dateentry').setValue(value);
-       me.lookupReference('timeentry').setValue(value);
-
-       if (me.minValue) {
-           me.setMinValue(me.minValue);
-       }
-
-       if (me.maxValue) {
-           me.setMaxValue(me.maxValue);
-       }
-
-       me.relayEvents(me.lookupReference('dateentry'), ['change']);
-       me.relayEvents(me.lookupReference('timeentry'), ['change']);
-    },
-});
diff --git a/form/DisplayEdit.js b/form/DisplayEdit.js
deleted file mode 100644 (file)
index b8060a5..0000000
+++ /dev/null
@@ -1,77 +0,0 @@
-Ext.define('Proxmox.form.field.DisplayEdit', {
-    extend: 'Ext.form.FieldContainer',
-    alias: 'widget.pmxDisplayEditField',
-
-    viewModel: {
-       parent: null,
-       data: {
-           editable: false,
-           value: undefined,
-       },
-    },
-
-    displayType: 'displayfield',
-
-    editConfig: {},
-    editable: false,
-    setEditable: function(editable) {
-       let me = this;
-       let vm = me.getViewModel();
-
-       me.editable = editable;
-       vm.set('editable', editable);
-    },
-
-    layout: 'fit',
-    defaults: {
-       hideLabel: true,
-    },
-
-    initComponent: function() {
-       let me = this;
-
-       let displayConfig = {
-           xtype: me.displayType,
-           bind: {},
-       };
-       Ext.applyIf(displayConfig, me.initialConfig);
-       delete displayConfig.editConfig;
-       delete displayConfig.editable;
-
-       let editConfig = Ext.apply({}, me.editConfig);
-       Ext.applyIf(editConfig, {
-           xtype: 'textfield',
-           bind: {},
-       });
-       Ext.applyIf(editConfig, displayConfig);
-
-       Ext.applyIf(displayConfig.bind, {
-           hidden: '{editable}',
-           disabled: '{editable}',
-           value: '{value}',
-       });
-       Ext.applyIf(editConfig.bind, {
-           hidden: '{!editable}',
-           disabled: '{!editable}',
-           value: '{value}',
-       });
-
-       // avoid glitch, start off correct even before viewmodel fixes it
-       editConfig.disabled = editConfig.hidden = !me.editable;
-       displayConfig.disabled = displayConfig.hidden = !!me.editable;
-
-       editConfig.name = displayConfig.name = me.name;
-
-       Ext.apply(me, {
-           items: [
-               editConfig,
-               displayConfig,
-           ],
-       });
-
-       me.callParent();
-
-       me.getViewModel().set('editable', me.editable);
-    },
-
-});
diff --git a/form/ExpireDate.js b/form/ExpireDate.js
deleted file mode 100644 (file)
index 61ff96f..0000000
+++ /dev/null
@@ -1,34 +0,0 @@
-// treats 0 as "never expires"
-Ext.define('Proxmox.form.field.ExpireDate', {
-    extend: 'Ext.form.field.Date',
-    alias: ['widget.pmxExpireDate'],
-
-    name: 'expire',
-    fieldLabel: gettext('Expire'),
-    emptyText: 'never',
-    format: 'Y-m-d',
-    submitFormat: 'U',
-
-    getSubmitValue: function() {
-       let me = this;
-
-       let value = me.callParent();
-       if (!value) value = 0;
-
-       return value;
-    },
-
-    setValue: function(value) {
-       let me = this;
-
-       if (Ext.isDefined(value)) {
-           if (!value) {
-               value = null;
-           } else if (!Ext.isDate(value)) {
-               value = new Date(value * 1000);
-           }
-       }
-       me.callParent([value]);
-    },
-
-});
diff --git a/form/IntegerField.js b/form/IntegerField.js
deleted file mode 100644 (file)
index 67dd215..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-Ext.define('Proxmox.form.field.Integer', {
-    extend: 'Ext.form.field.Number',
-    alias: 'widget.proxmoxintegerfield',
-
-    config: {
-       deleteEmpty: false,
-    },
-
-    allowDecimals: false,
-    allowExponential: false,
-    step: 1,
-
-   getSubmitData: function() {
-        let me = this,
-            data = null,
-            val;
-        if (!me.disabled && me.submitValue && !me.isFileUpload()) {
-            val = me.getSubmitValue();
-            if (val !== undefined && val !== null && val !== '') {
-                data = {};
-                data[me.getName()] = val;
-            } else if (me.getDeleteEmpty()) {
-               data = {};
-                data.delete = me.getName();
-           }
-        }
-        return data;
-    },
-
-});
diff --git a/form/KVComboBox.js b/form/KVComboBox.js
deleted file mode 100644 (file)
index 361217e..0000000
+++ /dev/null
@@ -1,81 +0,0 @@
-/* Key-Value ComboBox
- *
- * config properties:
- * comboItems: an array of Key - Value pairs
- * deleteEmpty: if set to true (default), an empty value received from the
- * comboBox will reset the property to its default value
- */
-Ext.define('Proxmox.form.KVComboBox', {
-    extend: 'Ext.form.field.ComboBox',
-    alias: 'widget.proxmoxKVComboBox',
-
-    config: {
-       deleteEmpty: true,
-    },
-
-    comboItems: undefined,
-    displayField: 'value',
-    valueField: 'key',
-    queryMode: 'local',
-
-    // overide framework function to implement deleteEmpty behaviour
-    getSubmitData: function() {
-        let me = this,
-            data = null,
-            val;
-        if (!me.disabled && me.submitValue) {
-            val = me.getSubmitValue();
-            if (val !== null && val !== '' && val !== '__default__') {
-                data = {};
-                data[me.getName()] = val;
-            } else if (me.getDeleteEmpty()) {
-                data = {};
-                data.delete = me.getName();
-            }
-        }
-        return data;
-    },
-
-    validator: function(val) {
-       let me = this;
-
-       if (me.editable || val === null || val === '') {
-           return true;
-       }
-
-       if (me.store.getCount() > 0) {
-           let values = me.multiSelect ? val.split(me.delimiter) : [val];
-           let items = me.store.getData().collect('value', 'data');
-           if (Ext.Array.every(values, function(value) {
-               return Ext.Array.contains(items, value);
-           })) {
-               return true;
-           }
-       }
-
-       // returns a boolean or string
-       return "value '" + val + "' not allowed!";
-    },
-
-    initComponent: function() {
-       let me = this;
-
-       me.store = Ext.create('Ext.data.ArrayStore', {
-           model: 'KeyValue',
-           data: me.comboItems,
-       });
-
-       if (me.initialConfig.editable === undefined) {
-           me.editable = false;
-       }
-
-       me.callParent();
-    },
-
-    setComboItems: function(items) {
-       let me = this;
-
-       me.getStore().setData(items);
-    },
-
-});
diff --git a/form/LanguageSelector.js b/form/LanguageSelector.js
deleted file mode 100644 (file)
index 92f28b1..0000000
+++ /dev/null
@@ -1,6 +0,0 @@
-Ext.define('Proxmox.form.LanguageSelector', {
-    extend: 'Proxmox.form.KVComboBox',
-    xtype: 'proxmoxLanguageSelector',
-
-    comboItems: Proxmox.Utils.language_array(),
-});
diff --git a/form/NetworkSelector.js b/form/NetworkSelector.js
deleted file mode 100644 (file)
index b87efc1..0000000
+++ /dev/null
@@ -1,129 +0,0 @@
-Ext.define('Proxmox.form.NetworkSelectorController', {
-    extend: 'Ext.app.ViewController',
-    alias: 'controller.proxmoxNetworkSelectorController',
-
-    init: function(view) {
-       let me = this;
-
-       if (!view.nodename) {
-           throw "missing custom view config: nodename";
-       }
-       view.getStore().getProxy().setUrl('/api2/json/nodes/'+ view.nodename + '/network');
-    },
-});
-
-Ext.define('Proxmox.data.NetworkSelector', {
-    extend: 'Ext.data.Model',
-    fields: [
-       { name: 'active' },
-       { name: 'cidr' },
-       { name: 'cidr6' },
-       { name: 'address' },
-       { name: 'address6' },
-       { name: 'comments' },
-       { name: 'iface' },
-       { name: 'slaves' },
-       { name: 'type' },
-    ],
-});
-
-Ext.define('Proxmox.form.NetworkSelector', {
-    extend: 'Proxmox.form.ComboGrid',
-    alias: 'widget.proxmoxNetworkSelector',
-
-    controller: 'proxmoxNetworkSelectorController',
-
-    nodename: 'localhost',
-    setNodename: function(nodename) {
-       this.nodename = nodename;
-       let networkSelectorStore = this.getStore();
-       networkSelectorStore.removeAll();
-       // because of manual local copy of data for ip4/6
-       this.getPicker().refresh();
-       if (networkSelectorStore && typeof networkSelectorStore.getProxy === 'function') {
-           networkSelectorStore.getProxy().setUrl('/api2/json/nodes/'+ nodename + '/network');
-           networkSelectorStore.load();
-       }
-    },
-    // set default value to empty array, else it inits it with
-    // null and after the store load it is an empty array,
-    // triggering dirtychange
-    value: [],
-    valueField: 'cidr',
-    displayField: 'cidr',
-    store: {
-       autoLoad: true,
-       model: 'Proxmox.data.NetworkSelector',
-       proxy: {
-           type: 'proxmox',
-       },
-       sorters: [
-           {
-               property: 'iface',
-               direction: 'ASC',
-           },
-       ],
-       filters: [
-           function(item) {
-               return item.data.cidr;
-           },
-       ],
-       listeners: {
-           load: function(store, records, successfull) {
-               if (successfull) {
-                   records.forEach(function(record) {
-                       if (record.data.cidr6) {
-                           let dest = record.data.cidr ? record.copy(null) : record;
-                           dest.data.cidr = record.data.cidr6;
-                           dest.data.address = record.data.address6;
-                           delete record.data.cidr6;
-                           dest.data.comments = record.data.comments6;
-                           delete record.data.comments6;
-                           store.add(dest);
-                       }
-                   });
-               }
-           },
-       },
-    },
-    listConfig: {
-       width: 600,
-       columns: [
-           {
-
-               header: gettext('CIDR'),
-               dataIndex: 'cidr',
-               hideable: false,
-               flex: 1,
-           },
-           {
-
-               header: gettext('IP'),
-               dataIndex: 'address',
-               hidden: true,
-           },
-           {
-               header: gettext('Interface'),
-               width: 90,
-               dataIndex: 'iface',
-           },
-           {
-               header: gettext('Active'),
-               renderer: Proxmox.Utils.format_boolean,
-               width: 60,
-               dataIndex: 'active',
-           },
-           {
-               header: gettext('Type'),
-               width: 80,
-               hidden: true,
-               dataIndex: 'type',
-           },
-           {
-               header: gettext('Comment'),
-               flex: 2,
-               dataIndex: 'comments',
-           },
-       ],
-    },
-});
diff --git a/form/RRDTypeSelector.js b/form/RRDTypeSelector.js
deleted file mode 100644 (file)
index 16cac20..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-Ext.define('Proxmox.form.RRDTypeSelector', {
-    extend: 'Ext.form.field.ComboBox',
-    alias: ['widget.proxmoxRRDTypeSelector'],
-
-    displayField: 'text',
-    valueField: 'id',
-    editable: false,
-    queryMode: 'local',
-    value: 'hour',
-    stateEvents: ['select'],
-    stateful: true,
-    stateId: 'proxmoxRRDTypeSelection',
-    store: {
-       type: 'array',
-       fields: ['id', 'timeframe', 'cf', 'text'],
-       data: [
-           ['hour', 'hour', 'AVERAGE',
-             gettext('Hour') + ' (' + gettext('average') +')'],
-           ['hourmax', 'hour', 'MAX',
-             gettext('Hour') + ' (' + gettext('maximum') + ')'],
-           ['day', 'day', 'AVERAGE',
-             gettext('Day') + ' (' + gettext('average') + ')'],
-           ['daymax', 'day', 'MAX',
-             gettext('Day') + ' (' + gettext('maximum') + ')'],
-           ['week', 'week', 'AVERAGE',
-             gettext('Week') + ' (' + gettext('average') + ')'],
-           ['weekmax', 'week', 'MAX',
-             gettext('Week') + ' (' + gettext('maximum') + ')'],
-           ['month', 'month', 'AVERAGE',
-             gettext('Month') + ' (' + gettext('average') + ')'],
-           ['monthmax', 'month', 'MAX',
-             gettext('Month') + ' (' + gettext('maximum') + ')'],
-           ['year', 'year', 'AVERAGE',
-             gettext('Year') + ' (' + gettext('average') + ')'],
-           ['yearmax', 'year', 'MAX',
-             gettext('Year') + ' (' + gettext('maximum') + ')'],
-       ],
-    },
-    // save current selection in the state Provider so RRDView can read it
-    getState: function() {
-       let ind = this.getStore().findExact('id', this.getValue());
-       let rec = this.getStore().getAt(ind);
-       if (!rec) {
-           return undefined;
-       }
-       return {
-           id: rec.data.id,
-           timeframe: rec.data.timeframe,
-           cf: rec.data.cf,
-       };
-    },
-    // set selection based on last saved state
-    applyState: function(state) {
-       if (state && state.id) {
-           this.setValue(state.id);
-       }
-    },
-});
diff --git a/form/RealmComboBox.js b/form/RealmComboBox.js
deleted file mode 100644 (file)
index 309bf4f..0000000
+++ /dev/null
@@ -1,57 +0,0 @@
-Ext.define('Proxmox.form.RealmComboBox', {
-    extend: 'Ext.form.field.ComboBox',
-    alias: 'widget.pmxRealmComboBox',
-
-    controller: {
-       xclass: 'Ext.app.ViewController',
-
-       init: function(view) {
-           view.store.on('load', this.onLoad, view);
-       },
-
-       onLoad: function(store, records, success) {
-           if (!success) {
-               return;
-           }
-           let me = this;
-           let val = me.getValue();
-           if (!val || !me.store.findRecord('realm', val)) {
-               let def = 'pam';
-               Ext.each(records, function(rec) {
-                   if (rec.data && rec.data.default) {
-                       def = rec.data.realm;
-                   }
-               });
-               me.setValue(def);
-           }
-       },
-    },
-
-    fieldLabel: gettext('Realm'),
-    name: 'realm',
-    queryMode: 'local',
-    allowBlank: false,
-    editable: false,
-    forceSelection: true,
-    autoSelect: false,
-    triggerAction: 'all',
-    valueField: 'realm',
-    displayField: 'descr',
-    getState: function() {
-       return { value: this.getValue() };
-    },
-    applyState: function(state) {
-       if (state && state.value) {
-           this.setValue(state.value);
-       }
-    },
-    stateEvents: ['select'],
-    stateful: true, // last chosen auth realm is saved between page reloads
-    id: 'pveloginrealm', // We need stable ids when using stateful, not autogenerated
-    stateID: 'pveloginrealm',
-
-    store: {
-       model: 'pmx-domains',
-       autoLoad: true,
-    },
-});
diff --git a/form/RoleSelector.js b/form/RoleSelector.js
deleted file mode 100644 (file)
index 142cdfd..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-Ext.define('pmx-roles', {
-    extend: 'Ext.data.Model',
-    fields: ['roleid', 'privs'],
-    proxy: {
-       type: 'proxmox',
-       url: "/api2/json/access/roles",
-    },
-    idProperty: 'roleid',
-});
-
-Ext.define('Proxmox.form.RoleSelector', {
-    extend: 'Proxmox.form.ComboGrid',
-    alias: 'widget.pmxRoleSelector',
-
-    allowBlank: false,
-    autoSelect: false,
-    valueField: 'roleid',
-    displayField: 'roleid',
-
-    listConfig: {
-       columns: [
-           {
-               header: gettext('Role'),
-               sortable: true,
-               dataIndex: 'roleid',
-               flex: 1,
-           },
-           {
-               header: gettext('Privileges'),
-               dataIndex: 'privs',
-               flex: 1,
-           },
-       ],
-    },
-
-    store: {
-       autoLoad: true,
-       model: 'pmx-roles',
-       sorters: 'roleid',
-    },
-});
diff --git a/form/TextField.js b/form/TextField.js
deleted file mode 100644 (file)
index 56e5976..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-Ext.define('Proxmox.form.field.Textfield', {
-    extend: 'Ext.form.field.Text',
-    alias: ['widget.proxmoxtextfield'],
-
-    config: {
-       skipEmptyText: true,
-
-       deleteEmpty: false,
-    },
-
-    getSubmitData: function() {
-        let me = this,
-            data = null,
-            val;
-        if (!me.disabled && me.submitValue && !me.isFileUpload()) {
-            val = me.getSubmitValue();
-            if (val !== null) {
-                data = {};
-                data[me.getName()] = val;
-            } else if (me.getDeleteEmpty()) {
-               data = {};
-                data.delete = me.getName();
-           }
-        }
-        return data;
-    },
-
-    getSubmitValue: function() {
-       let me = this;
-
-        let value = this.processRawValue(this.getRawValue());
-       if (value !== '') {
-           return value;
-       }
-
-       return me.getSkipEmptyText() ? null: value;
-    },
-
-    setAllowBlank: function(allowBlank) {
-       this.allowBlank = allowBlank;
-       this.validate();
-    },
-});
diff --git a/grid/ObjectGrid.js b/grid/ObjectGrid.js
deleted file mode 100644 (file)
index 541cf1d..0000000
+++ /dev/null
@@ -1,326 +0,0 @@
-/* Renders a list of key values objets
-
-mandatory config parameters:
-rows: an object container where each propery is a key-value object we want to render
-       let rows = {
-           keyboard: {
-               header: gettext('Keyboard Layout'),
-               editor: 'Your.KeyboardEdit',
-               required: true
-           },
-
-optional:
-disabled: setting this parameter to true will disable selection and focus on the
-proxmoxObjectGrid as well as greying out input elements.
-Useful for a readonly tabular display
-
-*/
-
-Ext.define('Proxmox.grid.ObjectGrid', {
-    extend: 'Ext.grid.GridPanel',
-    alias: ['widget.proxmoxObjectGrid'],
-    disabled: false,
-    hideHeaders: true,
-
-    monStoreErrors: false,
-
-    add_combobox_row: function(name, text, opts) {
-       let me = this;
-
-       opts = opts || {};
-       me.rows = me.rows || {};
-
-       me.rows[name] = {
-           required: true,
-           defaultValue: opts.defaultValue,
-           header: text,
-           renderer: opts.renderer,
-           editor: {
-               xtype: 'proxmoxWindowEdit',
-               subject: text,
-               onlineHelp: opts.onlineHelp,
-               fieldDefaults: {
-                   labelWidth: opts.labelWidth || 100,
-               },
-               items: {
-                   xtype: 'proxmoxKVComboBox',
-                   name: name,
-                   comboItems: opts.comboItems,
-                   value: opts.defaultValue,
-                   deleteEmpty: !!opts.deleteEmpty,
-                   emptyText: opts.defaultValue,
-                   labelWidth: Proxmox.Utils.compute_min_label_width(
-                       text, opts.labelWidth),
-                   fieldLabel: text,
-               },
-           },
-       };
-    },
-
-    add_text_row: function(name, text, opts) {
-       let me = this;
-
-       opts = opts || {};
-       me.rows = me.rows || {};
-
-       me.rows[name] = {
-           required: true,
-           defaultValue: opts.defaultValue,
-           header: text,
-           renderer: opts.renderer,
-           editor: {
-               xtype: 'proxmoxWindowEdit',
-               subject: text,
-               onlineHelp: opts.onlineHelp,
-               fieldDefaults: {
-                   labelWidth: opts.labelWidth || 100,
-               },
-               items: {
-                   xtype: 'proxmoxtextfield',
-                   name: name,
-                   deleteEmpty: !!opts.deleteEmpty,
-                   emptyText: opts.defaultValue,
-                   labelWidth: Proxmox.Utils.compute_min_label_width(
-                       text, opts.labelWidth),
-                   vtype: opts.vtype,
-                   fieldLabel: text,
-               },
-           },
-       };
-    },
-
-    add_boolean_row: function(name, text, opts) {
-       let me = this;
-
-       opts = opts || {};
-       me.rows = me.rows || {};
-
-       me.rows[name] = {
-           required: true,
-           defaultValue: opts.defaultValue || 0,
-           header: text,
-           renderer: opts.renderer || Proxmox.Utils.format_boolean,
-           editor: {
-               xtype: 'proxmoxWindowEdit',
-               subject: text,
-               onlineHelp: opts.onlineHelp,
-               fieldDefaults: {
-                   labelWidth: opts.labelWidth || 100,
-               },
-               items: {
-                   xtype: 'proxmoxcheckbox',
-                   name: name,
-                   uncheckedValue: 0,
-                   defaultValue: opts.defaultValue || 0,
-                   checked: !!opts.defaultValue,
-                   deleteDefaultValue: !!opts.deleteDefaultValue,
-                   labelWidth: Proxmox.Utils.compute_min_label_width(
-                       text, opts.labelWidth),
-                   fieldLabel: text,
-               },
-           },
-       };
-    },
-
-    add_integer_row: function(name, text, opts) {
-       let me = this;
-
-       opts = opts || {};
-       me.rows = me.rows || {};
-
-       me.rows[name] = {
-           required: true,
-           defaultValue: opts.defaultValue,
-           header: text,
-           renderer: opts.renderer,
-           editor: {
-               xtype: 'proxmoxWindowEdit',
-               subject: text,
-               onlineHelp: opts.onlineHelp,
-               fieldDefaults: {
-                   labelWidth: opts.labelWidth || 100,
-               },
-               items: {
-                   xtype: 'proxmoxintegerfield',
-                   name: name,
-                   minValue: opts.minValue,
-                   maxValue: opts.maxValue,
-                   emptyText: gettext('Default'),
-                   deleteEmpty: !!opts.deleteEmpty,
-                   value: opts.defaultValue,
-                   labelWidth: Proxmox.Utils.compute_min_label_width(
-                       text, opts.labelWidth),
-                   fieldLabel: text,
-               },
-           },
-       };
-    },
-
-    editorConfig: {}, // default config passed to editor
-
-    run_editor: function() {
-       let me = this;
-
-       let sm = me.getSelectionModel();
-       let rec = sm.getSelection()[0];
-       if (!rec) {
-           return;
-       }
-
-       let rows = me.rows;
-       let rowdef = rows[rec.data.key];
-       if (!rowdef.editor) {
-           return;
-       }
-
-       let win;
-       let config;
-       if (Ext.isString(rowdef.editor)) {
-           config = Ext.apply({
-               confid: rec.data.key,
-           }, me.editorConfig);
-           win = Ext.create(rowdef.editor, config);
-       } else {
-           config = Ext.apply({
-               confid: rec.data.key,
-           }, me.editorConfig);
-           Ext.apply(config, rowdef.editor);
-           win = Ext.createWidget(rowdef.editor.xtype, config);
-           win.load();
-       }
-
-       win.show();
-       win.on('destroy', me.reload, me);
-    },
-
-    reload: function() {
-       let me = this;
-       me.rstore.load();
-    },
-
-    getObjectValue: function(key, defaultValue) {
-       let me = this;
-       let rec = me.store.getById(key);
-       if (rec) {
-           return rec.data.value;
-       }
-       return defaultValue;
-    },
-
-    renderKey: function(key, metaData, record, rowIndex, colIndex, store) {
-       let me = this;
-       let rows = me.rows;
-       let rowdef = rows && rows[key] ? rows[key] : {};
-       return rowdef.header || key;
-    },
-
-    renderValue: function(value, metaData, record, rowIndex, colIndex, store) {
-       let me = this;
-       let rows = me.rows;
-       let key = record.data.key;
-       let rowdef = rows && rows[key] ? rows[key] : {};
-
-       let renderer = rowdef.renderer;
-       if (renderer) {
-           return renderer(value, metaData, record, rowIndex, colIndex, store);
-       }
-
-       return value;
-    },
-
-    listeners: {
-       itemkeydown: function(view, record, item, index, e) {
-           if (e.getKey() === e.ENTER) {
-               this.pressedIndex = index;
-           }
-       },
-       itemkeyup: function(view, record, item, index, e) {
-           if (e.getKey() === e.ENTER && index === this.pressedIndex) {
-               this.run_editor();
-           }
-
-           this.pressedIndex = undefined;
-       },
-    },
-
-    initComponent: function() {
-       let me = this;
-
-       let rows = me.rows;
-
-       if (!me.rstore) {
-           if (!me.url) {
-               throw "no url specified";
-           }
-
-           me.rstore = Ext.create('Proxmox.data.ObjectStore', {
-               url: me.url,
-               interval: me.interval,
-               extraParams: me.extraParams,
-               rows: me.rows,
-           });
-       }
-
-       let rstore = me.rstore;
-       let store = Ext.create('Proxmox.data.DiffStore', {
-           rstore: rstore,
-           sorters: [],
-           filters: [],
-       });
-
-       if (rows) {
-           Ext.Object.each(rows, function(key, rowdef) {
-               if (Ext.isDefined(rowdef.defaultValue)) {
-                   store.add({ key: key, value: rowdef.defaultValue });
-               } else if (rowdef.required) {
-                   store.add({ key: key, value: undefined });
-               }
-           });
-       }
-
-       if (me.sorterFn) {
-           store.sorters.add(Ext.create('Ext.util.Sorter', {
-               sorterFn: me.sorterFn,
-           }));
-       }
-
-       store.filters.add(Ext.create('Ext.util.Filter', {
-           filterFn: function(item) {
-               if (rows) {
-                   let rowdef = rows[item.data.key];
-                   if (!rowdef || rowdef.visible === false) {
-                       return false;
-                   }
-               }
-               return true;
-           },
-       }));
-
-       Proxmox.Utils.monStoreErrors(me, rstore);
-
-       Ext.applyIf(me, {
-           store: store,
-           stateful: false,
-           columns: [
-               {
-                   header: gettext('Name'),
-                   width: me.cwidth1 || 200,
-                   dataIndex: 'key',
-                   renderer: me.renderKey,
-               },
-               {
-                   flex: 1,
-                   header: gettext('Value'),
-                   dataIndex: 'value',
-                   renderer: me.renderValue,
-               },
-           ],
-       });
-
-       me.callParent();
-
-       if (me.monStoreErrors) {
-           Proxmox.Utils.monStoreErrors(me, me.store);
-       }
-   },
-});
diff --git a/grid/PendingObjectGrid.js b/grid/PendingObjectGrid.js
deleted file mode 100644 (file)
index a5a640c..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
-Ext.define('Proxmox.grid.PendingObjectGrid', {
-    extend: 'Proxmox.grid.ObjectGrid',
-    alias: ['widget.proxmoxPendingObjectGrid'],
-
-    getObjectValue: function(key, defaultValue, pending) {
-       let me = this;
-       let rec = me.store.getById(key);
-       if (rec) {
-           let value = rec.data.value;
-           if (pending) {
-               if (Ext.isDefined(rec.data.pending) && rec.data.pending !== '') {
-                   value = rec.data.pending;
-               } else if (rec.data.delete === 1) {
-                   value = defaultValue;
-               }
-           }
-
-            if (Ext.isDefined(value) && value !== '') {
-               return value;
-            } else {
-               return defaultValue;
-            }
-       }
-       return defaultValue;
-    },
-
-    hasPendingChanges: function(key) {
-       let me = this;
-       let rows = me.rows;
-       let rowdef = rows && rows[key] ? rows[key] : {};
-       let keys = rowdef.multiKey || [key];
-       let pending = false;
-
-       Ext.Array.each(keys, function(k) {
-           let rec = me.store.getById(k);
-           if (rec && rec.data && (
-                   (Ext.isDefined(rec.data.pending) && rec.data.pending !== '') ||
-                   rec.data.delete === 1
-           )) {
-               pending = true;
-               return false; // break
-           }
-           return true;
-       });
-
-       return pending;
-    },
-
-    renderValue: function(value, metaData, record, rowIndex, colIndex, store) {
-       let me = this;
-       let rows = me.rows;
-       let key = record.data.key;
-       let rowdef = rows && rows[key] ? rows[key] : {};
-       let renderer = rowdef.renderer;
-       let current = '';
-       let pending = '';
-
-       if (renderer) {
-           current = renderer(value, metaData, record, rowIndex, colIndex, store, false);
-           if (me.hasPendingChanges(key)) {
-               pending = renderer(record.data.pending, metaData, record, rowIndex, colIndex, store, true);
-           }
-           if (pending === current) {
-               pending = undefined;
-           }
-       } else {
-           current = value || '';
-           pending = record.data.pending;
-       }
-
-       if (record.data.delete) {
-           let delete_all = true;
-           if (rowdef.multiKey) {
-               Ext.Array.each(rowdef.multiKey, function(k) {
-                   let rec = me.store.getById(k);
-                   if (rec && rec.data && rec.data.delete !== 1) {
-                       delete_all = false;
-                       return false; // break
-                   }
-                   return true;
-               });
-           }
-           if (delete_all) {
-               pending = '<div style="text-decoration: line-through;">'+ current +'</div>';
-           }
-       }
-
-       if (pending) {
-           return current + '<div style="color:darkorange">' + pending + '</div>';
-       } else {
-           return current;
-       }
-    },
-
-    initComponent: function() {
-       let me = this;
-
-       if (!me.rstore) {
-           if (!me.url) {
-               throw "no url specified";
-           }
-
-           me.rstore = Ext.create('Proxmox.data.ObjectStore', {
-               model: 'KeyValuePendingDelete',
-               readArray: true,
-               url: me.url,
-               interval: me.interval,
-               extraParams: me.extraParams,
-               rows: me.rows,
-           });
-       }
-
-       me.callParent();
-   },
-});
diff --git a/images/Makefile b/images/Makefile
deleted file mode 100644 (file)
index 45bc50a..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-include ../defines.mk
-
-IMAGES=pmx-clear-trigger.png
-
-all:
-
-.PHONY: install
-install: ${IMAGES}
-       install -d ${WWWIMAGESDIR}
-       for i in ${IMAGES}; do install -m 0755 $$i ${WWWIMAGESDIR}/$$i; done
-
-.PHONY: clean
-clean:
diff --git a/images/pmx-clear-trigger.png b/images/pmx-clear-trigger.png
deleted file mode 100644 (file)
index cc374cf..0000000
Binary files a/images/pmx-clear-trigger.png and /dev/null differ
diff --git a/mixin/CBind.js b/mixin/CBind.js
deleted file mode 100644 (file)
index afef53a..0000000
+++ /dev/null
@@ -1,161 +0,0 @@
-Ext.define('Proxmox.Mixin.CBind', {
-    extend: 'Ext.Mixin',
-
-    mixinConfig: {
-        before: {
-            initComponent: 'cloneTemplates',
-        },
-    },
-
-    cloneTemplates: function() {
-       let me = this;
-
-       if (typeof me.cbindData === "function") {
-           me.cbindData = me.cbindData(me.initialConfig);
-       }
-       me.cbindData = me.cbindData || {};
-
-       let getConfigValue = function(cname) {
-           if (cname in me.initialConfig) {
-               return me.initialConfig[cname];
-           }
-           if (cname in me.cbindData) {
-               let res = me.cbindData[cname];
-               if (typeof res === "function") {
-                   return res(me.initialConfig);
-               } else {
-                   return res;
-               }
-           }
-           if (cname in me) {
-               return me[cname];
-           }
-           throw "unable to get cbind data for '" + cname + "'";
-       };
-
-       let applyCBind = function(obj) {
-           let cbind = obj.cbind, cdata;
-           if (!cbind) return;
-
-           for (const prop in cbind) { // eslint-disable-line guard-for-in
-               let match, found;
-               cdata = cbind[prop];
-
-               found = false;
-               if (typeof cdata === 'function') {
-                   obj[prop] = cdata(getConfigValue, prop);
-                   found = true;
-               } else if ((match = /^\{(!)?([a-z_][a-z0-9_]*)\}$/i.exec(cdata))) {
-                   let cvalue = getConfigValue(match[2]);
-                   if (match[1]) cvalue = !cvalue;
-                   obj[prop] = cvalue;
-                   found = true;
-               } else if ((match = /^\{(!)?([a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)+)\}$/i.exec(cdata))) {
-                   let keys = match[2].split('.');
-                   let cvalue = getConfigValue(keys.shift());
-                   keys.forEach(function(k) {
-                       if (k in cvalue) {
-                           cvalue = cvalue[k];
-                       } else {
-                           throw "unable to get cbind data for '" + match[2] + "'";
-                       }
-                   });
-                   if (match[1]) cvalue = !cvalue;
-                   obj[prop] = cvalue;
-                   found = true;
-               } else {
-                   obj[prop] = cdata.replace(/{([a-z_][a-z0-9_]*)\}/ig, (_match, cname) => {
-                       let cvalue = getConfigValue(cname);
-                       found = true;
-                       return cvalue;
-                   });
-               }
-               if (!found) {
-                   throw "unable to parse cbind template '" + cdata + "'";
-               }
-           }
-       };
-
-       if (me.cbind) {
-           applyCBind(me);
-       }
-
-       let cloneTemplateObject;
-       let cloneTemplateArray = function(org) {
-           let copy, i, found, el, elcopy, arrayLength;
-
-           arrayLength = org.length;
-           found = false;
-           for (i = 0; i < arrayLength; i++) {
-               el = org[i];
-               if (el.constructor === Object && el.xtype) {
-                   found = true;
-                   break;
-               }
-           }
-
-           if (!found) return org; // no need to copy
-
-           copy = [];
-           for (i = 0; i < arrayLength; i++) {
-               el = org[i];
-               if (el.constructor === Object && el.xtype) {
-                   elcopy = cloneTemplateObject(el);
-                   if (elcopy.cbind) {
-                       applyCBind(elcopy);
-                   }
-                   copy.push(elcopy);
-               } else if (el.constructor === Array) {
-                   elcopy = cloneTemplateArray(el);
-                   copy.push(elcopy);
-               } else {
-                   copy.push(el);
-               }
-           }
-           return copy;
-       };
-
-       cloneTemplateObject = function(org) {
-           let res = {}, prop, el, copy;
-           for (prop in org) { // eslint-disable-line guard-for-in
-               el = org[prop];
-               if (el === undefined || el === null) {
-                   res[prop] = el;
-                   continue;
-               }
-               if (el.constructor === Object && el.xtype) {
-                   copy = cloneTemplateObject(el);
-                   if (copy.cbind) {
-                       applyCBind(copy);
-                   }
-                   res[prop] = copy;
-               } else if (el.constructor === Array) {
-                   copy = cloneTemplateArray(el);
-                   res[prop] = copy;
-               } else {
-                   res[prop] = el;
-               }
-           }
-           return res;
-       };
-
-       let condCloneProperties = function() {
-           let prop, el, tmp;
-
-           for (prop in me) { // eslint-disable-line guard-for-in
-               el = me[prop];
-               if (el === undefined || el === null) continue;
-               if (typeof el === 'object' && el.constructor === Object) {
-                   if (el.xtype && prop !== 'config') {
-                       me[prop] = cloneTemplateObject(el);
-                   }
-               } else if (el.constructor === Array) {
-                   tmp = cloneTemplateArray(el);
-                   me[prop] = tmp;
-               }
-           }
-       };
-
-       condCloneProperties();
-    },
-});
diff --git a/node/APT.js b/node/APT.js
deleted file mode 100644 (file)
index 1b564ef..0000000
+++ /dev/null
@@ -1,217 +0,0 @@
-Ext.define('apt-pkglist', {
-    extend: 'Ext.data.Model',
-    fields: ['Package', 'Title', 'Description', 'Section', 'Arch',
-             'Priority', 'Version', 'OldVersion', 'ChangeLogUrl', 'Origin'],
-    idProperty: 'Package',
-});
-
-Ext.define('Proxmox.node.APT', {
-    extend: 'Ext.grid.GridPanel',
-
-    xtype: 'proxmoxNodeAPT',
-
-    upgradeBtn: undefined,
-
-    columns: [
-       {
-           header: gettext('Package'),
-           width: 200,
-           sortable: true,
-           dataIndex: 'Package',
-       },
-       {
-           text: gettext('Version'),
-           columns: [
-               {
-                   header: gettext('current'),
-                   width: 100,
-                   sortable: false,
-                   dataIndex: 'OldVersion',
-               },
-               {
-                   header: gettext('new'),
-                   width: 100,
-                   sortable: false,
-                   dataIndex: 'Version',
-               },
-           ],
-       },
-       {
-           header: gettext('Description'),
-           sortable: false,
-           dataIndex: 'Title',
-           flex: 1,
-       },
-    ],
-
-    initComponent: function() {
-       let me = this;
-
-       if (!me.nodename) {
-           throw "no node name specified";
-       }
-
-       let store = Ext.create('Ext.data.Store', {
-           model: 'apt-pkglist',
-           groupField: 'Origin',
-           proxy: {
-               type: 'proxmox',
-               url: "/api2/json/nodes/" + me.nodename + "/apt/update",
-           },
-           sorters: [
-               {
-                   property: 'Package',
-                   direction: 'ASC',
-               },
-           ],
-       });
-
-       let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
-            groupHeaderTpl: '{[ "Origin: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})',
-           enableGroupingMenu: false,
-       });
-
-       let rowBodyFeature = Ext.create('Ext.grid.feature.RowBody', {
-            getAdditionalData: function(data, rowIndex, record, orig) {
-               let headerCt = this.view.headerCt;
-               let colspan = headerCt.getColumnCount();
-               return {
-                   rowBody: '<div style="padding: 1em">' +
-                       Ext.String.htmlEncode(data.Description) +
-                       '</div>',
-                   rowBodyCls: me.full_description ? '' : Ext.baseCSSPrefix + 'grid-row-body-hidden',
-                   rowBodyColspan: colspan,
-               };
-           },
-       });
-
-       let reload = function() {
-           store.load();
-       };
-
-       Proxmox.Utils.monStoreErrors(me, store, true);
-
-       let apt_command = function(cmd) {
-           Proxmox.Utils.API2Request({
-               url: "/nodes/" + me.nodename + "/apt/" + cmd,
-               method: 'POST',
-               failure: function(response, opts) {
-                   Ext.Msg.alert(gettext('Error'), response.htmlStatus);
-               },
-               success: function(response, opts) {
-                   let upid = response.result.data;
-
-                   let win = Ext.create('Proxmox.window.TaskViewer', {
-                       upid: upid,
-                   });
-                   win.show();
-                   me.mon(win, 'close', reload);
-               },
-           });
-       };
-
-       let sm = Ext.create('Ext.selection.RowModel', {});
-
-       let update_btn = new Ext.Button({
-           text: gettext('Refresh'),
-           handler: function() {
-               Proxmox.Utils.checked_command(function() { apt_command('update'); });
-           },
-       });
-
-       let show_changelog = function(rec) {
-           if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) {
-               return;
-           }
-
-           let view = Ext.createWidget('component', {
-               autoScroll: true,
-               style: {
-                   'background-color': 'white',
-                   'white-space': 'pre',
-                   'font-family': 'monospace',
-                   padding: '5px',
-               },
-           });
-
-           let win = Ext.create('Ext.window.Window', {
-               title: gettext('Changelog') + ": " + rec.data.Package,
-               width: 800,
-               height: 400,
-               layout: 'fit',
-               modal: true,
-               items: [view],
-           });
-
-           Proxmox.Utils.API2Request({
-               waitMsgTarget: me,
-               url: "/nodes/" + me.nodename + "/apt/changelog",
-               params: {
-                   name: rec.data.Package,
-                   version: rec.data.Version,
-               },
-               method: 'GET',
-               failure: function(response, opts) {
-                   win.close();
-                   Ext.Msg.alert(gettext('Error'), response.htmlStatus);
-               },
-               success: function(response, opts) {
-                   win.show();
-                   view.update(Ext.htmlEncode(response.result.data));
-               },
-           });
-       };
-
-       let changelog_btn = new Proxmox.button.Button({
-           text: gettext('Changelog'),
-           selModel: sm,
-           disabled: true,
-           enableFn: function(rec) {
-               if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) {
-                   return false;
-               }
-               return true;
-           },
-           handler: function(b, e, rec) {
-               show_changelog(rec);
-           },
-       });
-
-       let verbose_desc_checkbox = new Ext.form.field.Checkbox({
-           boxLabel: gettext('Show details'),
-           value: false,
-           listeners: {
-               change: (f, val) => {
-                   me.full_description = val;
-                   me.getView().refresh();
-               },
-           },
-       });
-
-       if (me.upgradeBtn) {
-           me.tbar = [update_btn, me.upgradeBtn, changelog_btn, '->', verbose_desc_checkbox];
-       } else {
-           me.tbar = [update_btn, changelog_btn, '->', verbose_desc_checkbox];
-       }
-
-       Ext.apply(me, {
-           store: store,
-           stateful: true,
-           stateId: 'grid-update',
-           selModel: sm,
-            viewConfig: {
-               stripeRows: false,
-               emptyText: '<div style="display:table; width:100%; height:100%;"><div style="display:table-cell; vertical-align: middle; text-align:center;"><b>' + gettext('No updates available.') + '</div></div>',
-           },
-           features: [groupingFeature, rowBodyFeature],
-           listeners: {
-               activate: reload,
-               itemdblclick: function(v, rec) {
-                   show_changelog(rec);
-               },
-           },
-       });
-
-       me.callParent();
-    },
-});
diff --git a/node/DNSEdit.js b/node/DNSEdit.js
deleted file mode 100644 (file)
index 0b1135e..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-Ext.define('Proxmox.node.DNSEdit', {
-    extend: 'Proxmox.window.Edit',
-    alias: ['widget.proxmoxNodeDNSEdit'],
-
-    initComponent: function() {
-       let me = this;
-
-       if (!me.nodename) {
-           throw "no node name specified";
-       }
-
-       me.items = [
-           {
-               xtype: 'textfield',
-                fieldLabel: gettext('Search domain'),
-                name: 'search',
-                allowBlank: false,
-           },
-           {
-               xtype: 'proxmoxtextfield',
-                fieldLabel: gettext('DNS server') + " 1",
-               vtype: 'IP64Address',
-               skipEmptyText: true,
-                name: 'dns1',
-           },
-           {
-               xtype: 'proxmoxtextfield',
-               fieldLabel: gettext('DNS server') + " 2",
-               vtype: 'IP64Address',
-               skipEmptyText: true,
-                name: 'dns2',
-           },
-           {
-               xtype: 'proxmoxtextfield',
-                fieldLabel: gettext('DNS server') + " 3",
-               vtype: 'IP64Address',
-               skipEmptyText: true,
-                name: 'dns3',
-           },
-       ];
-
-       Ext.applyIf(me, {
-           subject: gettext('DNS'),
-           url: "/api2/extjs/nodes/" + me.nodename + "/dns",
-           fieldDefaults: {
-               labelWidth: 120,
-           },
-       });
-
-       me.callParent();
-
-       me.load();
-    },
-});
diff --git a/node/DNSView.js b/node/DNSView.js
deleted file mode 100644 (file)
index e40e5a1..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-Ext.define('Proxmox.node.DNSView', {
-    extend: 'Proxmox.grid.ObjectGrid',
-    alias: ['widget.proxmoxNodeDNSView'],
-
-    initComponent: function() {
-       let me = this;
-
-       if (!me.nodename) {
-           throw "no node name specified";
-       }
-
-       let run_editor = function() {
-           let win = Ext.create('Proxmox.node.DNSEdit', {
-               nodename: me.nodename,
-           });
-           win.show();
-       };
-
-       Ext.apply(me, {
-           url: "/api2/json/nodes/" + me.nodename + "/dns",
-           cwidth1: 130,
-           interval: 1000,
-           run_editor: run_editor,
-           rows: {
-               search: {
-                   header: 'Search domain',
-                   required: true,
-                   renderer: Ext.htmlEncode,
-               },
-               dns1: {
-                   header: gettext('DNS server') + " 1",
-                   required: true,
-                   renderer: Ext.htmlEncode,
-               },
-               dns2: {
-                   header: gettext('DNS server') + " 2",
-                   renderer: Ext.htmlEncode,
-               },
-               dns3: {
-                   header: gettext('DNS server') + " 3",
-                   renderer: Ext.htmlEncode,
-               },
-           },
-           tbar: [
-               {
-                   text: gettext("Edit"),
-                   handler: run_editor,
-               },
-           ],
-           listeners: {
-               itemdblclick: run_editor,
-           },
-       });
-
-       me.callParent();
-
-       me.on('activate', me.rstore.startUpdate);
-       me.on('deactivate', me.rstore.stopUpdate);
-       me.on('destroy', me.rstore.stopUpdate);
-    },
-});
diff --git a/node/HostsView.js b/node/HostsView.js
deleted file mode 100644 (file)
index 9adb6b2..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-Ext.define('Proxmox.node.HostsView', {
-    extend: 'Ext.panel.Panel',
-    xtype: 'proxmoxNodeHostsView',
-
-    reload: function() {
-       let me = this;
-       me.store.load();
-    },
-
-    tbar: [
-       {
-           text: gettext('Save'),
-           disabled: true,
-           itemId: 'savebtn',
-           handler: function() {
-               let view = this.up('panel');
-               Proxmox.Utils.API2Request({
-                   params: {
-                       digest: view.digest,
-                       data: view.down('#hostsfield').getValue(),
-                   },
-                   method: 'POST',
-                   url: '/nodes/' + view.nodename + '/hosts',
-                   waitMsgTarget: view,
-                   success: function(response, opts) {
-                       view.reload();
-                   },
-                   failure: function(response, opts) {
-                       Ext.Msg.alert('Error', response.htmlStatus);
-                   },
-               });
-           },
-       },
-       {
-           text: gettext('Revert'),
-           disabled: true,
-           itemId: 'resetbtn',
-           handler: function() {
-               let view = this.up('panel');
-               view.down('#hostsfield').reset();
-           },
-       },
-    ],
-
-           layout: 'fit',
-
-    items: [
-       {
-           xtype: 'textarea',
-           itemId: 'hostsfield',
-           fieldStyle: {
-               'font-family': 'monospace',
-               'white-space': 'pre',
-           },
-           listeners: {
-               dirtychange: function(ta, dirty) {
-                   let view = this.up('panel');
-                   view.down('#savebtn').setDisabled(!dirty);
-                   view.down('#resetbtn').setDisabled(!dirty);
-               },
-           },
-       },
-    ],
-
-    initComponent: function() {
-       let me = this;
-
-       if (!me.nodename) {
-           throw "no node name specified";
-       }
-
-       me.store = Ext.create('Ext.data.Store', {
-           proxy: {
-               type: 'proxmox',
-               url: "/api2/json/nodes/" + me.nodename + "/hosts",
-           },
-       });
-
-       me.callParent();
-
-       Proxmox.Utils.monStoreErrors(me, me.store);
-
-       me.mon(me.store, 'load', function(store, records, success) {
-           if (!success || records.length < 1) {
-               return;
-           }
-           me.digest = records[0].data.digest;
-           let data = records[0].data.data;
-           me.down('#hostsfield').setValue(data);
-           me.down('#hostsfield').resetOriginalValue();
-       });
-
-       me.reload();
-    },
-});
diff --git a/node/NetworkEdit.js b/node/NetworkEdit.js
deleted file mode 100644 (file)
index 72aab6f..0000000
+++ /dev/null
@@ -1,362 +0,0 @@
-Ext.define('Proxmox.node.NetworkEdit', {
-    extend: 'Proxmox.window.Edit',
-    alias: ['widget.proxmoxNodeNetworkEdit'],
-
-    initComponent: function() {
-       let me = this;
-
-       if (!me.nodename) {
-           throw "no node name specified";
-       }
-
-       if (!me.iftype) {
-           throw "no network device type specified";
-       }
-
-       me.isCreate = !me.iface;
-
-       let iface_vtype;
-
-       if (me.iftype === 'bridge') {
-           iface_vtype = 'BridgeName';
-       } else if (me.iftype === 'bond') {
-           iface_vtype = 'BondName';
-       } else if (me.iftype === 'eth' && !me.isCreate) {
-           iface_vtype = 'InterfaceName';
-       } else if (me.iftype === 'vlan') {
-           iface_vtype = 'VlanName';
-       } else if (me.iftype === 'OVSBridge') {
-           iface_vtype = 'BridgeName';
-       } else if (me.iftype === 'OVSBond') {
-           iface_vtype = 'BondName';
-       } else if (me.iftype === 'OVSIntPort') {
-           iface_vtype = 'InterfaceName';
-       } else if (me.iftype === 'OVSPort') {
-           iface_vtype = 'InterfaceName';
-       } else {
-           console.log(me.iftype);
-           throw "unknown network device type specified";
-       }
-
-       me.subject = Proxmox.Utils.render_network_iface_type(me.iftype);
-
-       let column1 = [],
-           column2 = [],
-           columnB = [],
-           advancedColumn1 = [],
-           advancedColumn2 = [];
-
-       if (!(me.iftype === 'OVSIntPort' || me.iftype === 'OVSPort' || me.iftype === 'OVSBond')) {
-           column2.push({
-               xtype: 'proxmoxcheckbox',
-               fieldLabel: gettext('Autostart'),
-               name: 'autostart',
-               uncheckedValue: 0,
-               checked: me.isCreate ? true : undefined,
-           });
-       }
-
-       if (me.iftype === 'bridge') {
-           column2.push({
-               xtype: 'proxmoxcheckbox',
-               fieldLabel: gettext('VLAN aware'),
-               name: 'bridge_vlan_aware',
-               deleteEmpty: !me.isCreate,
-           });
-           column2.push({
-               xtype: 'textfield',
-               fieldLabel: gettext('Bridge ports'),
-               name: 'bridge_ports',
-           });
-       } else if (me.iftype === 'OVSBridge') {
-           column2.push({
-               xtype: 'textfield',
-               fieldLabel: gettext('Bridge ports'),
-               name: 'ovs_ports',
-           });
-           column2.push({
-               xtype: 'textfield',
-               fieldLabel: gettext('OVS options'),
-               name: 'ovs_options',
-           });
-       } else if (me.iftype === 'OVSPort' || me.iftype === 'OVSIntPort') {
-           column2.push({
-               xtype: me.isCreate ? 'PVE.form.BridgeSelector' : 'displayfield',
-               fieldLabel: Proxmox.Utils.render_network_iface_type('OVSBridge'),
-               allowBlank: false,
-               nodename: me.nodename,
-               bridgeType: 'OVSBridge',
-               name: 'ovs_bridge',
-           });
-           column2.push({
-               xtype: 'pveVlanField',
-               deleteEmpty: !me.isCreate,
-               name: 'ovs_tag',
-               value: '',
-           });
-           column2.push({
-               xtype: 'textfield',
-               fieldLabel: gettext('OVS options'),
-               name: 'ovs_options',
-           });
-       } else if (me.iftype === 'vlan') {
-           if (!me.isCreate) {
-               me.disablevlanid = false;
-               me.disablevlanrawdevice = false;
-               me.vlanrawdevicevalue = '';
-               me.vlanidvalue = '';
-
-               if (Proxmox.Utils.VlanInterface_match.test(me.iface)) {
-                  me.disablevlanid = true;
-                  me.disablevlanrawdevice = true;
-                  let arr = Proxmox.Utils.VlanInterface_match.exec(me.iface);
-                  me.vlanrawdevicevalue = arr[1];
-                  me.vlanidvalue = arr[2];
-               } else if (Proxmox.Utils.Vlan_match.test(me.iface)) {
-                  me.disablevlanid = true;
-                  let arr = Proxmox.Utils.Vlan_match.exec(me.iface);
-                  me.vlanidvalue = arr[1];
-               }
-           } else {
-               me.disablevlanid = true;
-               me.disablevlanrawdevice = true;
-           }
-
-           column2.push({
-               xtype: 'textfield',
-               fieldLabel: gettext('Vlan raw device'),
-               name: 'vlan-raw-device',
-               value: me.vlanrawdevicevalue,
-               disabled: me.disablevlanrawdevice,
-           });
-
-           column2.push({
-               xtype: 'pveVlanField',
-               name: 'vlan-id',
-               value: me.vlanidvalue,
-               disabled: me.disablevlanid,
-           });
-
-           columnB.push({
-               xtype: 'label',
-               userCls: 'pmx-hint',
-               text: 'Either add the VLAN number to an existing interface name, or choose your own name and set the VLAN raw device (for the latter ifupdown1 supports vlanXY naming only)',
-           });
-       } else if (me.iftype === 'bond') {
-           column2.push({
-               xtype: 'textfield',
-               fieldLabel: gettext('Slaves'),
-               name: 'slaves',
-           });
-
-           let policySelector = Ext.createWidget('bondPolicySelector', {
-               fieldLabel: gettext('Hash policy'),
-               name: 'bond_xmit_hash_policy',
-               deleteEmpty: !me.isCreate,
-               disabled: true,
-           });
-
-           let primaryfield = Ext.createWidget('textfield', {
-               fieldLabel: gettext('bond-primary'),
-               name: 'bond-primary',
-               value: '',
-               disabled: true,
-           });
-
-           column2.push({
-               xtype: 'bondModeSelector',
-               fieldLabel: gettext('Mode'),
-               name: 'bond_mode',
-               value: me.isCreate ? 'balance-rr' : undefined,
-               listeners: {
-                   change: function(f, value) {
-                       if (value === 'balance-xor' ||
-                           value === '802.3ad') {
-                           policySelector.setDisabled(false);
-                           primaryfield.setDisabled(true);
-                           primaryfield.setValue('');
-                       } else if (value === 'active-backup') {
-                           primaryfield.setDisabled(false);
-                           policySelector.setDisabled(true);
-                           policySelector.setValue('');
-                       } else {
-                           policySelector.setDisabled(true);
-                           policySelector.setValue('');
-                           primaryfield.setDisabled(true);
-                           primaryfield.setValue('');
-                       }
-                   },
-               },
-               allowBlank: false,
-           });
-
-           column2.push(policySelector);
-           column2.push(primaryfield);
-       } else if (me.iftype === 'OVSBond') {
-           column2.push({
-               xtype: me.isCreate ? 'PVE.form.BridgeSelector' : 'displayfield',
-               fieldLabel: Proxmox.Utils.render_network_iface_type('OVSBridge'),
-               allowBlank: false,
-               nodename: me.nodename,
-               bridgeType: 'OVSBridge',
-               name: 'ovs_bridge',
-           });
-           column2.push({
-               xtype: 'pveVlanField',
-               deleteEmpty: !me.isCreate,
-               name: 'ovs_tag',
-               value: '',
-           });
-           column2.push({
-               xtype: 'textfield',
-               fieldLabel: gettext('OVS options'),
-               name: 'ovs_options',
-           });
-       }
-
-       column2.push({
-           xtype: 'textfield',
-           fieldLabel: gettext('Comment'),
-           allowBlank: true,
-           nodename: me.nodename,
-           name: 'comments',
-       });
-
-       let url;
-       let method;
-
-       if (me.isCreate) {
-           url = "/api2/extjs/nodes/" + me.nodename + "/network";
-           method = 'POST';
-       } else {
-           url = "/api2/extjs/nodes/" + me.nodename + "/network/" + me.iface;
-           method = 'PUT';
-       }
-
-       column1.push({
-           xtype: 'hiddenfield',
-           name: 'type',
-           value: me.iftype,
-       },
-       {
-           xtype: me.isCreate ? 'textfield' : 'displayfield',
-           fieldLabel: gettext('Name'),
-           name: 'iface',
-           value: me.iface,
-           vtype: iface_vtype,
-           allowBlank: false,
-           listeners: {
-               change: function(f, value) {
-                   if (me.isCreate && iface_vtype === 'VlanName') {
-                       let vlanidField = me.down('field[name=vlan-id]');
-                       let vlanrawdeviceField = me.down('field[name=vlan-raw-device]');
-                       if (Proxmox.Utils.VlanInterface_match.test(value)) {
-                           vlanidField.setDisabled(true);
-                           vlanrawdeviceField.setDisabled(true);
-                       } else if (Proxmox.Utils.Vlan_match.test(value)) {
-                           vlanidField.setDisabled(true);
-                           vlanrawdeviceField.setDisabled(false);
-                       } else {
-                           vlanidField.setDisabled(false);
-                           vlanrawdeviceField.setDisabled(false);
-                       }
-                   }
-               },
-           },
-       });
-
-       if (me.iftype === 'OVSBond') {
-           column1.push(
-               {
-                   xtype: 'bondModeSelector',
-                   fieldLabel: gettext('Mode'),
-                   name: 'bond_mode',
-                   openvswitch: true,
-                   value: me.isCreate ? 'active-backup' : undefined,
-                   allowBlank: false,
-               },
-               {
-                   xtype: 'textfield',
-                   fieldLabel: gettext('Slaves'),
-                   name: 'ovs_bonds',
-               },
-           );
-       } else {
-           column1.push(
-               {
-                   xtype: 'proxmoxtextfield',
-                   deleteEmpty: !me.isCreate,
-                   fieldLabel: 'IPv4/CIDR',
-                   vtype: 'IPCIDRAddress',
-                   name: 'cidr',
-               },
-               {
-                   xtype: 'proxmoxtextfield',
-                   deleteEmpty: !me.isCreate,
-                   fieldLabel: gettext('Gateway') + ' (IPv4)',
-                   vtype: 'IPAddress',
-                   name: 'gateway',
-               },
-               {
-                   xtype: 'proxmoxtextfield',
-                   deleteEmpty: !me.isCreate,
-                   fieldLabel: 'IPv6/CIDR',
-                   vtype: 'IP6CIDRAddress',
-                   name: 'cidr6',
-               },
-               {
-                   xtype: 'proxmoxtextfield',
-                   deleteEmpty: !me.isCreate,
-                   fieldLabel: gettext('Gateway') + ' (IPv6)',
-                   vtype: 'IP6Address',
-                   name: 'gateway6',
-               },
-           );
-           advancedColumn1.push(
-               {
-                   xtype: 'proxmoxintegerfield',
-                   minValue: 1280,
-                   maxValue: 65520,
-                   deleteEmpty: !me.isCreate,
-                   emptyText: 1500,
-                   fieldLabel: 'MTU',
-                   name: 'mtu',
-               },
-           );
-       }
-
-       Ext.applyIf(me, {
-           url: url,
-           method: method,
-           items: {
-                xtype: 'inputpanel',
-               column1: column1,
-               column2: column2,
-               columnB: columnB,
-               advancedColumn1: advancedColumn1,
-               advancedColumn2: advancedColumn2,
-           },
-       });
-
-       me.callParent();
-
-       if (me.isCreate) {
-           me.down('field[name=iface]').setValue(me.iface_default);
-       } else {
-           me.load({
-               success: function(response, options) {
-                   let data = response.result.data;
-                   if (data.type !== me.iftype) {
-                       let msg = "Got unexpected device type";
-                       Ext.Msg.alert(gettext('Error'), msg, function() {
-                           me.close();
-                       });
-                       return;
-                   }
-                   me.setValues(data);
-                   me.isValid(); // trigger validation
-               },
-           });
-       }
-    },
-});
diff --git a/node/NetworkView.js b/node/NetworkView.js
deleted file mode 100644 (file)
index 886b8de..0000000
+++ /dev/null
@@ -1,465 +0,0 @@
-Ext.define('proxmox-networks', {
-    extend: 'Ext.data.Model',
-    fields: [
-       'active',
-       'address',
-       'address6',
-       'autostart',
-       'bridge_ports',
-       'cidr',
-       'cidr6',
-       'comments',
-       'gateway',
-       'gateway6',
-       'iface',
-       'netmask',
-       'netmask6',
-       'slaves',
-       'type',
-    ],
-    idProperty: 'iface',
-});
-
-Ext.define('Proxmox.node.NetworkView', {
-    extend: 'Ext.panel.Panel',
-
-    alias: ['widget.proxmoxNodeNetworkView'],
-
-    // defines what types of network devices we want to create
-    // order is always the same
-    types: ['bridge', 'bond', 'vlan', 'ovs'],
-
-    showApplyBtn: false,
-
-    initComponent: function() {
-       let me = this;
-
-       if (!me.nodename) {
-           throw "no node name specified";
-       }
-
-       let baseUrl = '/nodes/' + me.nodename + '/network';
-
-       let store = Ext.create('Ext.data.Store', {
-           model: 'proxmox-networks',
-           proxy: {
-                type: 'proxmox',
-                url: '/api2/json' + baseUrl,
-           },
-           sorters: [
-               {
-                   property: 'iface',
-                   direction: 'ASC',
-               },
-           ],
-       });
-
-       let reload = function() {
-           let changeitem = me.down('#changes');
-           let apply_btn = me.down('#apply');
-           let revert_btn = me.down('#revert');
-           Proxmox.Utils.API2Request({
-               url: baseUrl,
-               failure: function(response, opts) {
-                   store.loadData({});
-                   Proxmox.Utils.setErrorMask(me, response.htmlStatus);
-                   changeitem.update('');
-                   changeitem.setHidden(true);
-               },
-               success: function(response, opts) {
-                   let result = Ext.decode(response.responseText);
-                   store.loadData(result.data);
-                   let changes = result.changes;
-                   if (changes === undefined || changes === '') {
-                       changes = gettext("No changes");
-                       changeitem.setHidden(true);
-                       apply_btn.setDisabled(true);
-                       revert_btn.setDisabled(true);
-                   } else {
-                       changeitem.update("<pre>" + Ext.htmlEncode(changes) + "</pre>");
-                       changeitem.setHidden(false);
-                       apply_btn.setDisabled(false);
-                       revert_btn.setDisabled(false);
-                   }
-               },
-           });
-       };
-
-       let run_editor = function() {
-           let grid = me.down('gridpanel');
-           let sm = grid.getSelectionModel();
-           let rec = sm.getSelection()[0];
-           if (!rec) {
-               return;
-           }
-
-           let win = Ext.create('Proxmox.node.NetworkEdit', {
-               nodename: me.nodename,
-               iface: rec.data.iface,
-               iftype: rec.data.type,
-           });
-           win.show();
-           win.on('destroy', reload);
-       };
-
-       let edit_btn = new Ext.Button({
-           text: gettext('Edit'),
-           disabled: true,
-           handler: run_editor,
-       });
-
-       let del_btn = new Ext.Button({
-           text: gettext('Remove'),
-           disabled: true,
-           handler: function() {
-               let grid = me.down('gridpanel');
-               let sm = grid.getSelectionModel();
-               let rec = sm.getSelection()[0];
-               if (!rec) {
-                   return;
-               }
-
-               let iface = rec.data.iface;
-
-               Proxmox.Utils.API2Request({
-                   url: baseUrl + '/' + iface,
-                   method: 'DELETE',
-                   waitMsgTarget: me,
-                   callback: function() {
-                       reload();
-                   },
-                   failure: function(response, opts) {
-                       Ext.Msg.alert(gettext('Error'), response.htmlStatus);
-                   },
-               });
-           },
-       });
-
-       let apply_btn = Ext.create('Proxmox.button.Button', {
-           text: gettext('Apply Configuration'),
-           itemId: 'apply',
-           disabled: true,
-           confirmMsg: 'Do you want to apply pending network changes?',
-           hidden: !me.showApplyBtn,
-           handler: function() {
-               Proxmox.Utils.API2Request({
-                   url: baseUrl,
-                   method: 'PUT',
-                   waitMsgTarget: me,
-                   success: function(response, opts) {
-                       let upid = response.result.data;
-
-                       let win = Ext.create('Proxmox.window.TaskProgress', {
-                           taskDone: reload,
-                           upid: upid,
-                       });
-                       win.show();
-                   },
-                   failure: function(response, opts) {
-                       Ext.Msg.alert(gettext('Error'), response.htmlStatus);
-                   },
-               });
-           },
-       });
-
-       let set_button_status = function() {
-           let grid = me.down('gridpanel');
-           let sm = grid.getSelectionModel();
-           let rec = sm.getSelection()[0];
-
-           edit_btn.setDisabled(!rec);
-           del_btn.setDisabled(!rec);
-       };
-
-       let render_ports = function(value, metaData, record) {
-           if (value === 'bridge') {
-               return record.data.bridge_ports;
-           } else if (value === 'bond') {
-               return record.data.slaves;
-           } else if (value === 'OVSBridge') {
-               return record.data.ovs_ports;
-           } else if (value === 'OVSBond') {
-               return record.data.ovs_bonds;
-           }
-           return '';
-       };
-
-       let find_next_iface_id = function(prefix) {
-           let next;
-           for (next = 0; next <= 9999; next++) {
-               if (!store.getById(prefix + next.toString())) {
-                   break;
-               }
-           }
-           return prefix + next.toString();
-       };
-
-       let menu_items = [];
-
-       if (me.types.indexOf('bridge') !== -1) {
-           menu_items.push({
-               text: Proxmox.Utils.render_network_iface_type('bridge'),
-               handler: function() {
-                   let win = Ext.create('Proxmox.node.NetworkEdit', {
-                       nodename: me.nodename,
-                       iftype: 'bridge',
-                       iface_default: find_next_iface_id('vmbr'),
-                       onlineHelp: 'sysadmin_network_configuration',
-                   });
-                   win.on('destroy', reload);
-                   win.show();
-               },
-           });
-       }
-
-       if (me.types.indexOf('bond') !== -1) {
-           menu_items.push({
-               text: Proxmox.Utils.render_network_iface_type('bond'),
-               handler: function() {
-                   let win = Ext.create('Proxmox.node.NetworkEdit', {
-                       nodename: me.nodename,
-                       iftype: 'bond',
-                       iface_default: find_next_iface_id('bond'),
-                       onlineHelp: 'sysadmin_network_configuration',
-                   });
-                   win.on('destroy', reload);
-                   win.show();
-               },
-           });
-       }
-
-       if (me.types.indexOf('vlan') !== -1) {
-           menu_items.push({
-               text: Proxmox.Utils.render_network_iface_type('vlan'),
-               handler: function() {
-                   let win = Ext.create('Proxmox.node.NetworkEdit', {
-                       nodename: me.nodename,
-                       iftype: 'vlan',
-                       iface_default: 'interfaceX.1',
-                       onlineHelp: 'sysadmin_network_configuration',
-                   });
-                   win.on('destroy', reload);
-                   win.show();
-               },
-           });
-       }
-
-       if (me.types.indexOf('ovs') !== -1) {
-           if (menu_items.length > 0) {
-               menu_items.push({ xtype: 'menuseparator' });
-           }
-
-           menu_items.push(
-               {
-                   text: Proxmox.Utils.render_network_iface_type('OVSBridge'),
-                   handler: function() {
-                       let win = Ext.create('Proxmox.node.NetworkEdit', {
-                           nodename: me.nodename,
-                           iftype: 'OVSBridge',
-                           iface_default: find_next_iface_id('vmbr'),
-                       });
-                       win.on('destroy', reload);
-                       win.show();
-                   },
-               },
-               {
-                   text: Proxmox.Utils.render_network_iface_type('OVSBond'),
-                   handler: function() {
-                       let win = Ext.create('Proxmox.node.NetworkEdit', {
-                           nodename: me.nodename,
-                           iftype: 'OVSBond',
-                           iface_default: find_next_iface_id('bond'),
-                       });
-                       win.on('destroy', reload);
-                       win.show();
-                   },
-               },
-               {
-                   text: Proxmox.Utils.render_network_iface_type('OVSIntPort'),
-                   handler: function() {
-                       let win = Ext.create('Proxmox.node.NetworkEdit', {
-                           nodename: me.nodename,
-                           iftype: 'OVSIntPort',
-                       });
-                       win.on('destroy', reload);
-                       win.show();
-                   },
-               },
-           );
-       }
-
-       let renderer_generator = function(fieldname) {
-           return function(val, metaData, rec) {
-               let tmp = [];
-               if (rec.data[fieldname]) {
-                   tmp.push(rec.data[fieldname]);
-               }
-               if (rec.data[fieldname + '6']) {
-                   tmp.push(rec.data[fieldname + '6']);
-               }
-               return tmp.join('<br>') || '';
-           };
-       };
-
-       Ext.apply(me, {
-           layout: 'border',
-           tbar: [
-               {
-                   text: gettext('Create'),
-                   menu: {
-                       plain: true,
-                       items: menu_items,
-                   },
-               }, '-',
-               {
-                   text: gettext('Revert'),
-                   itemId: 'revert',
-                   handler: function() {
-                       Proxmox.Utils.API2Request({
-                           url: baseUrl,
-                           method: 'DELETE',
-                           waitMsgTarget: me,
-                           callback: function() {
-                               reload();
-                           },
-                           failure: function(response, opts) {
-                               Ext.Msg.alert(gettext('Error'), response.htmlStatus);
-                           },
-                       });
-                   },
-               },
-               edit_btn,
-               del_btn,
-               '-',
-               apply_btn,
-           ],
-           items: [
-               {
-                   xtype: 'gridpanel',
-                   stateful: true,
-                   stateId: 'grid-node-network',
-                   store: store,
-                   region: 'center',
-                   border: false,
-                   columns: [
-                       {
-                           header: gettext('Name'),
-                           sortable: true,
-                           dataIndex: 'iface',
-                       },
-                       {
-                           header: gettext('Type'),
-                           sortable: true,
-                           width: 120,
-                           renderer: Proxmox.Utils.render_network_iface_type,
-                           dataIndex: 'type',
-                       },
-                       {
-                           xtype: 'booleancolumn',
-                           header: gettext('Active'),
-                           width: 80,
-                           sortable: true,
-                           dataIndex: 'active',
-                           trueText: Proxmox.Utils.yesText,
-                           falseText: Proxmox.Utils.noText,
-                           undefinedText: Proxmox.Utils.noText,
-                       },
-                       {
-                           xtype: 'booleancolumn',
-                           header: gettext('Autostart'),
-                           width: 80,
-                           sortable: true,
-                           dataIndex: 'autostart',
-                           trueText: Proxmox.Utils.yesText,
-                           falseText: Proxmox.Utils.noText,
-                           undefinedText: Proxmox.Utils.noText,
-                       },
-                       {
-                           xtype: 'booleancolumn',
-                           header: gettext('VLAN aware'),
-                           width: 80,
-                           sortable: true,
-                           dataIndex: 'bridge_vlan_aware',
-                           trueText: Proxmox.Utils.yesText,
-                           falseText: Proxmox.Utils.noText,
-                           undefinedText: Proxmox.Utils.noText,
-                       },
-                       {
-                           header: gettext('Ports/Slaves'),
-                           dataIndex: 'type',
-                           renderer: render_ports,
-                       },
-                       {
-                           header: gettext('Bond Mode'),
-                           dataIndex: 'bond_mode',
-                           renderer: Proxmox.Utils.render_bond_mode,
-                       },
-                       {
-                           header: gettext('Hash Policy'),
-                           hidden: true,
-                           dataIndex: 'bond_xmit_hash_policy',
-                       },
-                       {
-                           header: gettext('IP address'),
-                           sortable: true,
-                           width: 120,
-                           hidden: true,
-                           dataIndex: 'address',
-                           renderer: renderer_generator('address'),
-                       },
-                       {
-                           header: gettext('Subnet mask'),
-                           width: 120,
-                           sortable: true,
-                           hidden: true,
-                           dataIndex: 'netmask',
-                           renderer: renderer_generator('netmask'),
-                       },
-                       {
-                           header: gettext('CIDR'),
-                           width: 150,
-                           sortable: true,
-                           dataIndex: 'cidr',
-                           renderer: renderer_generator('cidr'),
-                       },
-                       {
-                           header: gettext('Gateway'),
-                           width: 150,
-                           sortable: true,
-                           dataIndex: 'gateway',
-                           renderer: renderer_generator('gateway'),
-                       },
-                       {
-                           header: gettext('Comment'),
-                           dataIndex: 'comments',
-                           flex: 1,
-                           renderer: Ext.String.htmlEncode,
-                       },
-                   ],
-                   listeners: {
-                       selectionchange: set_button_status,
-                       itemdblclick: run_editor,
-                   },
-               },
-               {
-                   border: false,
-                   region: 'south',
-                   autoScroll: true,
-                   hidden: true,
-                   itemId: 'changes',
-                   tbar: [
-                       gettext('Pending changes') + ' (' +
-                           gettext("Either reboot or use 'Apply Configuration' (needs ifupdown2) to activate") + ')',
-                   ],
-                   split: true,
-                   bodyPadding: 5,
-                   flex: 0.6,
-                   html: gettext("No changes"),
-               },
-           ],
-       });
-
-       me.callParent();
-       reload();
-    },
-});
diff --git a/node/ServiceView.js b/node/ServiceView.js
deleted file mode 100644 (file)
index 88026a4..0000000
+++ /dev/null
@@ -1,183 +0,0 @@
-Ext.define('proxmox-services', {
-    extend: 'Ext.data.Model',
-    fields: ['service', 'name', 'desc', 'state'],
-    idProperty: 'service',
-});
-
-Ext.define('Proxmox.node.ServiceView', {
-    extend: 'Ext.grid.GridPanel',
-
-    alias: ['widget.proxmoxNodeServiceView'],
-
-    startOnlyServices: {},
-
-    initComponent: function() {
-       let me = this;
-
-       if (!me.nodename) {
-           throw "no node name specified";
-       }
-
-       let rstore = Ext.create('Proxmox.data.UpdateStore', {
-           interval: 1000,
-           storeid: 'proxmox-services' + me.nodename,
-           model: 'proxmox-services',
-           proxy: {
-                type: 'proxmox',
-                url: "/api2/json/nodes/" + me.nodename + "/services",
-           },
-       });
-
-       let store = Ext.create('Proxmox.data.DiffStore', {
-           rstore: rstore,
-           sortAfterUpdate: true,
-           sorters: [
-               {
-                   property: 'name',
-                   direction: 'ASC',
-               },
-           ],
-       });
-
-       let view_service_log = function() {
-           let sm = me.getSelectionModel();
-           let rec = sm.getSelection()[0];
-           let win = Ext.create('Ext.window.Window', {
-               title: gettext('Syslog') + ': ' + rec.data.service,
-               modal: true,
-               width: 800,
-               height: 400,
-               layout: 'fit',
-               items: {
-                   xtype: 'proxmoxLogView',
-                   url: "/api2/extjs/nodes/" + me.nodename + "/syslog?service=" +
-                       rec.data.service,
-                   log_select_timespan: 1,
-               },
-           });
-           win.show();
-       };
-
-       let service_cmd = function(cmd) {
-           let sm = me.getSelectionModel();
-           let rec = sm.getSelection()[0];
-           Proxmox.Utils.API2Request({
-               url: "/nodes/" + me.nodename + "/services/" + rec.data.service + "/" + cmd,
-               method: 'POST',
-               failure: function(response, opts) {
-                   Ext.Msg.alert(gettext('Error'), response.htmlStatus);
-                   me.loading = true;
-               },
-               success: function(response, opts) {
-                   rstore.startUpdate();
-                   let upid = response.result.data;
-
-                   let win = Ext.create('Proxmox.window.TaskProgress', {
-                       upid: upid,
-                   });
-                   win.show();
-               },
-           });
-       };
-
-       let start_btn = new Ext.Button({
-           text: gettext('Start'),
-           disabled: true,
-           handler: function() {
-               service_cmd("start");
-           },
-       });
-
-       let stop_btn = new Ext.Button({
-           text: gettext('Stop'),
-           disabled: true,
-           handler: function() {
-               service_cmd("stop");
-           },
-       });
-
-       let restart_btn = new Ext.Button({
-           text: gettext('Restart'),
-           disabled: true,
-           handler: function() {
-               service_cmd("restart");
-           },
-       });
-
-       let syslog_btn = new Ext.Button({
-           text: gettext('Syslog'),
-           disabled: true,
-           handler: view_service_log,
-       });
-
-       let set_button_status = function() {
-           let sm = me.getSelectionModel();
-           let rec = sm.getSelection()[0];
-
-           if (!rec) {
-               start_btn.disable();
-               stop_btn.disable();
-               restart_btn.disable();
-               syslog_btn.disable();
-               return;
-           }
-           let service = rec.data.service;
-           let state = rec.data.state;
-
-           syslog_btn.enable();
-
-           if (state === 'running') {
-               start_btn.disable();
-               restart_btn.enable();
-           } else {
-               start_btn.enable();
-               restart_btn.disable();
-           }
-           if (!me.startOnlyServices[service]) {
-               if (state === 'running') {
-                   stop_btn.enable();
-               } else {
-                   stop_btn.disable();
-               }
-           }
-       };
-
-       me.mon(store, 'refresh', set_button_status);
-
-       Proxmox.Utils.monStoreErrors(me, rstore);
-
-       Ext.apply(me, {
-           store: store,
-           stateful: false,
-           tbar: [start_btn, stop_btn, restart_btn, syslog_btn],
-           columns: [
-               {
-                   header: gettext('Name'),
-                   flex: 1,
-                   sortable: true,
-                   dataIndex: 'name',
-               },
-               {
-                   header: gettext('Status'),
-                   width: 100,
-                   sortable: true,
-                   dataIndex: 'state',
-               },
-               {
-                   header: gettext('Description'),
-                   renderer: Ext.String.htmlEncode,
-                   dataIndex: 'desc',
-                   flex: 2,
-               },
-           ],
-           listeners: {
-               selectionchange: set_button_status,
-               itemdblclick: view_service_log,
-               activate: rstore.startUpdate,
-               destroy: rstore.stopUpdate,
-           },
-       });
-
-       me.callParent();
-    },
-});
diff --git a/node/Tasks.js b/node/Tasks.js
deleted file mode 100644 (file)
index 3d9267e..0000000
+++ /dev/null
@@ -1,188 +0,0 @@
-Ext.define('Proxmox.node.Tasks', {
-    extend: 'Ext.grid.GridPanel',
-
-    alias: ['widget.proxmoxNodeTasks'],
-    stateful: true,
-    stateId: 'grid-node-tasks',
-    loadMask: true,
-    sortableColumns: false,
-    vmidFilter: 0,
-
-    initComponent: function() {
-       let me = this;
-
-       if (!me.nodename) {
-           throw "no node name specified";
-       }
-
-       let store = Ext.create('Ext.data.BufferedStore', {
-           pageSize: 500,
-           autoLoad: true,
-           remoteFilter: true,
-           model: 'proxmox-tasks',
-           proxy: {
-                type: 'proxmox',
-               startParam: 'start',
-               limitParam: 'limit',
-                url: "/api2/json/nodes/" + me.nodename + "/tasks",
-           },
-       });
-
-       let userfilter = '';
-       let filter_errors = 0;
-
-       let updateProxyParams = function() {
-           let params = {
-               errors: filter_errors,
-           };
-           if (userfilter) {
-               params.userfilter = userfilter;
-           }
-           if (me.vmidFilter) {
-               params.vmid = me.vmidFilter;
-           }
-           store.proxy.extraParams = params;
-       };
-
-       updateProxyParams();
-
-       let reload_task = Ext.create('Ext.util.DelayedTask', function() {
-           updateProxyParams();
-           store.reload();
-       });
-
-       let run_task_viewer = function() {
-           let sm = me.getSelectionModel();
-           let rec = sm.getSelection()[0];
-           if (!rec) {
-               return;
-           }
-
-           let win = Ext.create('Proxmox.window.TaskViewer', {
-               upid: rec.data.upid,
-           });
-           win.show();
-       };
-
-       let view_btn = new Ext.Button({
-           text: gettext('View'),
-           disabled: true,
-           handler: run_task_viewer,
-       });
-
-       Proxmox.Utils.monStoreErrors(me, store, true);
-
-       Ext.apply(me, {
-           store: store,
-           viewConfig: {
-               trackOver: false,
-               stripeRows: false, // does not work with getRowClass()
-
-               getRowClass: function(record, index) {
-                   let status = record.get('status');
-
-                   if (status && status !== 'OK') {
-                       return "proxmox-invalid-row";
-                   }
-                   return '';
-               },
-           },
-           tbar: [
-               view_btn,
-               {
-                   text: gettext('Refresh'), // FIXME: smart-auto-refresh store
-                   handler: () => store.reload(),
-               },
-               '->',
-               gettext('User name') +':',
-               ' ',
-               {
-                   xtype: 'textfield',
-                   width: 200,
-                   value: userfilter,
-                   enableKeyEvents: true,
-                   listeners: {
-                       keyup: function(field, e) {
-                           userfilter = field.getValue();
-                           reload_task.delay(500);
-                       },
-                   },
-               }, ' ', gettext('Only Errors') + ':', ' ',
-               {
-                   xtype: 'checkbox',
-                   hideLabel: true,
-                   checked: filter_errors,
-                   listeners: {
-                       change: function(field, checked) {
-                           filter_errors = checked ? 1 : 0;
-                           reload_task.delay(10);
-                       },
-                   },
-               }, ' ',
-           ],
-           columns: [
-               {
-                   header: gettext("Start Time"),
-                   dataIndex: 'starttime',
-                   width: 130,
-                   renderer: function(value) {
-                       return Ext.Date.format(value, "M d H:i:s");
-                   },
-               },
-               {
-                   header: gettext("End Time"),
-                   dataIndex: 'endtime',
-                   width: 130,
-                   renderer: function(value, metaData, record) {
-                       if (!value) {
-                           metaData.tdCls = "x-grid-row-loading";
-                           return '';
-                       }
-                       return Ext.Date.format(value, "M d H:i:s");
-                   },
-               },
-               {
-                   header: gettext("Node"),
-                   dataIndex: 'node',
-                   width: 120,
-               },
-               {
-                   header: gettext("User name"),
-                   dataIndex: 'user',
-                   width: 150,
-               },
-               {
-                   header: gettext("Description"),
-                   dataIndex: 'upid',
-                   flex: 1,
-                   renderer: Proxmox.Utils.render_upid,
-               },
-               {
-                   header: gettext("Status"),
-                   dataIndex: 'status',
-                   width: 200,
-                   renderer: function(value, metaData, record) {
-                       if (value === 'OK') {
-                           return 'OK';
-                       }
-                       if (value === undefined && !record.data.endtime) {
-                           metaData.tdCls = "x-grid-row-loading";
-                           return '';
-                       }
-                       return "ERROR: " + value;
-                   },
-               },
-           ],
-           listeners: {
-               itemdblclick: run_task_viewer,
-               selectionchange: function(v, selections) {
-                   view_btn.setDisabled(!(selections && selections[0]));
-               },
-               show: function() { reload_task.delay(10); },
-               destroy: function() { reload_task.cancel(); },
-           },
-       });
-
-       me.callParent();
-    },
-});
diff --git a/node/TimeEdit.js b/node/TimeEdit.js
deleted file mode 100644 (file)
index 613a2f5..0000000
+++ /dev/null
@@ -1,38 +0,0 @@
-Ext.define('Proxmox.node.TimeEdit', {
-    extend: 'Proxmox.window.Edit',
-    alias: ['widget.proxmoxNodeTimeEdit'],
-
-    subject: gettext('Time zone'),
-
-    width: 400,
-
-    autoLoad: true,
-
-    fieldDefaults: {
-       labelWidth: 70,
-    },
-
-    items: {
-       xtype: 'combo',
-       fieldLabel: gettext('Time zone'),
-       name: 'timezone',
-       queryMode: 'local',
-       store: Ext.create('Proxmox.data.TimezoneStore'),
-       displayField: 'zone',
-       editable: true,
-       anyMatch: true,
-       forceSelection: true,
-       allowBlank: false,
-    },
-
-    initComponent: function() {
-       let me = this;
-
-       if (!me.nodename) {
-           throw "no node name specified";
-       }
-       me.url = "/api2/extjs/nodes/" + me.nodename + "/time";
-
-       me.callParent();
-    },
-});
diff --git a/node/TimeView.js b/node/TimeView.js
deleted file mode 100644 (file)
index 9033d13..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-Ext.define('Proxmox.node.TimeView', {
-    extend: 'Proxmox.grid.ObjectGrid',
-    alias: ['widget.proxmoxNodeTimeView'],
-
-    initComponent: function() {
-       let me = this;
-
-       if (!me.nodename) {
-           throw "no node name specified";
-       }
-
-       let tzoffset = new Date().getTimezoneOffset()*60000;
-       let renderlocaltime = function(value) {
-           let servertime = new Date((value * 1000) + tzoffset);
-           return Ext.Date.format(servertime, 'Y-m-d H:i:s');
-       };
-
-       let run_editor = function() {
-           let win = Ext.create('Proxmox.node.TimeEdit', {
-               nodename: me.nodename,
-           });
-           win.show();
-       };
-
-       Ext.apply(me, {
-           url: "/api2/json/nodes/" + me.nodename + "/time",
-           cwidth1: 150,
-           interval: 1000,
-           run_editor: run_editor,
-           rows: {
-               timezone: {
-                   header: gettext('Time zone'),
-                   required: true,
-               },
-               localtime: {
-                   header: gettext('Server time'),
-                   required: true,
-                   renderer: renderlocaltime,
-               },
-           },
-           tbar: [
-               {
-                   text: gettext("Edit"),
-                   handler: run_editor,
-               },
-           ],
-           listeners: {
-               itemdblclick: run_editor,
-           },
-       });
-
-       me.callParent();
-
-       me.on('activate', me.rstore.startUpdate);
-       me.on('deactivate', me.rstore.stopUpdate);
-       me.on('destroy', me.rstore.stopUpdate);
-    },
-});
diff --git a/panel/GaugeWidget.js b/panel/GaugeWidget.js
deleted file mode 100644 (file)
index 6cd6b60..0000000
+++ /dev/null
@@ -1,102 +0,0 @@
-Ext.define('Proxmox.panel.GaugeWidget', {
-    extend: 'Ext.panel.Panel',
-    alias: 'widget.proxmoxGauge',
-
-    defaults: {
-       style: {
-           'text-align': 'center',
-       },
-    },
-    items: [
-       {
-           xtype: 'box',
-           itemId: 'title',
-           data: {
-               title: '',
-           },
-           tpl: '<h3>{title}</h3>',
-       },
-       {
-           xtype: 'polar',
-           height: 120,
-           border: false,
-           itemId: 'chart',
-           series: [{
-               type: 'gauge',
-               value: 0,
-               colors: ['#f5f5f5'],
-               sectors: [0],
-               donut: 90,
-               needleLength: 100,
-               totalAngle: Math.PI,
-           }],
-           sprites: [{
-               id: 'valueSprite',
-               type: 'text',
-               text: '',
-               textAlign: 'center',
-               textBaseline: 'bottom',
-               x: 125,
-               y: 110,
-               fontSize: 30,
-           }],
-       },
-       {
-           xtype: 'box',
-           itemId: 'text',
-       },
-    ],
-
-    header: false,
-    border: false,
-
-    warningThreshold: 0.6,
-    criticalThreshold: 0.9,
-    warningColor: '#fc0',
-    criticalColor: '#FF6C59',
-    defaultColor: '#c2ddf2',
-    backgroundColor: '#f5f5f5',
-
-    initialValue: 0,
-
-
-    updateValue: function(value, text) {
-       let me = this;
-       let color = me.defaultColor;
-       let attr = {};
-
-       if (value >= me.criticalThreshold) {
-           color = me.criticalColor;
-       } else if (value >= me.warningThreshold) {
-           color = me.warningColor;
-       }
-
-       me.chart.series[0].setColors([color, me.backgroundColor]);
-       me.chart.series[0].setValue(value*100);
-
-       me.valueSprite.setText(' '+(value*100).toFixed(0) + '%');
-       attr.x = me.chart.getWidth()/2;
-       attr.y = me.chart.getHeight()-20;
-       if (me.spriteFontSize) {
-           attr.fontSize = me.spriteFontSize;
-       }
-       me.valueSprite.setAttributes(attr, true);
-
-       if (text !== undefined) {
-           me.text.setHtml(text);
-       }
-    },
-
-    initComponent: function() {
-       let me = this;
-
-       me.callParent();
-
-       if (me.title) {
-           me.getComponent('title').update({ title: me.title });
-       }
-       me.text = me.getComponent('text');
-       me.chart = me.getComponent('chart');
-       me.valueSprite = me.chart.getSurface('chart').get('valueSprite');
-    },
-});
diff --git a/panel/InputPanel.js b/panel/InputPanel.js
deleted file mode 100644 (file)
index 0ac5e48..0000000
+++ /dev/null
@@ -1,241 +0,0 @@
-Ext.define('Proxmox.panel.InputPanel', {
-    extend: 'Ext.panel.Panel',
-    alias: ['widget.inputpanel'],
-    listeners: {
-       activate: function() {
-           // notify owning container that it should display a help button
-           if (this.onlineHelp) {
-               Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp);
-           }
-       },
-       deactivate: function() {
-           if (this.onlineHelp) {
-               Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp);
-           }
-       },
-    },
-    border: false,
-
-    // override this with an URL to a relevant chapter of the pve manual
-    // setting this will display a help button in our parent panel
-    onlineHelp: undefined,
-
-    // will be set if the inputpanel has advanced items
-    hasAdvanced: false,
-
-    // if the panel has advanced items,
-    // this will determine if they are shown by default
-    showAdvanced: false,
-
-    // overwrite this to modify submit data
-    onGetValues: function(values) {
-       return values;
-    },
-
-    getValues: function(dirtyOnly) {
-       let me = this;
-
-       if (Ext.isFunction(me.onGetValues)) {
-           dirtyOnly = false;
-       }
-
-       let values = {};
-
-       Ext.Array.each(me.query('[isFormField]'), function(field) {
-           if (!dirtyOnly || field.isDirty()) {
-               Proxmox.Utils.assemble_field_data(values, field.getSubmitData());
-           }
-       });
-
-       return me.onGetValues(values);
-    },
-
-    setAdvancedVisible: function(visible) {
-       let me = this;
-       let advItems = me.getComponent('advancedContainer');
-       if (advItems) {
-           advItems.setVisible(visible);
-       }
-    },
-
-    setValues: function(values) {
-       let me = this;
-
-       let form = me.up('form');
-
-        Ext.iterate(values, function(fieldId, val) {
-           let fields = me.query('[isFormField][name=' + fieldId + ']');
-           for (const field of fields) {
-               if (field) {
-                   field.setValue(val);
-                   if (form.trackResetOnLoad) {
-                       field.resetOriginalValue();
-                   }
-               }
-           }
-       });
-    },
-
-    initComponent: function() {
-       let me = this;
-
-       let items;
-
-       if (me.items) {
-           me.columns = 1;
-           items = [
-               {
-                   columnWidth: 1,
-                   layout: 'anchor',
-                   items: me.items,
-               },
-           ];
-           me.items = undefined;
-       } else if (me.column4) {
-           me.columns = 4;
-           items = [
-               {
-                   columnWidth: 0.25,
-                   padding: '0 10 0 0',
-                   layout: 'anchor',
-                   items: me.column1,
-               },
-               {
-                   columnWidth: 0.25,
-                   padding: '0 10 0 0',
-                   layout: 'anchor',
-                   items: me.column2,
-               },
-               {
-                   columnWidth: 0.25,
-                   padding: '0 10 0 0',
-                   layout: 'anchor',
-                   items: me.column3,
-               },
-               {
-                   columnWidth: 0.25,
-                   padding: '0 0 0 10',
-                   layout: 'anchor',
-                   items: me.column4,
-               },
-           ];
-           if (me.columnB) {
-               items.push({
-                   columnWidth: 1,
-                   padding: '10 0 0 0',
-                   layout: 'anchor',
-                   items: me.columnB,
-               });
-           }
-       } else if (me.column1) {
-           me.columns = 2;
-           items = [
-               {
-                   columnWidth: 0.5,
-                   padding: '0 10 0 0',
-                   layout: 'anchor',
-                   items: me.column1,
-               },
-               {
-                   columnWidth: 0.5,
-                   padding: '0 0 0 10',
-                   layout: 'anchor',
-                   items: me.column2 || [], // allow empty column
-               },
-           ];
-           if (me.columnB) {
-               items.push({
-                   columnWidth: 1,
-                   padding: '10 0 0 0',
-                   layout: 'anchor',
-                   items: me.columnB,
-               });
-           }
-       } else {
-           throw "unsupported config";
-       }
-
-       let advItems;
-       if (me.advancedItems) {
-           advItems = [
-               {
-                   columnWidth: 1,
-                   layout: 'anchor',
-                   items: me.advancedItems,
-               },
-           ];
-           me.advancedItems = undefined;
-       } else if (me.advancedColumn1) {
-           advItems = [
-               {
-                   columnWidth: 0.5,
-                   padding: '0 10 0 0',
-                   layout: 'anchor',
-                   items: me.advancedColumn1,
-               },
-               {
-                   columnWidth: 0.5,
-                   padding: '0 0 0 10',
-                   layout: 'anchor',
-                   items: me.advancedColumn2 || [], // allow empty column
-               },
-           ];
-
-           me.advancedColumn1 = undefined;
-           me.advancedColumn2 = undefined;
-
-           if (me.advancedColumnB) {
-               advItems.push({
-                   columnWidth: 1,
-                   padding: '10 0 0 0',
-                   layout: 'anchor',
-                   items: me.advancedColumnB,
-               });
-               me.advancedColumnB = undefined;
-           }
-       }
-
-       if (advItems) {
-           me.hasAdvanced = true;
-           advItems.unshift({
-               columnWidth: 1,
-               xtype: 'box',
-               hidden: false,
-               border: true,
-               autoEl: {
-                   tag: 'hr',
-               },
-           });
-           items.push({
-               columnWidth: 1,
-               xtype: 'container',
-               itemId: 'advancedContainer',
-               hidden: !me.showAdvanced,
-               layout: 'column',
-               defaults: {
-                   border: false,
-               },
-               items: advItems,
-           });
-       }
-
-       if (me.useFieldContainer) {
-           Ext.apply(me, {
-               layout: 'fit',
-               items: Ext.apply(me.useFieldContainer, {
-                   layout: 'column',
-                   defaultType: 'container',
-                   items: items,
-               }),
-           });
-       } else {
-           Ext.apply(me, {
-               layout: 'column',
-               defaultType: 'container',
-               items: items,
-           });
-       }
-
-       me.callParent();
-    },
-});
diff --git a/panel/JournalView.js b/panel/JournalView.js
deleted file mode 100644 (file)
index b81cc45..0000000
+++ /dev/null
@@ -1,347 +0,0 @@
-/*
- * Display log entries in a panel with scrollbar
- * The log entries are automatically refreshed via a background task,
- * with newest entries comming at the bottom
- */
-Ext.define('Proxmox.panel.JournalView', {
-    extend: 'Ext.panel.Panel',
-    xtype: 'proxmoxJournalView',
-
-    numEntries: 500,
-    lineHeight: 16,
-
-    scrollToEnd: true,
-
-    controller: {
-       xclass: 'Ext.app.ViewController',
-
-       updateParams: function() {
-           let me = this;
-           let viewModel = me.getViewModel();
-           let since = viewModel.get('since');
-           let until = viewModel.get('until');
-
-           since.setHours(0, 0, 0, 0);
-           until.setHours(0, 0, 0, 0);
-           until.setDate(until.getDate()+1);
-
-           me.getView().loadTask.delay(200, undefined, undefined, [
-               false,
-               false,
-               Ext.Date.format(since, "U"),
-               Ext.Date.format(until, "U"),
-           ]);
-       },
-
-       scrollPosBottom: function() {
-           let view = this.getView();
-           let pos = view.getScrollY();
-           let maxPos = view.getScrollable().getMaxPosition().y;
-           return maxPos - pos;
-       },
-
-       scrollPosTop: function() {
-           let view = this.getView();
-           return view.getScrollY();
-       },
-
-       updateScroll: function(livemode, num, scrollPos, scrollPosTop) {
-           let me = this;
-           let view = me.getView();
-
-           if (!livemode) {
-               setTimeout(function() { view.scrollTo(0, 0); }, 10);
-           } else if (view.scrollToEnd && scrollPos <= 0) {
-               setTimeout(function() { view.scrollTo(0, Infinity); }, 10);
-           } else if (!view.scrollToEnd && scrollPosTop < 20 * view.lineHeight) {
-               setTimeout(function() { view.scrollTo(0, (num * view.lineHeight) + scrollPosTop); }, 10);
-           }
-       },
-
-       updateView: function(lines, livemode, top) {
-           let me = this;
-           let view = me.getView();
-           let viewmodel = me.getViewModel();
-           if (!viewmodel || viewmodel.get('livemode') !== livemode) {
-               return; // we switched mode, do not update the content
-           }
-           let contentEl = me.lookup('content');
-
-           // save old scrollpositions
-           let scrollPos = me.scrollPosBottom();
-           let scrollPosTop = me.scrollPosTop();
-
-           let newend = lines.shift();
-           let newstart = lines.pop();
-
-           let num = lines.length;
-           let text = lines.map(Ext.htmlEncode).join('<br>');
-
-           if (!livemode) {
-               if (num) {
-                   view.content = text;
-               } else {
-                   view.content = 'nothing logged or no timespan selected';
-               }
-           } else {
-               // update content
-               if (top && num) {
-                   view.content = view.content ? text + '<br>' + view.content : text;
-               } else if (!top && num) {
-                   view.content = view.content ? view.content + '<br>' + text : text;
-               }
-
-               // update cursors
-               if (!top || !view.startcursor) {
-                   view.startcursor = newstart;
-               }
-
-               if (top || !view.endcursor) {
-                   view.endcursor = newend;
-               }
-           }
-
-           contentEl.update(view.content);
-
-           me.updateScroll(livemode, num, scrollPos, scrollPosTop);
-       },
-
-       doLoad: function(livemode, top, since, until) {
-           let me = this;
-           if (me.running) {
-               me.requested = true;
-               return;
-           }
-           me.running = true;
-           let view = me.getView();
-           let params = {
-               lastentries: view.numEntries || 500,
-           };
-           if (livemode) {
-               if (!top && view.startcursor) {
-                   params = {
-                       startcursor: view.startcursor,
-                   };
-               } else if (view.endcursor) {
-                   params.endcursor = view.endcursor;
-               }
-           } else {
-               params = {
-                   since: since,
-                   until: until,
-               };
-           }
-           Proxmox.Utils.API2Request({
-               url: view.url,
-               params: params,
-               waitMsgTarget: !livemode ? view : undefined,
-               method: 'GET',
-               success: function(response) {
-                   Proxmox.Utils.setErrorMask(me, false);
-                   let lines = response.result.data;
-                   me.updateView(lines, livemode, top);
-                   me.running = false;
-                   if (me.requested) {
-                       me.requested = false;
-                       view.loadTask.delay(200);
-                   }
-               },
-               failure: function(response) {
-                   let msg = response.htmlStatus;
-                   Proxmox.Utils.setErrorMask(me, msg);
-                   me.running = false;
-                   if (me.requested) {
-                       me.requested = false;
-                       view.loadTask.delay(200);
-                   }
-               },
-           });
-       },
-
-       onScroll: function(x, y) {
-           let me = this;
-           let view = me.getView();
-           let viewmodel = me.getViewModel();
-           let livemode = viewmodel.get('livemode');
-           if (!livemode) {
-               return;
-           }
-
-           if (me.scrollPosTop() < 20*view.lineHeight) {
-               view.scrollToEnd = false;
-               view.loadTask.delay(200, undefined, undefined, [true, true]);
-           } else if (me.scrollPosBottom() <= 1) {
-               view.scrollToEnd = true;
-           }
-       },
-
-       init: function(view) {
-           let me = this;
-
-           if (!view.url) {
-               throw "no url specified";
-           }
-
-           let viewmodel = me.getViewModel();
-           let viewModel = this.getViewModel();
-           let since = new Date();
-           since.setDate(since.getDate() - 3);
-           viewModel.set('until', new Date());
-           viewModel.set('since', since);
-           me.lookup('content').setStyle('line-height', view.lineHeight + 'px');
-
-           view.loadTask = new Ext.util.DelayedTask(me.doLoad, me, [true, false]);
-
-           me.updateParams();
-           view.task = Ext.TaskManager.start({
-               run: function() {
-                   if (!view.isVisible() || !view.scrollToEnd || !viewmodel.get('livemode')) {
-                       return;
-                   }
-
-                   if (me.scrollPosBottom() <= 1) {
-                       view.loadTask.delay(200, undefined, undefined, [true, false]);
-                   }
-               },
-               interval: 1000,
-           });
-       },
-
-       onLiveMode: function() {
-           let me = this;
-           let view = me.getView();
-           delete view.startcursor;
-           delete view.endcursor;
-           delete view.content;
-           me.getViewModel().set('livemode', true);
-           view.scrollToEnd = true;
-           me.updateView([], true, false);
-       },
-
-       onTimespan: function() {
-           let me = this;
-           me.getViewModel().set('livemode', false);
-           me.updateView([], false);
-       },
-    },
-
-    onDestroy: function() {
-       let me = this;
-       me.loadTask.cancel();
-       Ext.TaskManager.stop(me.task);
-       delete me.content;
-    },
-
-    // for user to initiate a load from outside
-    requestUpdate: function() {
-       let me = this;
-       me.loadTask.delay(200);
-    },
-
-    viewModel: {
-       data: {
-           livemode: true,
-           until: null,
-           since: null,
-       },
-    },
-
-    layout: 'auto',
-    bodyPadding: 5,
-    scrollable: {
-       x: 'auto',
-       y: 'auto',
-       listeners: {
-           // we have to have this here, since we cannot listen to events
-           // of the scroller in the viewcontroller (extjs bug?), nor does
-           // the panel have a 'scroll' event'
-           scroll: {
-               fn: function(scroller, x, y) {
-                   let controller = this.component.getController();
-                   if (controller) { // on destroy, controller can be gone
-                       controller.onScroll(x, y);
-                   }
-               },
-               buffer: 200,
-           },
-       },
-    },
-
-    tbar: {
-
-       items: [
-           '->',
-           {
-               xtype: 'segmentedbutton',
-               items: [
-                   {
-                       text: gettext('Live Mode'),
-                       bind: {
-                           pressed: '{livemode}',
-                       },
-                       handler: 'onLiveMode',
-                   },
-                   {
-                       text: gettext('Select Timespan'),
-                       bind: {
-                           pressed: '{!livemode}',
-                       },
-                       handler: 'onTimespan',
-                   },
-               ],
-           },
-           {
-               xtype: 'box',
-               bind: { disabled: '{livemode}' },
-               autoEl: { cn: gettext('Since') + ':' },
-           },
-           {
-               xtype: 'datefield',
-               name: 'since_date',
-               reference: 'since',
-               format: 'Y-m-d',
-               bind: {
-                   disabled: '{livemode}',
-                   value: '{since}',
-                   maxValue: '{until}',
-               },
-           },
-           {
-               xtype: 'box',
-               bind: { disabled: '{livemode}' },
-               autoEl: { cn: gettext('Until') + ':' },
-           },
-           {
-               xtype: 'datefield',
-               name: 'until_date',
-               reference: 'until',
-               format: 'Y-m-d',
-               bind: {
-                   disabled: '{livemode}',
-                   value: '{until}',
-                   minValue: '{since}',
-               },
-           },
-           {
-               xtype: 'button',
-               text: 'Update',
-               reference: 'updateBtn',
-               handler: 'updateParams',
-               bind: {
-                   disabled: '{livemode}',
-               },
-           },
-       ],
-    },
-
-    items: [
-       {
-           xtype: 'box',
-           reference: 'content',
-           style: {
-               font: 'normal 11px tahoma, arial, verdana, sans-serif',
-               'white-space': 'pre',
-           },
-       },
-    ],
-});
diff --git a/panel/LogView.js b/panel/LogView.js
deleted file mode 100644 (file)
index 1ce83bc..0000000
+++ /dev/null
@@ -1,275 +0,0 @@
-/*
- * Display log entries in a panel with scrollbar
- * The log entries are automatically refreshed via a background task,
- * with newest entries comming at the bottom
- */
-Ext.define('Proxmox.panel.LogView', {
-    extend: 'Ext.panel.Panel',
-    xtype: 'proxmoxLogView',
-
-    pageSize: 500,
-    viewBuffer: 50,
-    lineHeight: 16,
-
-    scrollToEnd: true,
-
-    // callback for load failure, used for ceph
-    failCallback: undefined,
-
-    controller: {
-       xclass: 'Ext.app.ViewController',
-
-       updateParams: function() {
-           let me = this;
-           let viewModel = me.getViewModel();
-           let since = viewModel.get('since');
-           let until = viewModel.get('until');
-           if (viewModel.get('hide_timespan')) {
-               return;
-           }
-
-           if (since > until) {
-               Ext.Msg.alert('Error', 'Since date must be less equal than Until date.');
-               return;
-           }
-
-           viewModel.set('params.since', Ext.Date.format(since, 'Y-m-d'));
-           viewModel.set('params.until', Ext.Date.format(until, 'Y-m-d') + ' 23:59:59');
-           me.getView().loadTask.delay(200);
-       },
-
-       scrollPosBottom: function() {
-           let view = this.getView();
-           let pos = view.getScrollY();
-           let maxPos = view.getScrollable().getMaxPosition().y;
-           return maxPos - pos;
-       },
-
-       updateView: function(text, first, total) {
-           let me = this;
-           let view = me.getView();
-           let viewModel = me.getViewModel();
-           let content = me.lookup('content');
-           let data = viewModel.get('data');
-
-           if (first === data.first && total === data.total && text.length === data.textlen) {
-               return; // same content, skip setting and scrolling
-           }
-           viewModel.set('data', {
-               first: first,
-               total: total,
-               textlen: text.length,
-           });
-
-           let scrollPos = me.scrollPosBottom();
-
-           content.update(text);
-
-           if (view.scrollToEnd && scrollPos <= 0) {
-               // we use setTimeout to work around scroll handling on touchscreens
-               setTimeout(function() { view.scrollTo(0, Infinity); }, 10);
-           }
-       },
-
-       doLoad: function() {
-           let me = this;
-           if (me.running) {
-               me.requested = true;
-               return;
-           }
-           me.running = true;
-           let view = me.getView();
-           let viewModel = me.getViewModel();
-           Proxmox.Utils.API2Request({
-               url: me.getView().url,
-               params: viewModel.get('params'),
-               method: 'GET',
-               success: function(response) {
-                   Proxmox.Utils.setErrorMask(me, false);
-                   let total = response.result.total;
-                   let lines = [];
-                   let first = Infinity;
-
-                   Ext.Array.each(response.result.data, function(line) {
-                       if (first > line.n) {
-                           first = line.n;
-                       }
-                       lines[line.n - 1] = Ext.htmlEncode(line.t);
-                   });
-
-                   lines.length = total;
-                   me.updateView(lines.join('<br>'), first - 1, total);
-                   me.running = false;
-                   if (me.requested) {
-                       me.requested = false;
-                       view.loadTask.delay(200);
-                   }
-               },
-               failure: function(response) {
-                   if (view.failCallback) {
-                       view.failCallback(response);
-                   } else {
-                       let msg = response.htmlStatus;
-                       Proxmox.Utils.setErrorMask(me, msg);
-                   }
-                   me.running = false;
-                   if (me.requested) {
-                       me.requested = false;
-                       view.loadTask.delay(200);
-                   }
-               },
-           });
-       },
-
-       onScroll: function(x, y) {
-           let me = this;
-           let view = me.getView();
-           let viewModel = me.getViewModel();
-
-           let lineHeight = view.lineHeight;
-           let line = view.getScrollY()/lineHeight;
-           let start = viewModel.get('params.start');
-           let limit = viewModel.get('params.limit');
-           let viewLines = view.getHeight()/lineHeight;
-
-           let viewStart = Math.max(parseInt(line - 1 - view.viewBuffer, 10), 0);
-           let viewEnd = parseInt(line + viewLines + 1 + view.viewBuffer, 10);
-
-           if (viewStart < start || viewEnd > start+limit) {
-               viewModel.set('params.start',
-                   Math.max(parseInt(line - (limit / 2) + 10, 10), 0));
-               view.loadTask.delay(200);
-           }
-       },
-
-       init: function(view) {
-           let me = this;
-
-           if (!view.url) {
-               throw "no url specified";
-           }
-
-           let viewModel = this.getViewModel();
-           let since = new Date();
-           since.setDate(since.getDate() - 3);
-           viewModel.set('until', new Date());
-           viewModel.set('since', since);
-           viewModel.set('params.limit', view.pageSize);
-           viewModel.set('hide_timespan', !view.log_select_timespan);
-           me.lookup('content').setStyle('line-height', view.lineHeight + 'px');
-
-           view.loadTask = new Ext.util.DelayedTask(me.doLoad, me);
-
-           me.updateParams();
-           view.task = Ext.TaskManager.start({
-               run: function() {
-                   if (!view.isVisible() || !view.scrollToEnd) {
-                       return;
-                   }
-
-                   if (me.scrollPosBottom() <= 1) {
-                       view.loadTask.delay(200);
-                   }
-               },
-               interval: 1000,
-           });
-       },
-    },
-
-    onDestroy: function() {
-       let me = this;
-       me.loadTask.cancel();
-       Ext.TaskManager.stop(me.task);
-    },
-
-    // for user to initiate a load from outside
-    requestUpdate: function() {
-       let me = this;
-       me.loadTask.delay(200);
-    },
-
-    viewModel: {
-       data: {
-           until: null,
-           since: null,
-           hide_timespan: false,
-           data: {
-               start: 0,
-               total: 0,
-               textlen: 0,
-           },
-           params: {
-               start: 0,
-               limit: 500,
-           },
-       },
-    },
-
-    layout: 'auto',
-    bodyPadding: 5,
-    scrollable: {
-       x: 'auto',
-       y: 'auto',
-       listeners: {
-           // we have to have this here, since we cannot listen to events
-           // of the scroller in the viewcontroller (extjs bug?), nor does
-           // the panel have a 'scroll' event'
-           scroll: {
-               fn: function(scroller, x, y) {
-                   let controller = this.component.getController();
-                   if (controller) { // on destroy, controller can be gone
-                       controller.onScroll(x, y);
-                   }
-               },
-               buffer: 200,
-           },
-       },
-    },
-
-    tbar: {
-       bind: {
-           hidden: '{hide_timespan}',
-       },
-       items: [
-           '->',
-           'Since: ',
-           {
-               xtype: 'datefield',
-               name: 'since_date',
-               reference: 'since',
-               format: 'Y-m-d',
-               bind: {
-                   value: '{since}',
-                   maxValue: '{until}',
-               },
-           },
-           'Until: ',
-           {
-               xtype: 'datefield',
-               name: 'until_date',
-               reference: 'until',
-               format: 'Y-m-d',
-               bind: {
-                   value: '{until}',
-                   minValue: '{since}',
-               },
-           },
-           {
-               xtype: 'button',
-               text: 'Update',
-               handler: 'updateParams',
-           },
-       ],
-    },
-
-    items: [
-       {
-           xtype: 'box',
-           reference: 'content',
-           style: {
-               font: 'normal 11px tahoma, arial, verdana, sans-serif',
-               'white-space': 'pre',
-           },
-       },
-    ],
-});
diff --git a/panel/RRDChart.js b/panel/RRDChart.js
deleted file mode 100644 (file)
index b4f89a4..0000000
+++ /dev/null
@@ -1,186 +0,0 @@
-Ext.define('Proxmox.widget.RRDChart', {
-    extend: 'Ext.chart.CartesianChart',
-    alias: 'widget.proxmoxRRDChart',
-
-    unit: undefined, // bytes, bytespersecond, percent
-
-    controller: {
-       xclass: 'Ext.app.ViewController',
-
-       convertToUnits: function(value) {
-           let units = ['', 'k', 'M', 'G', 'T', 'P'];
-           let si = 0;
-           let format = '0.##';
-           if (value < 0.1) format += '#';
-           while (value >= 1000 && si < units.length -1) {
-               value = value / 1000;
-               si++;
-           }
-
-           // javascript floating point weirdness
-           value = Ext.Number.correctFloat(value);
-
-           // limit decimal points
-           value = Ext.util.Format.number(value, format);
-
-           return value.toString() + " " + units[si];
-       },
-
-       leftAxisRenderer: function(axis, label, layoutContext) {
-           let me = this;
-           return me.convertToUnits(label);
-       },
-
-       onSeriesTooltipRender: function(tooltip, record, item) {
-           let view = this.getView();
-
-           let suffix = '';
-           if (view.unit === 'percent') {
-               suffix = '%';
-           } else if (view.unit === 'bytes') {
-               suffix = 'B';
-           } else if (view.unit === 'bytespersecond') {
-               suffix = 'B/s';
-           }
-
-           let prefix = item.field;
-           if (view.fieldTitles && view.fieldTitles[view.fields.indexOf(item.field)]) {
-               prefix = view.fieldTitles[view.fields.indexOf(item.field)];
-           }
-           let v = this.convertToUnits(record.get(item.field));
-           let t = new Date(record.get('time'));
-           tooltip.setHtml(`${prefix}: ${v}${suffix}<br>${t}`);
-       },
-
-       onAfterAnimation: function(chart, eopts) {
-           // if the undo button is disabled, disable our tool
-           let ourUndoZoomButton = chart.header.tools[0];
-           let undoButton = chart.interactions[0].getUndoButton();
-           ourUndoZoomButton.setDisabled(undoButton.isDisabled());
-       },
-    },
-
-    width: 770,
-    height: 300,
-    animation: false,
-    interactions: [
-       {
-           type: 'crosszoom',
-       },
-    ],
-    legend: {
-       padding: 0,
-    },
-    axes: [
-       {
-           type: 'numeric',
-           position: 'left',
-           grid: true,
-           renderer: 'leftAxisRenderer',
-           minimum: 0,
-       },
-       {
-           type: 'time',
-           position: 'bottom',
-           grid: true,
-           fields: ['time'],
-       },
-    ],
-    listeners: {
-       animationend: 'onAfterAnimation',
-    },
-
-    initComponent: function() {
-       let me = this;
-
-       if (!me.store) {
-           throw "cannot work without store";
-       }
-
-       if (!me.fields) {
-           throw "cannot work without fields";
-       }
-
-       me.callParent();
-
-       // add correct label for left axis
-       let axisTitle = "";
-       if (me.unit === 'percent') {
-           axisTitle = "%";
-       } else if (me.unit === 'bytes') {
-           axisTitle = "Bytes";
-       } else if (me.unit === 'bytespersecond') {
-           axisTitle = "Bytes/s";
-       } else if (me.fieldTitles && me.fieldTitles.length === 1) {
-           axisTitle = me.fieldTitles[0];
-       } else if (me.fields.length === 1) {
-           axisTitle = me.fields[0];
-       }
-
-       me.axes[0].setTitle(axisTitle);
-
-       me.updateHeader();
-
-       if (me.header && me.legend) {
-           me.header.padding = '4 9 4';
-           me.header.add(me.legend);
-       }
-
-       if (!me.noTool) {
-           me.addTool({
-               type: 'minus',
-               disabled: true,
-               tooltip: gettext('Undo Zoom'),
-               handler: function() {
-                   let undoButton = me.interactions[0].getUndoButton();
-                   if (undoButton.handler) {
-                       undoButton.handler();
-                   }
-               },
-           });
-       }
-
-       // add a series for each field we get
-       me.fields.forEach(function(item, index) {
-           let title = item;
-           if (me.fieldTitles && me.fieldTitles[index]) {
-               title = me.fieldTitles[index];
-           }
-           me.addSeries(Ext.apply(
-               {
-                   type: 'line',
-                   xField: 'time',
-                   yField: item,
-                   title: title,
-                   fill: true,
-                   style: {
-                       lineWidth: 1.5,
-                       opacity: 0.60,
-                   },
-                   marker: {
-                       opacity: 0,
-                       scaling: 0.01,
-                       fx: {
-                           duration: 200,
-                           easing: 'easeOut',
-                       },
-                   },
-                   highlightCfg: {
-                       opacity: 1,
-                       scaling: 1.5,
-                   },
-                   tooltip: {
-                       trackMouse: true,
-                       renderer: 'onSeriesTooltipRender',
-                   },
-               },
-               me.seriesConfig,
-           ));
-       });
-
-       // enable animation after the store is loaded
-       me.store.onAfter('load', function() {
-           me.setAnimation(true);
-       }, this, { single: true });
-    },
-});
diff --git a/src/Logo.js b/src/Logo.js
new file mode 100644 (file)
index 0000000..78d9aad
--- /dev/null
@@ -0,0 +1,21 @@
+Ext.define('PMX.image.Logo', {
+    extend: 'Ext.Img',
+    xtype: 'proxmoxlogo',
+
+    height: 30,
+    width: 172,
+    src: '/images/proxmox_logo.png',
+    alt: 'Proxmox',
+    autoEl: {
+       tag: 'a',
+       href: 'https://www.proxmox.com',
+       target: '_blank',
+    },
+
+    initComponent: function() {
+       let me = this;
+       let prefix = me.prefix !== undefined ? me.prefix : '/pve2';
+       me.src = prefix + me.src;
+       me.callParent();
+    },
+});
diff --git a/src/Makefile b/src/Makefile
new file mode 100644 (file)
index 0000000..659e876
--- /dev/null
@@ -0,0 +1,73 @@
+include defines.mk
+
+SUBDIRS= css images
+
+JSSRC=                                 \
+       Utils.js                        \
+       Toolkit.js                      \
+       Logo.js                         \
+       mixin/CBind.js                  \
+       data/reader/JsonObject.js       \
+       data/ProxmoxProxy.js            \
+       data/UpdateStore.js             \
+       data/DiffStore.js               \
+       data/ObjectStore.js             \
+       data/RRDStore.js                \
+       data/TimezoneStore.js           \
+       data/model/Realm.js             \
+       form/DisplayEdit.js             \
+       form/ExpireDate.js              \
+       form/IntegerField.js            \
+       form/TextField.js               \
+       form/DateTimeField.js           \
+       form/Checkbox.js                \
+       form/KVComboBox.js              \
+       form/LanguageSelector.js        \
+       form/ComboGrid.js               \
+       form/RRDTypeSelector.js         \
+       form/BondModeSelector.js        \
+       form/NetworkSelector.js         \
+       form/RealmComboBox.js           \
+       form/RoleSelector.js            \
+       button/Button.js                \
+       button/HelpButton.js            \
+       grid/ObjectGrid.js              \
+       grid/PendingObjectGrid.js       \
+       panel/InputPanel.js             \
+       panel/LogView.js                \
+       panel/JournalView.js            \
+       panel/RRDChart.js               \
+       panel/GaugeWidget.js            \
+       window/Edit.js                  \
+       window/PasswordEdit.js          \
+       window/TaskViewer.js            \
+       window/LanguageEdit.js          \
+       node/APT.js                     \
+       node/NetworkEdit.js             \
+       node/NetworkView.js             \
+       node/DNSEdit.js                 \
+       node/HostsView.js               \
+       node/DNSView.js                 \
+       node/Tasks.js                   \
+       node/ServiceView.js             \
+       node/TimeEdit.js                \
+       node/TimeView.js
+
+all: ${SUBDIRS}
+       set -e && for i in ${SUBDIRS}; do ${MAKE} -C $$i; done
+
+.PHONY: lint
+check: lint
+lint: ${JSSRC}
+       eslint ${JSSRC}
+
+proxmoxlib.js: ${JSSRC}
+       # add the version as comment in the file
+       echo "// ${DEB_VERSION_UPSTREAM_REVISION}" > $@.tmp
+       cat ${JSSRC} >> $@.tmp
+       mv $@.tmp $@
+
+install: proxmoxlib.js
+       install -d -m 755 ${WWWBASEDIR}
+       install -m 0644 proxmoxlib.js ${WWWBASEDIR}
+       set -e && for i in ${SUBDIRS}; do ${MAKE} -C $$i $@; done
diff --git a/src/Toolkit.js b/src/Toolkit.js
new file mode 100644 (file)
index 0000000..d528ea7
--- /dev/null
@@ -0,0 +1,673 @@
+// ExtJS related things
+
+ // do not send '_dc' parameter
+Ext.Ajax.disableCaching = false;
+
+// FIXME: HACK! Makes scrolling in number spinner work again. fixed in ExtJS >= 6.1
+if (Ext.isFirefox) {
+    Ext.$eventNameMap.DOMMouseScroll = 'DOMMouseScroll';
+}
+
+// custom Vtypes
+Ext.apply(Ext.form.field.VTypes, {
+    IPAddress: function(v) {
+       return Proxmox.Utils.IP4_match.test(v);
+    },
+    IPAddressText: gettext('Example') + ': 192.168.1.1',
+    IPAddressMask: /[\d.]/i,
+
+    IPCIDRAddress: function(v) {
+       let result = Proxmox.Utils.IP4_cidr_match.exec(v);
+       // limits according to JSON Schema see
+       // pve-common/src/PVE/JSONSchema.pm
+       return result !== null && result[1] >= 8 && result[1] <= 32;
+    },
+    IPCIDRAddressText: gettext('Example') + ': 192.168.1.1/24<br>' + gettext('Valid CIDR Range') + ': 8-32',
+    IPCIDRAddressMask: /[\d./]/i,
+
+    IP6Address: function(v) {
+        return Proxmox.Utils.IP6_match.test(v);
+    },
+    IP6AddressText: gettext('Example') + ': 2001:DB8::42',
+    IP6AddressMask: /[A-Fa-f0-9:]/,
+
+    IP6CIDRAddress: function(v) {
+       let result = Proxmox.Utils.IP6_cidr_match.exec(v);
+       // limits according to JSON Schema see
+       // pve-common/src/PVE/JSONSchema.pm
+       return result !== null && result[1] >= 8 && result[1] <= 128;
+    },
+    IP6CIDRAddressText: gettext('Example') + ': 2001:DB8::42/64<br>' + gettext('Valid CIDR Range') + ': 8-128',
+    IP6CIDRAddressMask: /[A-Fa-f0-9:/]/,
+
+    IP6PrefixLength: function(v) {
+       return v >= 0 && v <= 128;
+    },
+    IP6PrefixLengthText: gettext('Example') + ': X, where 0 <= X <= 128',
+    IP6PrefixLengthMask: /[0-9]/,
+
+    IP64Address: function(v) {
+        return Proxmox.Utils.IP64_match.test(v);
+    },
+    IP64AddressText: gettext('Example') + ': 192.168.1.1 2001:DB8::42',
+    IP64AddressMask: /[A-Fa-f0-9.:]/,
+
+    IP64CIDRAddress: function(v) {
+       let result = Proxmox.Utils.IP64_cidr_match.exec(v);
+       if (result === null) {
+           return false;
+       }
+       if (result[1] !== undefined) {
+           return result[1] >= 8 && result[1] <= 128;
+       } else if (result[2] !== undefined) {
+           return result[2] >= 8 && result[2] <= 32;
+       } else {
+           return false;
+       }
+    },
+    IP64CIDRAddressText: gettext('Example') + ': 192.168.1.1/24 2001:DB8::42/64',
+    IP64CIDRAddressMask: /[A-Fa-f0-9.:/]/,
+
+    MacAddress: function(v) {
+       return (/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/).test(v);
+    },
+    MacAddressMask: /[a-fA-F0-9:]/,
+    MacAddressText: gettext('Example') + ': 01:23:45:67:89:ab',
+
+    MacPrefix: function(v) {
+       return (/^[a-f0-9][02468ace](?::[a-f0-9]{2}){0,2}:?$/i).test(v);
+    },
+    MacPrefixMask: /[a-fA-F0-9:]/,
+    MacPrefixText: gettext('Example') + ': 02:8f - ' + gettext('only unicast addresses are allowed'),
+
+    BridgeName: function(v) {
+        return (/^vmbr\d{1,4}$/).test(v);
+    },
+    VlanName: function(v) {
+       if (Proxmox.Utils.VlanInterface_match.test(v)) {
+         return true;
+       } else if (Proxmox.Utils.Vlan_match.test(v)) {
+         return true;
+       }
+       return true;
+    },
+    BridgeNameText: gettext('Format') + ': vmbr<b>N</b>, where 0 <= <b>N</b> <= 9999',
+
+    BondName: function(v) {
+        return (/^bond\d{1,4}$/).test(v);
+    },
+    BondNameText: gettext('Format') + ': bond<b>N</b>, where 0 <= <b>N</b> <= 9999',
+
+    InterfaceName: function(v) {
+        return (/^[a-z][a-z0-9_]{1,20}$/).test(v);
+    },
+    InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'<br />" +
+                      gettext("Minimum characters") + ": 2<br />" +
+                      gettext("Maximum characters") + ": 21<br />" +
+                      gettext("Must start with") + ": 'a-z'",
+
+    StorageId: function(v) {
+        return (/^[a-z][a-z0-9\-_.]*[a-z0-9]$/i).test(v);
+    },
+    StorageIdText: gettext("Allowed characters") + ":  'A-Z', 'a-z', '0-9', '-', '_', '.'<br />" +
+                  gettext("Minimum characters") + ": 2<br />" +
+                  gettext("Must start with") + ": 'A-Z', 'a-z'<br />" +
+                  gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'<br />",
+
+    ConfigId: function(v) {
+        return (/^[a-z][a-z0-9_]+$/i).test(v);
+    },
+    ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'<br />" +
+                 gettext("Minimum characters") + ": 2<br />" +
+                 gettext("Must start with") + ": " + gettext("letter"),
+
+    HttpProxy: function(v) {
+        return (/^http:\/\/.*$/).test(v);
+    },
+    HttpProxyText: gettext('Example') + ": http://username:password&#64;host:port/",
+
+    DnsName: function(v) {
+       return Proxmox.Utils.DnsName_match.test(v);
+    },
+    DnsNameText: gettext('This is not a valid DNS name'),
+
+    // workaround for https://www.sencha.com/forum/showthread.php?302150
+    proxmoxMail: function(v) {
+        return (/^(\w+)([-+.][\w]+)*@(\w[-\w]*\.){1,5}([A-Za-z]){2,63}$/).test(v);
+    },
+    proxmoxMailText: gettext('Example') + ": user@example.com",
+
+    DnsOrIp: function(v) {
+       if (!Proxmox.Utils.DnsName_match.test(v) &&
+           !Proxmox.Utils.IP64_match.test(v)) {
+           return false;
+       }
+
+       return true;
+    },
+    DnsOrIpText: gettext('Not a valid DNS name or IP address.'),
+
+    HostList: function(v) {
+       let list = v.split(/[ ,;]+/);
+       let i;
+       for (i = 0; i < list.length; i++) {
+           if (list[i] === '') {
+               continue;
+           }
+
+           if (!Proxmox.Utils.HostPort_match.test(list[i]) &&
+               !Proxmox.Utils.HostPortBrackets_match.test(list[i]) &&
+               !Proxmox.Utils.IP6_dotnotation_match.test(list[i])) {
+               return false;
+           }
+       }
+
+       return true;
+    },
+    HostListText: gettext('Not a valid list of hosts'),
+
+    password: function(val, field) {
+        if (field.initialPassField) {
+            let pwd = field.up('form').down(`[name=${field.initialPassField}]`);
+            return val === pwd.getValue();
+        }
+        return true;
+    },
+
+    passwordText: gettext('Passwords do not match'),
+});
+
+// Firefox 52+ Touchscreen bug
+// see https://www.sencha.com/forum/showthread.php?336762-Examples-don-t-work-in-Firefox-52-touchscreen/page2
+// and https://bugzilla.proxmox.com/show_bug.cgi?id=1223
+Ext.define('EXTJS_23846.Element', {
+    override: 'Ext.dom.Element',
+}, function(Element) {
+    let supports = Ext.supports,
+        proto = Element.prototype,
+        eventMap = proto.eventMap,
+        additiveEvents = proto.additiveEvents;
+
+    if (Ext.os.is.Desktop && supports.TouchEvents && !supports.PointerEvents) {
+        eventMap.touchstart = 'mousedown';
+        eventMap.touchmove = 'mousemove';
+        eventMap.touchend = 'mouseup';
+        eventMap.touchcancel = 'mouseup';
+
+        additiveEvents.mousedown = 'mousedown';
+        additiveEvents.mousemove = 'mousemove';
+        additiveEvents.mouseup = 'mouseup';
+        additiveEvents.touchstart = 'touchstart';
+        additiveEvents.touchmove = 'touchmove';
+        additiveEvents.touchend = 'touchend';
+        additiveEvents.touchcancel = 'touchcancel';
+
+        additiveEvents.pointerdown = 'mousedown';
+        additiveEvents.pointermove = 'mousemove';
+        additiveEvents.pointerup = 'mouseup';
+        additiveEvents.pointercancel = 'mouseup';
+    }
+});
+
+Ext.define('EXTJS_23846.Gesture', {
+    override: 'Ext.event.publisher.Gesture',
+}, function(Gesture) {
+    let gestures = Gesture.instance;
+
+    if (Ext.supports.TouchEvents && !Ext.isWebKit && Ext.os.is.Desktop) {
+        gestures.handledDomEvents.push('mousedown', 'mousemove', 'mouseup');
+        gestures.registerEvents();
+    }
+});
+
+Ext.define('EXTJS_18900.Pie', {
+    override: 'Ext.chart.series.Pie',
+
+    // from 6.0.2
+    betweenAngle: function(x, a, b) {
+        let pp = Math.PI * 2,
+            offset = this.rotationOffset;
+
+        if (a === b) {
+            return false;
+        }
+
+        if (!this.getClockwise()) {
+            x *= -1;
+            a *= -1;
+            b *= -1;
+            a -= offset;
+            b -= offset;
+        } else {
+            a += offset;
+            b += offset;
+        }
+
+        x -= a;
+        b -= a;
+
+        // Normalize, so that both x and b are in the [0,360) interval.
+        x %= pp;
+        b %= pp;
+        x += pp;
+        b += pp;
+        x %= pp;
+        b %= pp;
+
+        // Because 360 * n angles will be normalized to 0,
+        // we need to treat b === 0 as a special case.
+        return x < b || b === 0;
+    },
+});
+
+// we always want the number in x.y format and never in, e.g., x,y
+Ext.define('PVE.form.field.Number', {
+    override: 'Ext.form.field.Number',
+    submitLocaleSeparator: false,
+});
+
+// ExtJs 5-6 has an issue with caching
+// see https://www.sencha.com/forum/showthread.php?308989
+Ext.define('Proxmox.UnderlayPool', {
+    override: 'Ext.dom.UnderlayPool',
+
+    checkOut: function() {
+        let cache = this.cache,
+            len = cache.length,
+            el;
+
+        // do cleanup because some of the objects might have been destroyed
+       while (len--) {
+            if (cache[len].destroyed) {
+                cache.splice(len, 1);
+            }
+        }
+        // end do cleanup
+
+       el = cache.shift();
+
+        if (!el) {
+            el = Ext.Element.create(this.elementConfig);
+            el.setVisibilityMode(2);
+            //<debug>
+            // tell the spec runner to ignore this element when checking if the dom is clean
+           el.dom.setAttribute('data-sticky', true);
+            //</debug>
+       }
+
+        return el;
+    },
+});
+
+// 'Enter' in Textareas and aria multiline fields should not activate the
+// defaultbutton, fixed in extjs 6.0.2
+Ext.define('PVE.panel.Panel', {
+    override: 'Ext.panel.Panel',
+
+    fireDefaultButton: function(e) {
+       if (e.target.getAttribute('aria-multiline') === 'true' ||
+           e.target.tagName === "TEXTAREA") {
+           return true;
+       }
+       return this.callParent(arguments);
+    },
+});
+
+// if the order of the values are not the same in originalValue and value
+// extjs will not overwrite value, but marks the field dirty and thus
+// the reset button will be enabled (but clicking it changes nothing)
+// so if the arrays are not the same after resetting, we
+// clear and set it
+Ext.define('Proxmox.form.ComboBox', {
+    override: 'Ext.form.field.ComboBox',
+
+    reset: function() {
+       // copied from combobox
+       let me = this;
+       me.callParent();
+
+       // clear and set when not the same
+       let value = me.getValue();
+       if (Ext.isArray(me.originalValue) && Ext.isArray(value) &&
+           !Ext.Array.equals(value, me.originalValue)) {
+           me.clearValue();
+           me.setValue(me.originalValue);
+       }
+    },
+
+    // we also want to open the trigger on editable comboboxes by default
+    initComponent: function() {
+       let me = this;
+       me.callParent();
+
+       if (me.editable) {
+           // The trigger.picker causes first a focus event on the field then
+           // toggles the selection picker. Thus skip expanding in this case,
+           // else our focus listener expands and the picker.trigger then
+           // collapses it directly afterwards.
+           Ext.override(me.triggers.picker, {
+               onMouseDown: function(e) {
+                   // copied "should we focus" check from Ext.form.trigger.Trigger
+                   if (e.pointerType !== 'touch' && !this.field.owns(Ext.Element.getActiveElement())) {
+                       me.skip_expand_on_focus = true;
+                   }
+                   this.callParent(arguments);
+               },
+           });
+
+           me.on("focus", function(combobox) {
+               if (!combobox.isExpanded && !combobox.skip_expand_on_focus) {
+                   combobox.expand();
+               }
+               combobox.skip_expand_on_focus = false;
+           });
+       }
+    },
+});
+
+// when refreshing a grid/tree view, restoring the focus moves the view back to
+// the previously focused item. Save scroll position before refocusing.
+Ext.define(null, {
+    override: 'Ext.view.Table',
+
+    jumpToFocus: false,
+
+    saveFocusState: function() {
+       let me = this,
+           store = me.dataSource,
+           actionableMode = me.actionableMode,
+           navModel = me.getNavigationModel(),
+           focusPosition = actionableMode ? me.actionPosition : navModel.getPosition(true),
+           refocusRow, refocusCol;
+
+       if (focusPosition) {
+           // Separate this from the instance that the nav model is using.
+           focusPosition = focusPosition.clone();
+
+           // Exit actionable mode.
+           // We must inform any Actionables that they must relinquish control.
+           // Tabbability must be reset.
+           if (actionableMode) {
+               me.ownerGrid.setActionableMode(false);
+           }
+
+           // Blur the focused descendant, but do not trigger focusLeave.
+           me.el.dom.focus();
+
+           // Exiting actionable mode navigates to the owning cell, so in either focus mode we must
+           // clear the navigation position
+           navModel.setPosition();
+
+           // The following function will attempt to refocus back in the same mode to the same cell
+           // as it was at before based upon the previous record (if it's still inthe store), or the row index.
+           return function() {
+               // If we still have data, attempt to refocus in the same mode.
+               if (store.getCount()) {
+                   // Adjust expectations of where we are able to refocus according to what kind of destruction
+                   // might have been wrought on this view's DOM during focus save.
+                   refocusRow = Math.min(focusPosition.rowIdx, me.all.getCount() - 1);
+                   refocusCol = Math.min(focusPosition.colIdx,
+                                         me.getVisibleColumnManager().getColumns().length - 1);
+                   refocusRow = store.contains(focusPosition.record) ? focusPosition.record : refocusRow;
+                   focusPosition = new Ext.grid.CellContext(me).setPosition(refocusRow, refocusCol);
+
+                   if (actionableMode) {
+                       me.ownerGrid.setActionableMode(true, focusPosition);
+                   } else {
+                       me.cellFocused = true;
+
+                       // we sometimes want to scroll back to where we were
+                       let x = me.getScrollX();
+                       let y = me.getScrollY();
+
+                       // Pass "preventNavigation" as true so that that does not cause selection.
+                       navModel.setPosition(focusPosition, null, null, null, true);
+
+                       if (!me.jumpToFocus) {
+                           me.scrollTo(x, y);
+                       }
+                   }
+               } else { // No rows - focus associated column header
+                   focusPosition.column.focus();
+               }
+           };
+       }
+       return Ext.emptyFn;
+    },
+});
+
+// should be fixed with ExtJS 6.0.2, see:
+// https://www.sencha.com/forum/showthread.php?307244-Bug-with-datefield-in-window-with-scroll
+Ext.define('Proxmox.Datepicker', {
+    override: 'Ext.picker.Date',
+    hideMode: 'visibility',
+});
+
+// ExtJS 6.0.1 has no setSubmitValue() (although you find it in the docs).
+// Note: this.submitValue is a boolean flag, whereas getSubmitValue() returns
+// data to be submitted.
+Ext.define('Proxmox.form.field.Text', {
+    override: 'Ext.form.field.Text',
+
+    setSubmitValue: function(v) {
+       this.submitValue = v;
+    },
+});
+
+// this should be fixed with ExtJS 6.0.2
+// make mousescrolling work in firefox in the containers overflowhandler
+Ext.define(null, {
+    override: 'Ext.layout.container.boxOverflow.Scroller',
+
+    createWheelListener: function() {
+       let me = this;
+       if (Ext.isFirefox) {
+           me.wheelListener = me.layout.innerCt.on('wheel', me.onMouseWheelFirefox, me, { destroyable: true });
+       } else {
+           me.wheelListener = me.layout.innerCt.on('mousewheel', me.onMouseWheel, me, { destroyable: true });
+       }
+    },
+
+    // special wheel handler for firefox. differs from the default onMouseWheel
+    // handler by using deltaY instead of wheelDeltaY and no normalizing,
+    // because it is already
+    onMouseWheelFirefox: function(e) {
+       e.stopEvent();
+       let delta = e.browserEvent.deltaY || 0;
+       this.scrollBy(delta * this.wheelIncrement, false);
+    },
+
+});
+
+// add '@' to the valid id
+Ext.define('Proxmox.validIdReOverride', {
+    override: 'Ext.Component',
+    validIdRe: /^[a-z_][a-z0-9\-_@]*$/i,
+});
+
+// force alert boxes to be rendered with an Error Icon
+// since Ext.Msg is an object and not a prototype, we need to override it
+// after the framework has been initiated
+Ext.onReady(function() {
+/*jslint confusion: true */
+    Ext.override(Ext.Msg, {
+       alert: function(title, message, fn, scope) { // eslint-disable-line consistent-return
+           if (Ext.isString(title)) {
+               let config = {
+                   title: title,
+                   message: message,
+                   icon: this.ERROR,
+                   buttons: this.OK,
+                   fn: fn,
+                   scope: scope,
+                   minWidth: this.minWidth,
+               };
+           return this.show(config);
+           }
+       },
+    });
+/*jslint confusion: false */
+});
+Ext.define('Ext.ux.IFrame', {
+    extend: 'Ext.Component',
+
+    alias: 'widget.uxiframe',
+
+    loadMask: 'Loading...',
+
+    src: 'about:blank',
+
+    renderTpl: [
+        '<iframe src="{src}" id="{id}-iframeEl" data-ref="iframeEl" name="{frameName}" width="100%" height="100%" frameborder="0" allowfullscreen="true"></iframe>',
+    ],
+    childEls: ['iframeEl'],
+
+    initComponent: function() {
+        this.callParent();
+
+        this.frameName = this.frameName || this.id + '-frame';
+    },
+
+    initEvents: function() {
+        let me = this;
+        me.callParent();
+        me.iframeEl.on('load', me.onLoad, me);
+    },
+
+    initRenderData: function() {
+        return Ext.apply(this.callParent(), {
+            src: this.src,
+            frameName: this.frameName,
+        });
+    },
+
+    getBody: function() {
+        let doc = this.getDoc();
+        return doc.body || doc.documentElement;
+    },
+
+    getDoc: function() {
+        try {
+            return this.getWin().document;
+        } catch (ex) {
+            return null;
+        }
+    },
+
+    getWin: function() {
+        let me = this,
+            name = me.frameName,
+            win = Ext.isIE
+                ? me.iframeEl.dom.contentWindow
+                : window.frames[name];
+        return win;
+    },
+
+    getFrame: function() {
+        let me = this;
+        return me.iframeEl.dom;
+    },
+
+    beforeDestroy: function() {
+        this.cleanupListeners(true);
+        this.callParent();
+    },
+
+    cleanupListeners: function(destroying) {
+        let doc, prop;
+
+        if (this.rendered) {
+            try {
+                doc = this.getDoc();
+                if (doc) {
+                    Ext.get(doc).un(this._docListeners);
+                    if (destroying && doc.hasOwnProperty) {
+                        for (prop in doc) {
+                            if (Object.prototype.hasOwnProperty.call(doc, prop)) {
+                                delete doc[prop];
+                            }
+                        }
+                    }
+                }
+            } catch (e) {
+                // do nothing
+            }
+        }
+    },
+
+    onLoad: function() {
+        let me = this,
+            doc = me.getDoc(),
+            fn = me.onRelayedEvent;
+
+        if (doc) {
+            try {
+                // These events need to be relayed from the inner document (where they stop
+                // bubbling) up to the outer document. This has to be done at the DOM level so
+                // the event reaches listeners on elements like the document body. The effected
+                // mechanisms that depend on this bubbling behavior are listed to the right
+                // of the event.
+               /*jslint nomen: true*/
+                Ext.get(doc).on(
+                    me._docListeners = {
+                        mousedown: fn, // menu dismisal (MenuManager) and Window onMouseDown (toFront)
+                        mousemove: fn, // window resize drag detection
+                        mouseup: fn, // window resize termination
+                        click: fn, // not sure, but just to be safe
+                        dblclick: fn, // not sure again
+                        scope: me,
+                    },
+                );
+               /*jslint nomen: false*/
+            } catch (e) {
+                // cannot do this xss
+            }
+
+            // We need to be sure we remove all our events from the iframe on unload or we're going to LEAK!
+            Ext.get(this.getWin()).on('beforeunload', me.cleanupListeners, me);
+
+            this.el.unmask();
+            this.fireEvent('load', this);
+        } else if (me.src) {
+            this.el.unmask();
+            this.fireEvent('error', this);
+        }
+    },
+
+    onRelayedEvent: function(event) {
+        // relay event from the iframe's document to the document that owns the iframe...
+
+        let iframeEl = this.iframeEl,
+
+            // Get the left-based iframe position
+            iframeXY = iframeEl.getTrueXY(),
+            originalEventXY = event.getXY(),
+
+            // Get the left-based XY position.
+            // This is because the consumer of the injected event will
+            // perform its own RTL normalization.
+            eventXY = event.getTrueXY();
+
+        // the event from the inner document has XY relative to that document's origin,
+        // so adjust it to use the origin of the iframe in the outer document:
+        event.xy = [iframeXY[0] + eventXY[0], iframeXY[1] + eventXY[1]];
+
+        event.injectEvent(iframeEl); // blame the iframe for the event...
+
+        event.xy = originalEventXY; // restore the original XY (just for safety)
+    },
+
+    load: function(src) {
+        let me = this,
+            text = me.loadMask,
+            frame = me.getFrame();
+
+        if (me.fireEvent('beforeload', me, src) !== false) {
+            if (text && me.el) {
+                me.el.mask(text);
+            }
+
+            frame.src = me.src = src || me.src;
+        }
+    },
+});
diff --git a/src/Utils.js b/src/Utils.js
new file mode 100644 (file)
index 0000000..c3b13f4
--- /dev/null
@@ -0,0 +1,812 @@
+Ext.ns('Proxmox');
+Ext.ns('Proxmox.Setup');
+
+if (!Ext.isDefined(Proxmox.Setup.auth_cookie_name)) {
+    throw "Proxmox library not initialized";
+}
+
+// avoid errors related to Accessible Rich Internet Applications
+// (access for people with disabilities)
+// TODO reenable after all components are upgraded
+Ext.enableAria = false;
+Ext.enableAriaButtons = false;
+Ext.enableAriaPanels = false;
+
+// avoid errors when running without development tools
+if (!Ext.isDefined(Ext.global.console)) {
+    let console = {
+       dir: function() {
+           // do nothing
+       },
+       log: function() {
+           // do nothing
+       },
+       warn: function() {
+           // do nothing
+       },
+    };
+    Ext.global.console = console;
+}
+
+Ext.Ajax.defaultHeaders = {
+    'Accept': 'application/json',
+};
+
+Ext.Ajax.on('beforerequest', function(conn, options) {
+    if (Proxmox.CSRFPreventionToken) {
+       if (!options.headers) {
+           options.headers = {};
+       }
+       options.headers.CSRFPreventionToken = Proxmox.CSRFPreventionToken;
+    }
+});
+
+Ext.define('Proxmox.Utils', { // a singleton
+utilities: {
+
+    yesText: gettext('Yes'),
+    noText: gettext('No'),
+    enabledText: gettext('Enabled'),
+    disabledText: gettext('Disabled'),
+    noneText: gettext('none'),
+    NoneText: gettext('None'),
+    errorText: gettext('Error'),
+    unknownText: gettext('Unknown'),
+    defaultText: gettext('Default'),
+    daysText: gettext('days'),
+    dayText: gettext('day'),
+    runningText: gettext('running'),
+    stoppedText: gettext('stopped'),
+    neverText: gettext('never'),
+    totalText: gettext('Total'),
+    usedText: gettext('Used'),
+    directoryText: gettext('Directory'),
+    stateText: gettext('State'),
+    groupText: gettext('Group'),
+
+    language_map: {
+       ar: 'Arabic',
+       ca: 'Catalan',
+       da: 'Danish',
+       de: 'German',
+       en: 'English',
+       es: 'Spanish',
+       eu: 'Euskera (Basque)',
+       fa: 'Persian (Farsi)',
+       fr: 'French',
+       he: 'Hebrew',
+       it: 'Italian',
+       ja: 'Japanese',
+       nb: 'Norwegian (Bokmal)',
+       nn: 'Norwegian (Nynorsk)',
+       pl: 'Polish',
+       pt_BR: 'Portuguese (Brazil)',
+       ru: 'Russian',
+       sl: 'Slovenian',
+       sv: 'Swedish',
+       tr: 'Turkish',
+       zh_CN: 'Chinese (Simplified)',
+       zh_TW: 'Chinese (Traditional)',
+    },
+
+    render_language: function(value) {
+       if (!value) {
+           return Proxmox.Utils.defaultText + ' (English)';
+       }
+       let text = Proxmox.Utils.language_map[value];
+       if (text) {
+           return text + ' (' + value + ')';
+       }
+       return value;
+    },
+
+    language_array: function() {
+       let data = [['__default__', Proxmox.Utils.render_language('')]];
+       Ext.Object.each(Proxmox.Utils.language_map, function(key, value) {
+           data.push([key, Proxmox.Utils.render_language(value)]);
+       });
+
+       return data;
+    },
+
+    bond_mode_gettext_map: {
+       '802.3ad': 'LACP (802.3ad)',
+       'lacp-balance-slb': 'LACP (balance-slb)',
+       'lacp-balance-tcp': 'LACP (balance-tcp)',
+    },
+
+    render_bond_mode: value => Proxmox.Utils.bond_mode_gettext_map[value] || value || '',
+
+    bond_mode_array: function(modes) {
+       return modes.map(mode => [mode, Proxmox.Utils.render_bond_mode(mode)]);
+    },
+
+    getNoSubKeyHtml: function(url) {
+       // url http://www.proxmox.com/products/proxmox-ve/subscription-service-plans
+       return Ext.String.format('You do not have a valid subscription for this server. Please visit <a target="_blank" href="{0}">www.proxmox.com</a> to get a list of available options.', url || 'https://www.proxmox.com');
+    },
+
+    format_boolean_with_default: function(value) {
+       if (Ext.isDefined(value) && value !== '__default__') {
+           return value ? Proxmox.Utils.yesText : Proxmox.Utils.noText;
+       }
+       return Proxmox.Utils.defaultText;
+    },
+
+    format_boolean: function(value) {
+       return value ? Proxmox.Utils.yesText : Proxmox.Utils.noText;
+    },
+
+    format_neg_boolean: function(value) {
+       return !value ? Proxmox.Utils.yesText : Proxmox.Utils.noText;
+    },
+
+    format_enabled_toggle: function(value) {
+       return value ? Proxmox.Utils.enabledText : Proxmox.Utils.disabledText;
+    },
+
+    format_expire: function(date) {
+       if (!date) {
+           return Proxmox.Utils.neverText;
+       }
+       return Ext.Date.format(date, "Y-m-d");
+    },
+
+    // somewhat like a human would tell durations, omit zero values and do not
+    // give seconds precision if we talk days already
+    format_duration_human: function(ut) {
+       let seconds = 0, minutes = 0, hours = 0, days = 0;
+
+       if (ut <= 0) {
+           return '0s';
+       }
+
+       let remaining = ut;
+       seconds = Number((remaining % 60).toFixed(1));
+       remaining = Math.trunc(remaining / 60);
+       if (remaining > 0) {
+           minutes = remaining % 60;
+           remaining = Math.trunc(remaining / 60);
+           if (remaining > 0) {
+               hours = remaining % 24;
+               remaining = Math.trunc(remaining / 24);
+               if (remaining > 0) {
+                   days = remaining;
+               }
+           }
+       }
+
+       let res = [];
+       let add = (t, unit) => {
+           if (t > 0) res.push(t + unit);
+           return t > 0;
+       };
+
+       let addSeconds = !add(days, 'd');
+       add(hours, 'h');
+       add(minutes, 'm');
+       if (addSeconds) {
+           add(seconds, 's');
+       }
+       return res.join(' ');
+    },
+
+    format_duration_long: function(ut) {
+       let days = Math.floor(ut / 86400);
+       ut -= days*86400;
+       let hours = Math.floor(ut / 3600);
+       ut -= hours*3600;
+       let mins = Math.floor(ut / 60);
+       ut -= mins*60;
+
+       let hours_str = '00' + hours.toString();
+       hours_str = hours_str.substr(hours_str.length - 2);
+       let mins_str = "00" + mins.toString();
+       mins_str = mins_str.substr(mins_str.length - 2);
+       let ut_str = "00" + ut.toString();
+       ut_str = ut_str.substr(ut_str.length - 2);
+
+       if (days) {
+           let ds = days > 1 ? Proxmox.Utils.daysText : Proxmox.Utils.dayText;
+           return days.toString() + ' ' + ds + ' ' +
+               hours_str + ':' + mins_str + ':' + ut_str;
+       } else {
+           return hours_str + ':' + mins_str + ':' + ut_str;
+       }
+    },
+
+    format_subscription_level: function(level) {
+       if (level === 'c') {
+           return 'Community';
+       } else if (level === 'b') {
+           return 'Basic';
+       } else if (level === 's') {
+           return 'Standard';
+       } else if (level === 'p') {
+           return 'Premium';
+       } else {
+           return Proxmox.Utils.noneText;
+       }
+    },
+
+    compute_min_label_width: function(text, width) {
+       if (width === undefined) { width = 100; }
+
+       let tm = new Ext.util.TextMetrics();
+       let min = tm.getWidth(text + ':');
+
+       return min < width ? width : min;
+    },
+
+    setAuthData: function(data) {
+       Proxmox.CSRFPreventionToken = data.CSRFPreventionToken;
+       Proxmox.UserName = data.username;
+       Proxmox.LoggedOut = data.LoggedOut;
+       // creates a session cookie (expire = null)
+       // that way the cookie gets deleted after the browser window is closed
+       Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, data.ticket, null, '/', null, true);
+    },
+
+    authOK: function() {
+       if (Proxmox.LoggedOut) {
+           return undefined;
+       }
+       let cookie = Ext.util.Cookies.get(Proxmox.Setup.auth_cookie_name);
+       if (Proxmox.UserName !== '' && cookie && !cookie.startsWith("PVE:tfa!")) {
+           return cookie;
+       } else {
+           return false;
+       }
+    },
+
+    authClear: function() {
+       if (Proxmox.LoggedOut) {
+           return;
+       }
+       Ext.util.Cookies.clear(Proxmox.Setup.auth_cookie_name);
+    },
+
+    // comp.setLoading() is buggy in ExtJS 4.0.7, so we
+    // use el.mask() instead
+    setErrorMask: function(comp, msg) {
+       let el = comp.el;
+       if (!el) {
+           return;
+       }
+       if (!msg) {
+           el.unmask();
+       } else if (msg === true) {
+           el.mask(gettext("Loading..."));
+       } else {
+           el.mask(msg);
+       }
+    },
+
+    getResponseErrorMessage: (err) => {
+       if (!err.statusText) {
+           return gettext('Connection error');
+       }
+       let msg = [`${err.statusText} (${err.status})`];
+       if (err.response && err.response.responseText) {
+           let txt = err.response.responseText;
+           try {
+               let res = JSON.parse(txt);
+               if (res.errors && typeof res.errors === 'object') {
+                   for (let [key, value] of Object.entries(res.errors)) {
+                       msg.push(Ext.String.htmlEncode(`${key}: ${value}`));
+                   }
+               }
+           } catch (e) {
+               // fallback to string
+               msg.push(Ext.String.htmlEncode(txt));
+           }
+       }
+       return msg.join('<br>');
+    },
+
+    monStoreErrors: function(component, store, clearMaskBeforeLoad) {
+       if (clearMaskBeforeLoad) {
+           component.mon(store, 'beforeload', function(s, operation, eOpts) {
+               Proxmox.Utils.setErrorMask(component, false);
+           });
+       } else {
+           component.mon(store, 'beforeload', function(s, operation, eOpts) {
+               if (!component.loadCount) {
+                   component.loadCount = 0; // make sure it is nucomponent.ic
+                   Proxmox.Utils.setErrorMask(component, true);
+               }
+           });
+       }
+
+       // only works with 'proxmox' proxy
+       component.mon(store.proxy, 'afterload', function(proxy, request, success) {
+           component.loadCount++;
+
+           if (success) {
+               Proxmox.Utils.setErrorMask(component, false);
+               return;
+           }
+
+           let error = request._operation.getError();
+           let msg = Proxmox.Utils.getResponseErrorMessage(error);
+           Proxmox.Utils.setErrorMask(component, msg);
+       });
+    },
+
+    extractRequestError: function(result, verbose) {
+       let msg = gettext('Successful');
+
+       if (!result.success) {
+           msg = gettext("Unknown error");
+           if (result.message) {
+               msg = result.message;
+               if (result.status) {
+                   msg += ' (' + result.status + ')';
+               }
+           }
+           if (verbose && Ext.isObject(result.errors)) {
+               msg += "<br>";
+               Ext.Object.each(result.errors, function(prop, desc) {
+                   msg += "<br><b>" + Ext.htmlEncode(prop) + "</b>: " +
+                       Ext.htmlEncode(desc);
+               });
+           }
+       }
+
+       return msg;
+    },
+
+    // Ext.Ajax.request
+    API2Request: function(reqOpts) {
+       let newopts = Ext.apply({
+           waitMsg: gettext('Please wait...'),
+       }, reqOpts);
+
+       if (!newopts.url.match(/^\/api2/)) {
+           newopts.url = '/api2/extjs' + newopts.url;
+       }
+       delete newopts.callback;
+
+       let createWrapper = function(successFn, callbackFn, failureFn) {
+           Ext.apply(newopts, {
+               success: function(response, options) {
+                   if (options.waitMsgTarget) {
+                       if (Proxmox.Utils.toolkit === 'touch') {
+                           options.waitMsgTarget.setMasked(false);
+                       } else {
+                           options.waitMsgTarget.setLoading(false);
+                       }
+                   }
+                   let result = Ext.decode(response.responseText);
+                   response.result = result;
+                   if (!result.success) {
+                       response.htmlStatus = Proxmox.Utils.extractRequestError(result, true);
+                       Ext.callback(callbackFn, options.scope, [options, false, response]);
+                       Ext.callback(failureFn, options.scope, [response, options]);
+                       return;
+                   }
+                   Ext.callback(callbackFn, options.scope, [options, true, response]);
+                   Ext.callback(successFn, options.scope, [response, options]);
+               },
+               failure: function(response, options) {
+                   if (options.waitMsgTarget) {
+                       if (Proxmox.Utils.toolkit === 'touch') {
+                           options.waitMsgTarget.setMasked(false);
+                       } else {
+                           options.waitMsgTarget.setLoading(false);
+                       }
+                   }
+                   response.result = {};
+                   try {
+                       response.result = Ext.decode(response.responseText);
+                   } catch (e) {
+                       // ignore
+                   }
+                   let msg = gettext('Connection error') + ' - server offline?';
+                   if (response.aborted) {
+                       msg = gettext('Connection error') + ' - aborted.';
+                   } else if (response.timedout) {
+                       msg = gettext('Connection error') + ' - Timeout.';
+                   } else if (response.status && response.statusText) {
+                       msg = gettext('Connection error') + ' ' + response.status + ': ' + response.statusText;
+                   }
+                   response.htmlStatus = msg;
+                   Ext.callback(callbackFn, options.scope, [options, false, response]);
+                   Ext.callback(failureFn, options.scope, [response, options]);
+               },
+           });
+       };
+
+       createWrapper(reqOpts.success, reqOpts.callback, reqOpts.failure);
+
+       let target = newopts.waitMsgTarget;
+       if (target) {
+           if (Proxmox.Utils.toolkit === 'touch') {
+               target.setMasked({ xtype: 'loadmask', message: newopts.waitMsg });
+           } else {
+               // Note: ExtJS bug - this does not work when component is not rendered
+               target.setLoading(newopts.waitMsg);
+           }
+       }
+       Ext.Ajax.request(newopts);
+    },
+
+    checked_command: function(orig_cmd) {
+       Proxmox.Utils.API2Request({
+           url: '/nodes/localhost/subscription',
+           method: 'GET',
+           failure: function(response, opts) {
+               Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+           },
+           success: function(response, opts) {
+               let data = response.result.data;
+               if (data.status !== 'Active') {
+                   Ext.Msg.show({
+                       title: gettext('No valid subscription'),
+                       icon: Ext.Msg.WARNING,
+                       message: Proxmox.Utils.getNoSubKeyHtml(data.url),
+                       buttons: Ext.Msg.OK,
+                       callback: function(btn) {
+                           if (btn !== 'ok') {
+                               return;
+                           }
+                           orig_cmd();
+                       },
+                   });
+               } else {
+                   orig_cmd();
+               }
+           },
+       });
+    },
+
+    assemble_field_data: function(values, data) {
+        if (!Ext.isObject(data)) {
+           return;
+       }
+       Ext.Object.each(data, function(name, val) {
+           if (Object.prototype.hasOwnProperty.call(values, name)) {
+               let bucket = values[name];
+               if (!Ext.isArray(bucket)) {
+                   bucket = values[name] = [bucket];
+               }
+               if (Ext.isArray(val)) {
+                   values[name] = bucket.concat(val);
+               } else {
+                   bucket.push(val);
+               }
+           } else {
+               values[name] = val;
+           }
+       });
+    },
+
+    updateColumnWidth: function(container) {
+       let mode = Ext.state.Manager.get('summarycolumns') || 'auto';
+       let factor;
+       if (mode !== 'auto') {
+           factor = parseInt(mode, 10);
+           if (Number.isNaN(factor)) {
+               factor = 1;
+           }
+       } else {
+           factor = container.getSize().width < 1600 ? 1 : 2;
+       }
+
+       if (container.oldFactor === factor) {
+           return;
+       }
+
+       let items = container.query('>'); // direct childs
+       factor = Math.min(factor, items.length);
+       container.oldFactor = factor;
+
+       items.forEach((item) => {
+           item.columnWidth = 1 / factor;
+       });
+
+       // we have to update the layout twice, since the first layout change
+       // can trigger the scrollbar which reduces the amount of space left
+       container.updateLayout();
+       container.updateLayout();
+    },
+
+    dialog_title: function(subject, create, isAdd) {
+       if (create) {
+           if (isAdd) {
+               return gettext('Add') + ': ' + subject;
+           } else {
+               return gettext('Create') + ': ' + subject;
+           }
+       } else {
+           return gettext('Edit') + ': ' + subject;
+       }
+    },
+
+    network_iface_types: {
+       eth: gettext("Network Device"),
+       bridge: 'Linux Bridge',
+       bond: 'Linux Bond',
+       vlan: 'Linux VLAN',
+       OVSBridge: 'OVS Bridge',
+       OVSBond: 'OVS Bond',
+       OVSPort: 'OVS Port',
+       OVSIntPort: 'OVS IntPort',
+    },
+
+    render_network_iface_type: function(value) {
+       return Proxmox.Utils.network_iface_types[value] ||
+           Proxmox.Utils.unknownText;
+    },
+
+    task_desc_table: {
+       acmenewcert: ['SRV', gettext('Order Certificate')],
+       acmeregister: ['ACME Account', gettext('Register')],
+       acmedeactivate: ['ACME Account', gettext('Deactivate')],
+       acmeupdate: ['ACME Account', gettext('Update')],
+       acmerefresh: ['ACME Account', gettext('Refresh')],
+       acmerenew: ['SRV', gettext('Renew Certificate')],
+       acmerevoke: ['SRV', gettext('Revoke Certificate')],
+       'auth-realm-sync': [gettext('Realm'), gettext('Sync')],
+       'auth-realm-sync-test': [gettext('Realm'), gettext('Sync Preview')],
+       'move_volume': ['CT', gettext('Move Volume')],
+       clustercreate: ['', gettext('Create Cluster')],
+       clusterjoin: ['', gettext('Join Cluster')],
+       diskinit: ['Disk', gettext('Initialize Disk with GPT')],
+       vncproxy: ['VM/CT', gettext('Console')],
+       spiceproxy: ['VM/CT', gettext('Console') + ' (Spice)'],
+       vncshell: ['', gettext('Shell')],
+       spiceshell: ['', gettext('Shell') + ' (Spice)'],
+       qmsnapshot: ['VM', gettext('Snapshot')],
+       qmrollback: ['VM', gettext('Rollback')],
+       qmdelsnapshot: ['VM', gettext('Delete Snapshot')],
+       qmcreate: ['VM', gettext('Create')],
+       qmrestore: ['VM', gettext('Restore')],
+       qmdestroy: ['VM', gettext('Destroy')],
+       qmigrate: ['VM', gettext('Migrate')],
+       qmclone: ['VM', gettext('Clone')],
+       qmmove: ['VM', gettext('Move disk')],
+       qmtemplate: ['VM', gettext('Convert to template')],
+       qmstart: ['VM', gettext('Start')],
+       qmstop: ['VM', gettext('Stop')],
+       qmreset: ['VM', gettext('Reset')],
+       qmshutdown: ['VM', gettext('Shutdown')],
+       qmreboot: ['VM', gettext('Reboot')],
+       qmsuspend: ['VM', gettext('Hibernate')],
+       qmpause: ['VM', gettext('Pause')],
+       qmresume: ['VM', gettext('Resume')],
+       qmconfig: ['VM', gettext('Configure')],
+       vzsnapshot: ['CT', gettext('Snapshot')],
+       vzrollback: ['CT', gettext('Rollback')],
+       vzdelsnapshot: ['CT', gettext('Delete Snapshot')],
+       vzcreate: ['CT', gettext('Create')],
+       vzrestore: ['CT', gettext('Restore')],
+       vzdestroy: ['CT', gettext('Destroy')],
+       vzmigrate: ['CT', gettext('Migrate')],
+       vzclone: ['CT', gettext('Clone')],
+       vztemplate: ['CT', gettext('Convert to template')],
+       vzstart: ['CT', gettext('Start')],
+       vzstop: ['CT', gettext('Stop')],
+       vzmount: ['CT', gettext('Mount')],
+       vzumount: ['CT', gettext('Unmount')],
+       vzshutdown: ['CT', gettext('Shutdown')],
+       vzreboot: ['CT', gettext('Reboot')],
+       vzsuspend: ['CT', gettext('Suspend')],
+       vzresume: ['CT', gettext('Resume')],
+       push_file: ['CT', gettext('Push file')],
+       pull_file: ['CT', gettext('Pull file')],
+       hamigrate: ['HA', gettext('Migrate')],
+       hastart: ['HA', gettext('Start')],
+       hastop: ['HA', gettext('Stop')],
+       hashutdown: ['HA', gettext('Shutdown')],
+       srvstart: ['SRV', gettext('Start')],
+       srvstop: ['SRV', gettext('Stop')],
+       srvrestart: ['SRV', gettext('Restart')],
+       srvreload: ['SRV', gettext('Reload')],
+       cephcreatemgr: ['Ceph Manager', gettext('Create')],
+       cephdestroymgr: ['Ceph Manager', gettext('Destroy')],
+       cephcreatemon: ['Ceph Monitor', gettext('Create')],
+       cephdestroymon: ['Ceph Monitor', gettext('Destroy')],
+       cephcreateosd: ['Ceph OSD', gettext('Create')],
+       cephdestroyosd: ['Ceph OSD', gettext('Destroy')],
+       cephcreatepool: ['Ceph Pool', gettext('Create')],
+       cephdestroypool: ['Ceph Pool', gettext('Destroy')],
+       cephfscreate: ['CephFS', gettext('Create')],
+       cephcreatemds: ['Ceph Metadata Server', gettext('Create')],
+       cephdestroymds: ['Ceph Metadata Server', gettext('Destroy')],
+       imgcopy: ['', gettext('Copy data')],
+       imgdel: ['', gettext('Erase data')],
+       unknownimgdel: ['', gettext('Destroy image from unknown guest')],
+       download: ['', gettext('Download')],
+       vzdump: (type, id) => id ? `VM/CT ${id} - ${gettext('Backup')}` : gettext('Backup Job'),
+       aptupdate: ['', gettext('Update package database')],
+       startall: ['', gettext('Start all VMs and Containers')],
+       stopall: ['', gettext('Stop all VMs and Containers')],
+       migrateall: ['', gettext('Migrate all VMs and Containers')],
+       dircreate: [gettext('Directory Storage'), gettext('Create')],
+       lvmcreate: [gettext('LVM Storage'), gettext('Create')],
+       lvmthincreate: [gettext('LVM-Thin Storage'), gettext('Create')],
+       zfscreate: [gettext('ZFS Storage'), gettext('Create')],
+    },
+
+    // to add or change existing for product specific ones
+    override_task_descriptions: function(extra) {
+       for (const [key, value] of Object.entries(extra)) {
+           Proxmox.Utils.task_desc_table[key] = value;
+       }
+    },
+
+    format_task_description: function(type, id) {
+       let farray = Proxmox.Utils.task_desc_table[type];
+       let text;
+       if (!farray) {
+           text = type;
+           if (id) {
+               type += ' ' + id;
+           }
+           return text;
+       } else if (Ext.isFunction(farray)) {
+           return farray(type, id);
+       }
+       let prefix = farray[0];
+       text = farray[1];
+       if (prefix && id !== undefined) {
+           return prefix + ' ' + id + ' - ' + text;
+       }
+       return text;
+    },
+
+    format_size: function(size) {
+       let units = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'];
+       let num = 0;
+       while (size >= 1024 && num++ <= units.length) {
+           size = size / 1024;
+       }
+
+       return size.toFixed(num > 0?2:0) + " " + units[num] + "B";
+    },
+
+    render_upid: function(value, metaData, record) {
+       let task = record.data;
+       let type = task.type || task.worker_type;
+       let id = task.id || task.worker_id;
+
+       return Proxmox.Utils.format_task_description(type, id);
+    },
+
+    render_uptime: function(value) {
+       let uptime = value;
+
+       if (uptime === undefined) {
+           return '';
+       }
+
+       if (uptime <= 0) {
+           return '-';
+       }
+
+       return Proxmox.Utils.format_duration_long(uptime);
+    },
+
+    parse_task_upid: function(upid) {
+       let task = {};
+
+       let res = upid.match(/^UPID:([^\s:]+):([0-9A-Fa-f]{8}):([0-9A-Fa-f]{8,9}):(([0-9A-Fa-f]{8,16}):)?([0-9A-Fa-f]{8}):([^:\s]+):([^:\s]*):([^:\s]+):$/);
+       if (!res) {
+           throw "unable to parse upid '" + upid + "'";
+       }
+       task.node = res[1];
+       task.pid = parseInt(res[2], 16);
+       task.pstart = parseInt(res[3], 16);
+       if (res[5] !== undefined) {
+           task.task_id = parseInt(res[5], 16);
+       }
+       task.starttime = parseInt(res[6], 16);
+       task.type = res[7];
+       task.id = res[8];
+       task.user = res[9];
+
+       task.desc = Proxmox.Utils.format_task_description(task.type, task.id);
+
+       return task;
+    },
+
+    render_duration: function(value) {
+       if (value === undefined) {
+           return '-';
+       }
+       return Proxmox.Utils.format_duration_human(value);
+    },
+
+    render_timestamp: function(value, metaData, record, rowIndex, colIndex, store) {
+       let servertime = new Date(value * 1000);
+       return Ext.Date.format(servertime, 'Y-m-d H:i:s');
+    },
+
+    get_help_info: function(section) {
+       let helpMap;
+       if (typeof proxmoxOnlineHelpInfo !== 'undefined') {
+           helpMap = proxmoxOnlineHelpInfo; // eslint-disable-line no-undef
+       } else if (typeof pveOnlineHelpInfo !== 'undefined') {
+           // be backward compatible with older pve-doc-generators
+           helpMap = pveOnlineHelpInfo; // eslint-disable-line no-undef
+       } else {
+           throw "no global OnlineHelpInfo map declared";
+       }
+
+       return helpMap[section];
+    },
+
+    get_help_link: function(section) {
+       let info = Proxmox.Utils.get_help_info(section);
+       if (!info) {
+           return undefined;
+       }
+       return window.location.origin + info.link;
+    },
+
+    openXtermJsViewer: function(vmtype, vmid, nodename, vmname, cmd) {
+       let url = Ext.Object.toQueryString({
+           console: vmtype, // kvm, lxc, upgrade or shell
+           xtermjs: 1,
+           vmid: vmid,
+           vmname: vmname,
+           node: nodename,
+           cmd: cmd,
+
+       });
+       let nw = window.open("?" + url, '_blank', 'toolbar=no,location=no,status=no,menubar=no,resizable=yes,width=800,height=420');
+       if (nw) {
+           nw.focus();
+       }
+    },
+
+},
+
+    singleton: true,
+    constructor: function() {
+       let me = this;
+       Ext.apply(me, me.utilities);
+
+       let IPV4_OCTET = "(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])";
+       let IPV4_REGEXP = "(?:(?:" + IPV4_OCTET + "\\.){3}" + IPV4_OCTET + ")";
+       let IPV6_H16 = "(?:[0-9a-fA-F]{1,4})";
+       let IPV6_LS32 = "(?:(?:" + IPV6_H16 + ":" + IPV6_H16 + ")|" + IPV4_REGEXP + ")";
+       let IPV4_CIDR_MASK = "([0-9]{1,2})";
+       let IPV6_CIDR_MASK = "([0-9]{1,3})";
+
+
+       me.IP4_match = new RegExp("^(?:" + IPV4_REGEXP + ")$");
+       me.IP4_cidr_match = new RegExp("^(?:" + IPV4_REGEXP + ")/" + IPV4_CIDR_MASK + "$");
+
+       /* eslint-disable no-useless-concat,no-multi-spaces */
+       let IPV6_REGEXP = "(?:" +
+           "(?:(?:"                                                  + "(?:" + IPV6_H16 + ":){6})" + IPV6_LS32 + ")|" +
+           "(?:(?:"                                         +   "::" + "(?:" + IPV6_H16 + ":){5})" + IPV6_LS32 + ")|" +
+           "(?:(?:(?:"                           + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){4})" + IPV6_LS32 + ")|" +
+           "(?:(?:(?:(?:" + IPV6_H16 + ":){0,1}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){3})" + IPV6_LS32 + ")|" +
+           "(?:(?:(?:(?:" + IPV6_H16 + ":){0,2}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){2})" + IPV6_LS32 + ")|" +
+           "(?:(?:(?:(?:" + IPV6_H16 + ":){0,3}" + IPV6_H16 + ")?::" + "(?:" + IPV6_H16 + ":){1})" + IPV6_LS32 + ")|" +
+           "(?:(?:(?:(?:" + IPV6_H16 + ":){0,4}" + IPV6_H16 + ")?::" +                         ")" + IPV6_LS32 + ")|" +
+           "(?:(?:(?:(?:" + IPV6_H16 + ":){0,5}" + IPV6_H16 + ")?::" +                         ")" + IPV6_H16  + ")|" +
+           "(?:(?:(?:(?:" + IPV6_H16 + ":){0,7}" + IPV6_H16 + ")?::" +                         ")"             + ")"  +
+           ")";
+       /* eslint-enable no-useless-concat,no-multi-spaces */
+
+       me.IP6_match = new RegExp("^(?:" + IPV6_REGEXP + ")$");
+       me.IP6_cidr_match = new RegExp("^(?:" + IPV6_REGEXP + ")/" + IPV6_CIDR_MASK + "$");
+       me.IP6_bracket_match = new RegExp("^\\[(" + IPV6_REGEXP + ")\\]");
+
+       me.IP64_match = new RegExp("^(?:" + IPV6_REGEXP + "|" + IPV4_REGEXP + ")$");
+       me.IP64_cidr_match = new RegExp("^(?:" + IPV6_REGEXP + "/" + IPV6_CIDR_MASK + ")|(?:" + IPV4_REGEXP + "/" + IPV4_CIDR_MASK + ")$");
+
+       let DnsName_REGEXP = "(?:(([a-zA-Z0-9]([a-zA-Z0-9\\-]*[a-zA-Z0-9])?)\\.)*([A-Za-z0-9]([A-Za-z0-9\\-]*[A-Za-z0-9])?))";
+       me.DnsName_match = new RegExp("^" + DnsName_REGEXP + "$");
+
+       me.HostPort_match = new RegExp("^(" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")(:\\d+)?$");
+       me.HostPortBrackets_match = new RegExp("^\\[(?:" + IPV6_REGEXP + "|" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")\\](:\\d+)?$");
+       me.IP6_dotnotation_match = new RegExp("^" + IPV6_REGEXP + "(\\.\\d+)?$");
+       me.Vlan_match = /^vlan(\\d+)/;
+       me.VlanInterface_match = /(\\w+)\\.(\\d+)/;
+    },
+});
diff --git a/src/button/Button.js b/src/button/Button.js
new file mode 100644 (file)
index 0000000..93c773c
--- /dev/null
@@ -0,0 +1,171 @@
+/* Button features:
+ * - observe selection changes to enable/disable the button using enableFn()
+ * - pop up confirmation dialog using confirmMsg()
+ */
+Ext.define('Proxmox.button.Button', {
+    extend: 'Ext.button.Button',
+    alias: 'widget.proxmoxButton',
+
+    // the selection model to observe
+    selModel: undefined,
+
+    // if 'false' handler will not be called (button disabled)
+    enableFn: function(record) {
+       // return undefined by default
+    },
+
+    // function(record) or text
+    confirmMsg: false,
+
+    // take special care in confirm box (select no as default).
+    dangerous: false,
+
+    // is used to get the parent container for its selection model
+    parentXType: 'grid',
+
+    initComponent: function() {
+        let me = this;
+
+       if (me.handler) {
+           // Note: me.realHandler may be a string (see named scopes)
+           let realHandler = me.handler;
+
+           me.handler = function(button, event) {
+               let rec, msg;
+               if (me.selModel) {
+                   rec = me.selModel.getSelection()[0];
+                   if (!rec || me.enableFn(rec) === false) {
+                       return;
+                   }
+               }
+
+               if (me.confirmMsg) {
+                   msg = me.confirmMsg;
+                   if (Ext.isFunction(me.confirmMsg)) {
+                       msg = me.confirmMsg(rec);
+                   }
+                   Ext.MessageBox.defaultButton = me.dangerous ? 2 : 1;
+                   Ext.Msg.show({
+                       title: gettext('Confirm'),
+                       icon: me.dangerous ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
+                       message: msg,
+                       buttons: Ext.Msg.YESNO,
+                       defaultFocus: me.dangerous ? 'no' : 'yes',
+                       callback: function(btn) {
+                           if (btn !== 'yes') {
+                               return;
+                           }
+                           Ext.callback(realHandler, me.scope, [button, event, rec], 0, me);
+                       },
+                   });
+               } else {
+                   Ext.callback(realHandler, me.scope, [button, event, rec], 0, me);
+               }
+           };
+       }
+
+       me.callParent();
+
+       let grid;
+       if (!me.selModel && me.selModel !== null && me.selModel !== false) {
+           let parent = me.up(me.parentXType);
+           if (parent && parent.selModel) {
+               me.selModel = parent.selModel;
+           }
+       }
+
+       if (me.waitMsgTarget === true) {
+           grid = me.up('grid');
+           if (grid) {
+               me.waitMsgTarget = grid;
+           } else {
+               throw "unable to find waitMsgTarget";
+           }
+       }
+
+       if (me.selModel) {
+           me.mon(me.selModel, "selectionchange", function() {
+               let rec = me.selModel.getSelection()[0];
+               if (!rec || me.enableFn(rec) === false) {
+                   me.setDisabled(true);
+               } else {
+                   me.setDisabled(false);
+               }
+           });
+       }
+    },
+});
+
+
+Ext.define('Proxmox.button.StdRemoveButton', {
+    extend: 'Proxmox.button.Button',
+    alias: 'widget.proxmoxStdRemoveButton',
+
+    text: gettext('Remove'),
+
+    disabled: true,
+
+    // time to wait for removal task to finish
+    delay: undefined,
+
+    config: {
+       baseurl: undefined,
+    },
+
+    getUrl: function(rec) {
+       let me = this;
+
+       if (me.selModel) {
+           return me.baseurl + '/' + rec.getId();
+       } else {
+           return me.baseurl;
+       }
+    },
+
+    // also works with names scopes
+    callback: function(options, success, response) {
+       // do nothing by default
+    },
+
+    getRecordName: (rec) => rec.getId(),
+
+    confirmMsg: function(rec) {
+       let me = this;
+
+       let name = me.getRecordName(rec);
+       return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), `'${name}'`);
+    },
+
+    handler: function(btn, event, rec) {
+       let me = this;
+
+       let url = me.getUrl(rec);
+
+       if (typeof me.delay !== 'undefined' && me.delay >= 0) {
+           url += "?delay=" + me.delay;
+       }
+
+       Proxmox.Utils.API2Request({
+           url: url,
+           method: 'DELETE',
+           waitMsgTarget: me.waitMsgTarget,
+           callback: function(options, success, response) {
+               Ext.callback(me.callback, me.scope, [options, success, response], 0, me);
+           },
+           failure: function(response, opts) {
+               Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+           },
+       });
+    },
+    initComponent: function() {
+       let me = this;
+
+       // enable by default if no seleModel is there and disabled not set
+       if (me.initialConfig.disabled === undefined &&
+           (me.selModel === null || me.selModel === false)) {
+           me.disabled = false;
+       }
+
+       me.callParent();
+    },
+});
diff --git a/src/button/HelpButton.js b/src/button/HelpButton.js
new file mode 100644 (file)
index 0000000..d97a6c4
--- /dev/null
@@ -0,0 +1,86 @@
+/* help button pointing to an online documentation
+   for components contained in a modal window
+*/
+Ext.define('Proxmox.button.Help', {
+    extend: 'Ext.button.Button',
+    xtype: 'proxmoxHelpButton',
+
+    text: gettext('Help'),
+
+    // make help button less flashy by styling it like toolbar buttons
+    iconCls: ' x-btn-icon-el-default-toolbar-small fa fa-question-circle',
+    cls: 'x-btn-default-toolbar-small proxmox-inline-button',
+
+    hidden: true,
+
+    listenToGlobalEvent: true,
+
+    controller: {
+       xclass: 'Ext.app.ViewController',
+       listen: {
+           global: {
+               proxmoxShowHelp: 'onProxmoxShowHelp',
+               proxmoxHideHelp: 'onProxmoxHideHelp',
+           },
+       },
+       onProxmoxShowHelp: function(helpLink) {
+           let view = this.getView();
+           if (view.listenToGlobalEvent === true) {
+               view.setOnlineHelp(helpLink);
+               view.show();
+           }
+       },
+       onProxmoxHideHelp: function() {
+           let view = this.getView();
+           if (view.listenToGlobalEvent === true) {
+               view.hide();
+           }
+       },
+    },
+
+    // this sets the link and the tooltip text
+    setOnlineHelp: function(blockid) {
+       let me = this;
+
+       let info = Proxmox.Utils.get_help_info(blockid);
+       if (info) {
+           me.onlineHelp = blockid;
+           let title = info.title;
+           if (info.subtitle) {
+               title += ' - ' + info.subtitle;
+           }
+           me.setTooltip(title);
+       }
+    },
+
+    // helper to set the onlineHelp via a config object
+    setHelpConfig: function(config) {
+       let me = this;
+       me.setOnlineHelp(config.onlineHelp);
+    },
+
+    handler: function() {
+       let me = this;
+       let docsURI;
+
+       if (me.onlineHelp) {
+           docsURI = Proxmox.Utils.get_help_link(me.onlineHelp);
+       }
+
+       if (docsURI) {
+           window.open(docsURI);
+       } else {
+           Ext.Msg.alert(gettext('Help'), gettext('No Help available'));
+       }
+    },
+
+    initComponent: function() {
+       let me = this;
+
+       me.callParent();
+
+       if (me.onlineHelp) {
+           me.setOnlineHelp(me.onlineHelp); // set tooltip
+       }
+    },
+});
diff --git a/src/css/Makefile b/src/css/Makefile
new file mode 100644 (file)
index 0000000..5054b55
--- /dev/null
@@ -0,0 +1,13 @@
+include ../defines.mk
+
+CSS=ext6-pmx.css
+
+all:
+
+.PHONY: install
+install: ${CSS}
+       install -d ${WWWCSSDIR}
+       for i in ${CSS}; do install -m 0755 $$i ${WWWCSSDIR}/$$i; done
+
+.PHONY: clean
+clean:
diff --git a/src/css/ext6-pmx.css b/src/css/ext6-pmx.css
new file mode 100644 (file)
index 0000000..a7d446b
--- /dev/null
@@ -0,0 +1,50 @@
+.pmx-clear-trigger {
+    background-image: url(../images/pmx-clear-trigger.png);
+}
+
+.pmx-hint {
+    background-color: LightYellow;
+}
+
+.x-mask-msg-text {
+    text-align: center;
+}
+
+.proxmox-invalid-row {
+    background-color: #f3d6d7;
+}
+
+/* some icons have to be color manually */
+.black {
+    color: #000;
+}
+
+.normal {
+    color: #c2ddf2;
+}
+
+.faded {
+    color: #cfcfcf;
+}
+
+.good {
+    color: #21BF4B;
+}
+
+.warning {
+    color: #fc0;
+}
+
+.critical {
+    color: #FF6C59;
+}
+
+/* reduce chart legend space usage to something more sane */
+.x-legend-item {
+       padding: 0.4em 0.8em 0.4em 1.8em;
+}
+
+.x-legend-item-marker {
+       left: 0.5em;
+       top: 0.6em;
+}
diff --git a/src/data/DiffStore.js b/src/data/DiffStore.js
new file mode 100644 (file)
index 0000000..e4143f5
--- /dev/null
@@ -0,0 +1,140 @@
+/*
+ * The DiffStore is a in-memory store acting as proxy between a real store
+ * instance and a component.
+ * Its purpose is to redisplay the component *only* if the data has been changed
+ * inside the real store, to avoid the annoying visual flickering of using
+ * the real store directly.
+ *
+ * Implementation:
+ * The DiffStore monitors via mon() the 'load' events sent by the real store.
+ * On each 'load' event, the DiffStore compares its own content with the target
+ * store (call to cond_add_item()) and then fires a 'refresh' event.
+ * The 'refresh' event will automatically trigger a view refresh on the component
+ * who binds to this store.
+ */
+
+/* Config properties:
+ * rstore: the realstore which will autorefresh its content from the API
+ * Only works if rstore has a model and use 'idProperty'
+ * sortAfterUpdate: sort the diffstore before rendering the view
+ */
+Ext.define('Proxmox.data.DiffStore', {
+    extend: 'Ext.data.Store',
+    alias: 'store.diff',
+
+    sortAfterUpdate: false,
+
+    // if true, destroy rstore on destruction. Defaults to true if a rstore
+    // config is passed instead of an existing rstore instance
+    autoDestroyRstore: false,
+
+    onDestroy: function() {
+       let me = this;
+       if (me.autoDestroyRstore) {
+           if (Ext.isFunction(me.rstore.destroy)) {
+               me.rstore.destroy();
+           }
+           delete me.rstore;
+       }
+       me.callParent();
+    },
+
+    constructor: function(config) {
+       let me = this;
+
+       config = config || {};
+
+       if (!config.rstore) {
+           throw "no rstore specified";
+       }
+
+       if (!config.rstore.model) {
+           throw "no rstore model specified";
+       }
+
+       let rstore;
+       if (config.rstore.isInstance) {
+           rstore = config.rstore;
+       } else if (config.rstore.type) {
+           Ext.applyIf(config.rstore, {
+               autoDestroyRstore: true,
+           });
+           rstore = Ext.create(`store.${config.rstore.type}`, config.rstore);
+       } else {
+           throw 'rstore is not an instance, and cannot autocreate without "type"';
+       }
+
+       Ext.apply(config, {
+           model: rstore.model,
+           proxy: { type: 'memory' },
+       });
+
+       me.callParent([config]);
+
+       me.rstore = rstore;
+
+       let first_load = true;
+
+       let cond_add_item = function(data, id) {
+           let olditem = me.getById(id);
+           if (olditem) {
+               olditem.beginEdit();
+               Ext.Array.each(me.model.prototype.fields, function(field) {
+                   if (olditem.data[field.name] !== data[field.name]) {
+                       olditem.set(field.name, data[field.name]);
+                   }
+               });
+               olditem.endEdit(true);
+               olditem.commit();
+           } else {
+               let newrec = Ext.create(me.model, data);
+               let pos = me.appendAtStart && !first_load ? 0 : me.data.length;
+               me.insert(pos, newrec);
+           }
+       };
+
+       let loadFn = function(s, records, success) {
+           if (!success) {
+               return;
+           }
+
+           me.suspendEvents();
+
+           // getSource returns null if data is not filtered
+           // if it is filtered it returns all records
+           let allItems = me.getData().getSource() || me.getData();
+
+           // remove vanished items
+           allItems.each(function(olditem) {
+               let item = me.rstore.getById(olditem.getId());
+               if (!item) {
+                   me.remove(olditem);
+               }
+           });
+
+           me.rstore.each(function(item) {
+               cond_add_item(item.data, item.getId());
+           });
+
+           me.filter();
+
+           if (me.sortAfterUpdate) {
+               me.sort();
+           }
+
+           first_load = false;
+
+           me.resumeEvents();
+           me.fireEvent('refresh', me);
+           me.fireEvent('datachanged', me);
+       };
+
+       if (me.rstore.isLoaded()) {
+           // if store is already loaded,
+           // insert items instantly
+           loadFn(me.rstore, [], true);
+       }
+
+       me.mon(me.rstore, 'load', loadFn);
+    },
+});
diff --git a/src/data/ObjectStore.js b/src/data/ObjectStore.js
new file mode 100644 (file)
index 0000000..860cbfd
--- /dev/null
@@ -0,0 +1,45 @@
+/* This store encapsulates data items which are organized as an Array of key-values Objects
+ * ie data[0] contains something like {key: "keyboard", value: "da"}
+*
+* Designed to work with the KeyValue model and the JsonObject data reader
+*/
+Ext.define('Proxmox.data.ObjectStore', {
+    extend: 'Proxmox.data.UpdateStore',
+
+    getRecord: function() {
+       let me = this;
+       let record = Ext.create('Ext.data.Model');
+       me.getData().each(function(item) {
+           record.set(item.data.key, item.data.value);
+       });
+       record.commit(true);
+       return record;
+    },
+
+    constructor: function(config) {
+       let me = this;
+
+        config = config || {};
+
+       if (!config.storeid) {
+           config.storeid = 'proxmox-store-' + ++Ext.idSeed;
+       }
+
+        Ext.applyIf(config, {
+           model: 'KeyValue',
+            proxy: {
+                type: 'proxmox',
+               url: config.url,
+               extraParams: config.extraParams,
+                reader: {
+                   type: 'jsonobject',
+                   rows: config.rows,
+                   readArray: config.readArray,
+                   rootProperty: config.root || 'data',
+               },
+            },
+        });
+
+        me.callParent([config]);
+    },
+});
diff --git a/src/data/ProxmoxProxy.js b/src/data/ProxmoxProxy.js
new file mode 100644 (file)
index 0000000..53e92f3
--- /dev/null
@@ -0,0 +1,74 @@
+Ext.define('Proxmox.RestProxy', {
+    extend: 'Ext.data.RestProxy',
+    alias: 'proxy.proxmox',
+
+    pageParam: null,
+    startParam: null,
+    limitParam: null,
+    groupParam: null,
+    sortParam: null,
+    filterParam: null,
+    noCache: false,
+
+    afterRequest: function(request, success) {
+       this.fireEvent('afterload', this, request, success);
+    },
+
+    constructor: function(config) {
+       Ext.applyIf(config, {
+           reader: {
+               type: 'json',
+               rootProperty: config.root || 'data',
+           },
+       });
+
+       this.callParent([config]);
+    },
+}, function() {
+    Ext.define('KeyValue', {
+       extend: "Ext.data.Model",
+       fields: ['key', 'value'],
+       idProperty: 'key',
+    });
+
+    Ext.define('KeyValuePendingDelete', {
+       extend: "Ext.data.Model",
+       fields: ['key', 'value', 'pending', 'delete'],
+       idProperty: 'key',
+    });
+
+    Ext.define('proxmox-tasks', {
+       extend: 'Ext.data.Model',
+       fields: [
+           { name: 'starttime', type: 'date', dateFormat: 'timestamp' },
+           { name: 'endtime', type: 'date', dateFormat: 'timestamp' },
+           { name: 'pid', type: 'int' },
+           'node', 'upid', 'user', 'status', 'type', 'id',
+       ],
+       idProperty: 'upid',
+    });
+
+    Ext.define('proxmox-cluster-log', {
+       extend: 'Ext.data.Model',
+       fields: [
+           { name: 'uid', type: 'int' },
+           { name: 'time', type: 'date', dateFormat: 'timestamp' },
+           { name: 'pri', type: 'int' },
+           { name: 'pid', type: 'int' },
+           'node', 'user', 'tag', 'msg',
+           {
+               name: 'id',
+               convert: function(value, record) {
+                   let info = record.data;
+
+                   if (value) {
+                       return value;
+                   }
+                   // compute unique ID
+                   return info.uid + ':' + info.node;
+               },
+           },
+       ],
+       idProperty: 'id',
+    });
+});
diff --git a/src/data/RRDStore.js b/src/data/RRDStore.js
new file mode 100644 (file)
index 0000000..67ffb57
--- /dev/null
@@ -0,0 +1,77 @@
+/* Extends the Proxmox.data.UpdateStore type
+ *
+ *
+ */
+Ext.define('Proxmox.data.RRDStore', {
+    extend: 'Proxmox.data.UpdateStore',
+    alias: 'store.proxmoxRRDStore',
+
+    setRRDUrl: function(timeframe, cf) {
+       let me = this;
+       if (!timeframe) {
+           timeframe = me.timeframe;
+       }
+
+       if (!cf) {
+           cf = me.cf;
+       }
+
+       me.proxy.url = me.rrdurl + "?timeframe=" + timeframe + "&cf=" + cf;
+    },
+
+    proxy: {
+       type: 'proxmox',
+    },
+
+    timeframe: 'hour',
+
+    cf: 'AVERAGE',
+
+    constructor: function(config) {
+       let me = this;
+
+       config = config || {};
+
+       // set default interval to 30seconds
+       if (!config.interval) {
+           config.interval = 30000;
+       }
+
+       // set a new storeid
+       if (!config.storeid) {
+           config.storeid = 'rrdstore-' + ++Ext.idSeed;
+       }
+
+       // rrdurl is required
+       if (!config.rrdurl) {
+           throw "no rrdurl specified";
+       }
+
+       let stateid = 'proxmoxRRDTypeSelection';
+       let sp = Ext.state.Manager.getProvider();
+       let stateinit = sp.get(stateid);
+
+        if (stateinit) {
+           if (stateinit.timeframe !== me.timeframe || stateinit.cf !== me.rrdcffn) {
+               me.timeframe = stateinit.timeframe;
+               me.rrdcffn = stateinit.cf;
+           }
+       }
+
+       me.callParent([config]);
+
+       me.setRRDUrl();
+       me.mon(sp, 'statechange', function(prov, key, state) {
+           if (key === stateid) {
+               if (state && state.id) {
+                   if (state.timeframe !== me.timeframe || state.cf !== me.cf) {
+                       me.timeframe = state.timeframe;
+                       me.cf = state.cf;
+                       me.setRRDUrl();
+                       me.reload();
+                   }
+               }
+           }
+       });
+    },
+});
diff --git a/src/data/TimezoneStore.js b/src/data/TimezoneStore.js
new file mode 100644 (file)
index 0000000..a67ad8b
--- /dev/null
@@ -0,0 +1,419 @@
+Ext.define('Timezone', {
+    extend: 'Ext.data.Model',
+    fields: ['zone'],
+});
+
+Ext.define('Proxmox.data.TimezoneStore', {
+    extend: 'Ext.data.Store',
+    model: 'Timezone',
+    data: [
+           ['Africa/Abidjan'],
+           ['Africa/Accra'],
+           ['Africa/Addis_Ababa'],
+           ['Africa/Algiers'],
+           ['Africa/Asmara'],
+           ['Africa/Bamako'],
+           ['Africa/Bangui'],
+           ['Africa/Banjul'],
+           ['Africa/Bissau'],
+           ['Africa/Blantyre'],
+           ['Africa/Brazzaville'],
+           ['Africa/Bujumbura'],
+           ['Africa/Cairo'],
+           ['Africa/Casablanca'],
+           ['Africa/Ceuta'],
+           ['Africa/Conakry'],
+           ['Africa/Dakar'],
+           ['Africa/Dar_es_Salaam'],
+           ['Africa/Djibouti'],
+           ['Africa/Douala'],
+           ['Africa/El_Aaiun'],
+           ['Africa/Freetown'],
+           ['Africa/Gaborone'],
+           ['Africa/Harare'],
+           ['Africa/Johannesburg'],
+           ['Africa/Kampala'],
+           ['Africa/Khartoum'],
+           ['Africa/Kigali'],
+           ['Africa/Kinshasa'],
+           ['Africa/Lagos'],
+           ['Africa/Libreville'],
+           ['Africa/Lome'],
+           ['Africa/Luanda'],
+           ['Africa/Lubumbashi'],
+           ['Africa/Lusaka'],
+           ['Africa/Malabo'],
+           ['Africa/Maputo'],
+           ['Africa/Maseru'],
+           ['Africa/Mbabane'],
+           ['Africa/Mogadishu'],
+           ['Africa/Monrovia'],
+           ['Africa/Nairobi'],
+           ['Africa/Ndjamena'],
+           ['Africa/Niamey'],
+           ['Africa/Nouakchott'],
+           ['Africa/Ouagadougou'],
+           ['Africa/Porto-Novo'],
+           ['Africa/Sao_Tome'],
+           ['Africa/Tripoli'],
+           ['Africa/Tunis'],
+           ['Africa/Windhoek'],
+           ['America/Adak'],
+           ['America/Anchorage'],
+           ['America/Anguilla'],
+           ['America/Antigua'],
+           ['America/Araguaina'],
+           ['America/Argentina/Buenos_Aires'],
+           ['America/Argentina/Catamarca'],
+           ['America/Argentina/Cordoba'],
+           ['America/Argentina/Jujuy'],
+           ['America/Argentina/La_Rioja'],
+           ['America/Argentina/Mendoza'],
+           ['America/Argentina/Rio_Gallegos'],
+           ['America/Argentina/Salta'],
+           ['America/Argentina/San_Juan'],
+           ['America/Argentina/San_Luis'],
+           ['America/Argentina/Tucuman'],
+           ['America/Argentina/Ushuaia'],
+           ['America/Aruba'],
+           ['America/Asuncion'],
+           ['America/Atikokan'],
+           ['America/Bahia'],
+           ['America/Bahia_Banderas'],
+           ['America/Barbados'],
+           ['America/Belem'],
+           ['America/Belize'],
+           ['America/Blanc-Sablon'],
+           ['America/Boa_Vista'],
+           ['America/Bogota'],
+           ['America/Boise'],
+           ['America/Cambridge_Bay'],
+           ['America/Campo_Grande'],
+           ['America/Cancun'],
+           ['America/Caracas'],
+           ['America/Cayenne'],
+           ['America/Cayman'],
+           ['America/Chicago'],
+           ['America/Chihuahua'],
+           ['America/Costa_Rica'],
+           ['America/Cuiaba'],
+           ['America/Curacao'],
+           ['America/Danmarkshavn'],
+           ['America/Dawson'],
+           ['America/Dawson_Creek'],
+           ['America/Denver'],
+           ['America/Detroit'],
+           ['America/Dominica'],
+           ['America/Edmonton'],
+           ['America/Eirunepe'],
+           ['America/El_Salvador'],
+           ['America/Fortaleza'],
+           ['America/Glace_Bay'],
+           ['America/Godthab'],
+           ['America/Goose_Bay'],
+           ['America/Grand_Turk'],
+           ['America/Grenada'],
+           ['America/Guadeloupe'],
+           ['America/Guatemala'],
+           ['America/Guayaquil'],
+           ['America/Guyana'],
+           ['America/Halifax'],
+           ['America/Havana'],
+           ['America/Hermosillo'],
+           ['America/Indiana/Indianapolis'],
+           ['America/Indiana/Knox'],
+           ['America/Indiana/Marengo'],
+           ['America/Indiana/Petersburg'],
+           ['America/Indiana/Tell_City'],
+           ['America/Indiana/Vevay'],
+           ['America/Indiana/Vincennes'],
+           ['America/Indiana/Winamac'],
+           ['America/Inuvik'],
+           ['America/Iqaluit'],
+           ['America/Jamaica'],
+           ['America/Juneau'],
+           ['America/Kentucky/Louisville'],
+           ['America/Kentucky/Monticello'],
+           ['America/La_Paz'],
+           ['America/Lima'],
+           ['America/Los_Angeles'],
+           ['America/Maceio'],
+           ['America/Managua'],
+           ['America/Manaus'],
+           ['America/Marigot'],
+           ['America/Martinique'],
+           ['America/Matamoros'],
+           ['America/Mazatlan'],
+           ['America/Menominee'],
+           ['America/Merida'],
+           ['America/Mexico_City'],
+           ['America/Miquelon'],
+           ['America/Moncton'],
+           ['America/Monterrey'],
+           ['America/Montevideo'],
+           ['America/Montreal'],
+           ['America/Montserrat'],
+           ['America/Nassau'],
+           ['America/New_York'],
+           ['America/Nipigon'],
+           ['America/Nome'],
+           ['America/Noronha'],
+           ['America/North_Dakota/Center'],
+           ['America/North_Dakota/New_Salem'],
+           ['America/Ojinaga'],
+           ['America/Panama'],
+           ['America/Pangnirtung'],
+           ['America/Paramaribo'],
+           ['America/Phoenix'],
+           ['America/Port-au-Prince'],
+           ['America/Port_of_Spain'],
+           ['America/Porto_Velho'],
+           ['America/Puerto_Rico'],
+           ['America/Rainy_River'],
+           ['America/Rankin_Inlet'],
+           ['America/Recife'],
+           ['America/Regina'],
+           ['America/Resolute'],
+           ['America/Rio_Branco'],
+           ['America/Santa_Isabel'],
+           ['America/Santarem'],
+           ['America/Santiago'],
+           ['America/Santo_Domingo'],
+           ['America/Sao_Paulo'],
+           ['America/Scoresbysund'],
+           ['America/Shiprock'],
+           ['America/St_Barthelemy'],
+           ['America/St_Johns'],
+           ['America/St_Kitts'],
+           ['America/St_Lucia'],
+           ['America/St_Thomas'],
+           ['America/St_Vincent'],
+           ['America/Swift_Current'],
+           ['America/Tegucigalpa'],
+           ['America/Thule'],
+           ['America/Thunder_Bay'],
+           ['America/Tijuana'],
+           ['America/Toronto'],
+           ['America/Tortola'],
+           ['America/Vancouver'],
+           ['America/Whitehorse'],
+           ['America/Winnipeg'],
+           ['America/Yakutat'],
+           ['America/Yellowknife'],
+           ['Antarctica/Casey'],
+           ['Antarctica/Davis'],
+           ['Antarctica/DumontDUrville'],
+           ['Antarctica/Macquarie'],
+           ['Antarctica/Mawson'],
+           ['Antarctica/McMurdo'],
+           ['Antarctica/Palmer'],
+           ['Antarctica/Rothera'],
+           ['Antarctica/South_Pole'],
+           ['Antarctica/Syowa'],
+           ['Antarctica/Vostok'],
+           ['Arctic/Longyearbyen'],
+           ['Asia/Aden'],
+           ['Asia/Almaty'],
+           ['Asia/Amman'],
+           ['Asia/Anadyr'],
+           ['Asia/Aqtau'],
+           ['Asia/Aqtobe'],
+           ['Asia/Ashgabat'],
+           ['Asia/Baghdad'],
+           ['Asia/Bahrain'],
+           ['Asia/Baku'],
+           ['Asia/Bangkok'],
+           ['Asia/Beirut'],
+           ['Asia/Bishkek'],
+           ['Asia/Brunei'],
+           ['Asia/Choibalsan'],
+           ['Asia/Chongqing'],
+           ['Asia/Colombo'],
+           ['Asia/Damascus'],
+           ['Asia/Dhaka'],
+           ['Asia/Dili'],
+           ['Asia/Dubai'],
+           ['Asia/Dushanbe'],
+           ['Asia/Gaza'],
+           ['Asia/Harbin'],
+           ['Asia/Ho_Chi_Minh'],
+           ['Asia/Hong_Kong'],
+           ['Asia/Hovd'],
+           ['Asia/Irkutsk'],
+           ['Asia/Jakarta'],
+           ['Asia/Jayapura'],
+           ['Asia/Jerusalem'],
+           ['Asia/Kabul'],
+           ['Asia/Kamchatka'],
+           ['Asia/Karachi'],
+           ['Asia/Kashgar'],
+           ['Asia/Kathmandu'],
+           ['Asia/Kolkata'],
+           ['Asia/Krasnoyarsk'],
+           ['Asia/Kuala_Lumpur'],
+           ['Asia/Kuching'],
+           ['Asia/Kuwait'],
+           ['Asia/Macau'],
+           ['Asia/Magadan'],
+           ['Asia/Makassar'],
+           ['Asia/Manila'],
+           ['Asia/Muscat'],
+           ['Asia/Nicosia'],
+           ['Asia/Novokuznetsk'],
+           ['Asia/Novosibirsk'],
+           ['Asia/Omsk'],
+           ['Asia/Oral'],
+           ['Asia/Phnom_Penh'],
+           ['Asia/Pontianak'],
+           ['Asia/Pyongyang'],
+           ['Asia/Qatar'],
+           ['Asia/Qyzylorda'],
+           ['Asia/Rangoon'],
+           ['Asia/Riyadh'],
+           ['Asia/Sakhalin'],
+           ['Asia/Samarkand'],
+           ['Asia/Seoul'],
+           ['Asia/Shanghai'],
+           ['Asia/Singapore'],
+           ['Asia/Taipei'],
+           ['Asia/Tashkent'],
+           ['Asia/Tbilisi'],
+           ['Asia/Tehran'],
+           ['Asia/Thimphu'],
+           ['Asia/Tokyo'],
+           ['Asia/Ulaanbaatar'],
+           ['Asia/Urumqi'],
+           ['Asia/Vientiane'],
+           ['Asia/Vladivostok'],
+           ['Asia/Yakutsk'],
+           ['Asia/Yekaterinburg'],
+           ['Asia/Yerevan'],
+           ['Atlantic/Azores'],
+           ['Atlantic/Bermuda'],
+           ['Atlantic/Canary'],
+           ['Atlantic/Cape_Verde'],
+           ['Atlantic/Faroe'],
+           ['Atlantic/Madeira'],
+           ['Atlantic/Reykjavik'],
+           ['Atlantic/South_Georgia'],
+           ['Atlantic/St_Helena'],
+           ['Atlantic/Stanley'],
+           ['Australia/Adelaide'],
+           ['Australia/Brisbane'],
+           ['Australia/Broken_Hill'],
+           ['Australia/Currie'],
+           ['Australia/Darwin'],
+           ['Australia/Eucla'],
+           ['Australia/Hobart'],
+           ['Australia/Lindeman'],
+           ['Australia/Lord_Howe'],
+           ['Australia/Melbourne'],
+           ['Australia/Perth'],
+           ['Australia/Sydney'],
+           ['Europe/Amsterdam'],
+           ['Europe/Andorra'],
+           ['Europe/Athens'],
+           ['Europe/Belgrade'],
+           ['Europe/Berlin'],
+           ['Europe/Bratislava'],
+           ['Europe/Brussels'],
+           ['Europe/Bucharest'],
+           ['Europe/Budapest'],
+           ['Europe/Chisinau'],
+           ['Europe/Copenhagen'],
+           ['Europe/Dublin'],
+           ['Europe/Gibraltar'],
+           ['Europe/Guernsey'],
+           ['Europe/Helsinki'],
+           ['Europe/Isle_of_Man'],
+           ['Europe/Istanbul'],
+           ['Europe/Jersey'],
+           ['Europe/Kaliningrad'],
+           ['Europe/Kiev'],
+           ['Europe/Lisbon'],
+           ['Europe/Ljubljana'],
+           ['Europe/London'],
+           ['Europe/Luxembourg'],
+           ['Europe/Madrid'],
+           ['Europe/Malta'],
+           ['Europe/Mariehamn'],
+           ['Europe/Minsk'],
+           ['Europe/Monaco'],
+           ['Europe/Moscow'],
+           ['Europe/Oslo'],
+           ['Europe/Paris'],
+           ['Europe/Podgorica'],
+           ['Europe/Prague'],
+           ['Europe/Riga'],
+           ['Europe/Rome'],
+           ['Europe/Samara'],
+           ['Europe/San_Marino'],
+           ['Europe/Sarajevo'],
+           ['Europe/Simferopol'],
+           ['Europe/Skopje'],
+           ['Europe/Sofia'],
+           ['Europe/Stockholm'],
+           ['Europe/Tallinn'],
+           ['Europe/Tirane'],
+           ['Europe/Uzhgorod'],
+           ['Europe/Vaduz'],
+           ['Europe/Vatican'],
+           ['Europe/Vienna'],
+           ['Europe/Vilnius'],
+           ['Europe/Volgograd'],
+           ['Europe/Warsaw'],
+           ['Europe/Zagreb'],
+           ['Europe/Zaporozhye'],
+           ['Europe/Zurich'],
+           ['Indian/Antananarivo'],
+           ['Indian/Chagos'],
+           ['Indian/Christmas'],
+           ['Indian/Cocos'],
+           ['Indian/Comoro'],
+           ['Indian/Kerguelen'],
+           ['Indian/Mahe'],
+           ['Indian/Maldives'],
+           ['Indian/Mauritius'],
+           ['Indian/Mayotte'],
+           ['Indian/Reunion'],
+           ['Pacific/Apia'],
+           ['Pacific/Auckland'],
+           ['Pacific/Chatham'],
+           ['Pacific/Chuuk'],
+           ['Pacific/Easter'],
+           ['Pacific/Efate'],
+           ['Pacific/Enderbury'],
+           ['Pacific/Fakaofo'],
+           ['Pacific/Fiji'],
+           ['Pacific/Funafuti'],
+           ['Pacific/Galapagos'],
+           ['Pacific/Gambier'],
+           ['Pacific/Guadalcanal'],
+           ['Pacific/Guam'],
+           ['Pacific/Honolulu'],
+           ['Pacific/Johnston'],
+           ['Pacific/Kiritimati'],
+           ['Pacific/Kosrae'],
+           ['Pacific/Kwajalein'],
+           ['Pacific/Majuro'],
+           ['Pacific/Marquesas'],
+           ['Pacific/Midway'],
+           ['Pacific/Nauru'],
+           ['Pacific/Niue'],
+           ['Pacific/Norfolk'],
+           ['Pacific/Noumea'],
+           ['Pacific/Pago_Pago'],
+           ['Pacific/Palau'],
+           ['Pacific/Pitcairn'],
+           ['Pacific/Pohnpei'],
+           ['Pacific/Port_Moresby'],
+           ['Pacific/Rarotonga'],
+           ['Pacific/Saipan'],
+           ['Pacific/Tahiti'],
+           ['Pacific/Tarawa'],
+           ['Pacific/Tongatapu'],
+           ['Pacific/Wake'],
+           ['Pacific/Wallis'],
+           ['UTC'],
+       ],
+});
diff --git a/src/data/UpdateStore.js b/src/data/UpdateStore.js
new file mode 100644 (file)
index 0000000..be85e4f
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * Extends the Ext.data.Store type with  startUpdate() and stopUpdate() methods
+ * to refresh the store data in the background.
+ * Components using this store directly will flicker due to the redisplay of
+ * the element ater 'config.interval' ms.
+ *
+ * Note that you have to set 'autoStart' or call startUpdate() once yourself
+ * for the background load to begin.
+ */
+Ext.define('Proxmox.data.UpdateStore', {
+    extend: 'Ext.data.Store',
+    alias: 'store.update',
+
+    config: {
+       interval: 3000,
+
+       isStopped: true,
+
+       autoStart: false,
+    },
+
+    destroy: function() {
+       let me = this;
+       me.stopUpdate();
+       me.callParent();
+    },
+
+    constructor: function(config) {
+       let me = this;
+
+       config = config || {};
+       if (config.interval === undefined) {
+           delete config.interval;
+       }
+
+       if (!config.storeid) {
+           throw "no storeid specified";
+       }
+
+       let load_task = new Ext.util.DelayedTask();
+
+       let run_load_task = function() {
+           if (me.getIsStopped()) {
+               return;
+           }
+
+           if (Proxmox.Utils.authOK()) {
+               let start = new Date();
+               me.load(function() {
+                   let runtime = new Date() - start;
+                   let interval = me.getInterval() + runtime*2;
+                   load_task.delay(interval, run_load_task);
+               });
+           } else {
+               load_task.delay(200, run_load_task);
+           }
+       };
+
+       Ext.apply(config, {
+           startUpdate: function() {
+               me.setIsStopped(false);
+               // run_load_task(); this makes problems with chrome
+               load_task.delay(1, run_load_task);
+           },
+           stopUpdate: function() {
+               me.setIsStopped(true);
+               load_task.cancel();
+           },
+       });
+
+       me.callParent([config]);
+
+       me.load_task = load_task;
+
+       if (me.getAutoStart()) {
+           me.startUpdate();
+       }
+    },
+});
diff --git a/src/data/model/Realm.js b/src/data/model/Realm.js
new file mode 100644 (file)
index 0000000..dce270d
--- /dev/null
@@ -0,0 +1,29 @@
+Ext.define('pmx-domains', {
+    extend: "Ext.data.Model",
+    fields: [
+       'realm', 'type', 'comment', 'default',
+       {
+           name: 'tfa',
+           allowNull: true,
+       },
+       {
+           name: 'descr',
+           convert: function(value, { data={} }) {
+               if (value) return Ext.String.htmlEncode(value);
+
+               let text = data.comment || data.realm;
+
+               if (data.tfa) {
+                   text += ` (+ ${data.tfa})`;
+               }
+
+               return Ext.String.htmlEncode(text);
+           },
+       },
+    ],
+    idProperty: 'realm',
+    proxy: {
+       type: 'proxmox',
+       url: "/api2/json/access/domains",
+    },
+});
diff --git a/src/data/reader/JsonObject.js b/src/data/reader/JsonObject.js
new file mode 100644 (file)
index 0000000..0489df8
--- /dev/null
@@ -0,0 +1,123 @@
+/* A reader to store a single JSON Object (hash) into a storage.
+ * Also accepts an array containing a single hash.
+ *
+ * So it can read:
+ *
+ * example1: {data1: "xyz", data2: "abc"}
+ * returns [{key: "data1", value: "xyz"}, {key: "data2", value: "abc"}]
+ *
+ * example2: [ {data1: "xyz", data2: "abc"} ]
+ * returns [{key: "data1", value: "xyz"}, {key: "data2", value: "abc"}]
+ *
+ * If you set 'readArray', the reader expexts the object as array:
+ *
+ * example3: [ { key: "data1", value: "xyz", p2: "cde" },  { key: "data2", value: "abc", p2: "efg" }]
+ * returns [{key: "data1", value: "xyz", p2: "cde}, {key: "data2", value: "abc", p2: "efg"}]
+ *
+ * Note: The records can contain additional properties (like 'p2' above) when you use 'readArray'
+ *
+ * Additional feature: specify allowed properties with default values with 'rows' object
+ *
+ * let rows = {
+ *   memory: {
+ *     required: true,
+ *     defaultValue: 512
+ *   }
+ * }
+ *
+ */
+
+Ext.define('Proxmox.data.reader.JsonObject', {
+    extend: 'Ext.data.reader.Json',
+    alias: 'reader.jsonobject',
+
+    readArray: false,
+
+    rows: undefined,
+
+    constructor: function(config) {
+        let me = this;
+
+        Ext.apply(me, config || {});
+
+       me.callParent([config]);
+    },
+
+    getResponseData: function(response) {
+       let me = this;
+
+       let data = [];
+        try {
+        let result = Ext.decode(response.responseText);
+        // get our data items inside the server response
+        let root = result[me.getRootProperty()];
+
+           if (me.readArray) {
+               let rec_hash = {};
+               Ext.Array.each(root, function(rec) {
+                   if (Ext.isDefined(rec.key)) {
+                       rec_hash[rec.key] = rec;
+                   }
+               });
+
+               if (me.rows) {
+                   Ext.Object.each(me.rows, function(key, rowdef) {
+                       let rec = rec_hash[key];
+                       if (Ext.isDefined(rec)) {
+                           if (!Ext.isDefined(rec.value)) {
+                               rec.value = rowdef.defaultValue;
+                           }
+                           data.push(rec);
+                       } else if (Ext.isDefined(rowdef.defaultValue)) {
+                           data.push({ key: key, value: rowdef.defaultValue });
+                       } else if (rowdef.required) {
+                           data.push({ key: key, value: undefined });
+                       }
+                   });
+               } else {
+                   Ext.Array.each(root, function(rec) {
+                       if (Ext.isDefined(rec.key)) {
+                           data.push(rec);
+                       }
+                   });
+               }
+           } else {
+               let org_root = root;
+
+               if (Ext.isArray(org_root)) {
+                   if (root.length === 1) {
+                       root = org_root[0];
+                   } else {
+                       root = {};
+                   }
+               }
+
+               if (me.rows) {
+                   Ext.Object.each(me.rows, function(key, rowdef) {
+                       if (Ext.isDefined(root[key])) {
+                           data.push({ key: key, value: root[key] });
+                       } else if (Ext.isDefined(rowdef.defaultValue)) {
+                           data.push({ key: key, value: rowdef.defaultValue });
+                       } else if (rowdef.required) {
+                           data.push({ key: key, value: undefined });
+                       }
+                   });
+               } else {
+                   Ext.Object.each(root, function(key, value) {
+                       data.push({ key: key, value: value });
+                   });
+               }
+           }
+       } catch (ex) {
+            Ext.Error.raise({
+                response: response,
+                json: response.responseText,
+                parseError: ex,
+                msg: 'Unable to parse the JSON returned by the server: ' + ex.toString(),
+            });
+        }
+
+       return data;
+    },
+});
+
diff --git a/src/defines.mk b/src/defines.mk
new file mode 100644 (file)
index 0000000..c07a4e1
--- /dev/null
@@ -0,0 +1,7 @@
+-include /usr/share/dpkg/pkg-info.mk
+
+DESTDIR=
+DOCDIR=${DESTDIR}/usr/share/doc/${PACKAGE}
+WWWBASEDIR=${DESTDIR}/usr/share/javascript/${PACKAGE}
+WWWCSSDIR=${WWWBASEDIR}/css
+WWWIMAGESDIR=${WWWBASEDIR}/images
diff --git a/src/form/BondModeSelector.js b/src/form/BondModeSelector.js
new file mode 100644 (file)
index 0000000..1fe45b9
--- /dev/null
@@ -0,0 +1,42 @@
+Ext.define('Proxmox.form.BondModeSelector', {
+    extend: 'Proxmox.form.KVComboBox',
+    alias: ['widget.bondModeSelector'],
+
+    openvswitch: false,
+
+    initComponent: function() {
+       let me = this;
+
+       if (me.openvswitch) {
+           me.comboItems = Proxmox.Utils.bond_mode_array([
+              'active-backup',
+              'balance-slb',
+              'lacp-balance-slb',
+              'lacp-balance-tcp',
+           ]);
+       } else {
+           me.comboItems = Proxmox.Utils.bond_mode_array([
+               'balance-rr',
+               'active-backup',
+               'balance-xor',
+               'broadcast',
+               '802.3ad',
+               'balance-tlb',
+               'balance-alb',
+           ]);
+       }
+
+       me.callParent();
+    },
+});
+
+Ext.define('Proxmox.form.BondPolicySelector', {
+    extend: 'Proxmox.form.KVComboBox',
+    alias: ['widget.bondPolicySelector'],
+    comboItems: [
+           ['layer2', 'layer2'],
+           ['layer2+3', 'layer2+3'],
+           ['layer3+4', 'layer3+4'],
+    ],
+});
+
diff --git a/src/form/Checkbox.js b/src/form/Checkbox.js
new file mode 100644 (file)
index 0000000..2b29e1c
--- /dev/null
@@ -0,0 +1,45 @@
+Ext.define('Proxmox.form.Checkbox', {
+    extend: 'Ext.form.field.Checkbox',
+    alias: ['widget.proxmoxcheckbox'],
+
+    config: {
+       defaultValue: undefined,
+       deleteDefaultValue: false,
+       deleteEmpty: false,
+    },
+
+    inputValue: '1',
+
+    getSubmitData: function() {
+        let me = this,
+            data = null,
+            val;
+        if (!me.disabled && me.submitValue) {
+            val = me.getSubmitValue();
+            if (val !== null) {
+                data = {};
+               if (val === me.getDefaultValue() && me.getDeleteDefaultValue()) {
+                   data.delete = me.getName();
+               } else {
+                    data[me.getName()] = val;
+               }
+            } else if (me.getDeleteEmpty()) {
+               data = {};
+               data.delete = me.getName();
+           }
+        }
+        return data;
+    },
+
+    // also accept integer 1 as true
+    setRawValue: function(value) {
+       let me = this;
+
+       if (value === 1) {
+            me.callParent([true]);
+       } else {
+            me.callParent([value]);
+       }
+    },
+
+});
diff --git a/src/form/ComboGrid.js b/src/form/ComboGrid.js
new file mode 100644 (file)
index 0000000..e5a1920
--- /dev/null
@@ -0,0 +1,457 @@
+/*
+ * ComboGrid component: a ComboBox where the dropdown menu (the
+ * "Picker") is a Grid with Rows and Columns expects a listConfig
+ * object with a columns property roughly based on the GridPicker from
+ * https://www.sencha.com/forum/showthread.php?299909
+ *
+*/
+
+Ext.define('Proxmox.form.ComboGrid', {
+    extend: 'Ext.form.field.ComboBox',
+    alias: ['widget.proxmoxComboGrid'],
+
+    // this value is used as default value after load()
+    preferredValue: undefined,
+
+    // hack: allow to select empty value
+    // seems extjs does not allow that when 'editable == false'
+    onKeyUp: function(e, t) {
+        let me = this;
+        let key = e.getKey();
+
+        if (!me.editable && me.allowBlank && !me.multiSelect &&
+           (key === e.BACKSPACE || key === e.DELETE)) {
+           me.setValue('');
+       }
+
+        me.callParent(arguments);
+    },
+
+    config: {
+       skipEmptyText: false,
+       notFoundIsValid: false,
+       deleteEmpty: false,
+    },
+
+    // needed to trigger onKeyUp etc.
+    enableKeyEvents: true,
+
+    editable: false,
+
+    triggers: {
+       clear: {
+           cls: 'pmx-clear-trigger',
+           weight: -1,
+           hidden: true,
+           handler: function() {
+               let me = this;
+               me.setValue('');
+           },
+       },
+    },
+
+    setValue: function(value) {
+       let me = this;
+       let empty = Ext.isArray(value) ? !value.length : !value;
+       me.triggers.clear.setVisible(!empty && me.allowBlank);
+       return me.callParent([value]);
+    },
+
+    // override ExtJS method
+    // if the field has multiSelect enabled, the store is not loaded, and
+    // the displayfield == valuefield, it saves the rawvalue as an array
+    // but the getRawValue method is only defined in the textfield class
+    // (which has not to deal with arrays) an returns the string in the
+    // field (not an array)
+    //
+    // so if we have multiselect enabled, return the rawValue (which
+    // should be an array) and else we do callParent so
+    // it should not impact any other use of the class
+    getRawValue: function() {
+       let me = this;
+       if (me.multiSelect) {
+           return me.rawValue;
+       } else {
+           return me.callParent();
+       }
+    },
+
+    getSubmitData: function() {
+       let me = this;
+
+       let data = null;
+       if (!me.disabled && me.submitValue) {
+           let val = me.getSubmitValue();
+           if (val !== null) {
+               data = {};
+               data[me.getName()] = val;
+           } else if (me.getDeleteEmpty()) {
+               data = {};
+               data.delete = me.getName();
+           }
+       }
+       return data;
+   },
+
+    getSubmitValue: function() {
+       let me = this;
+
+       let value = me.callParent();
+       if (value !== '') {
+           return value;
+       }
+
+       return me.getSkipEmptyText() ? null: value;
+    },
+
+    setAllowBlank: function(allowBlank) {
+       this.allowBlank = allowBlank;
+       this.validate();
+    },
+
+// override ExtJS protected method
+    onBindStore: function(store, initial) {
+       let me = this,
+           picker = me.picker,
+           extraKeySpec,
+           valueCollectionConfig;
+
+       // We're being bound, not unbound...
+       if (store) {
+           // If store was created from a 2 dimensional array with generated field names 'field1' and 'field2'
+           if (store.autoCreated) {
+               me.queryMode = 'local';
+               me.valueField = me.displayField = 'field1';
+               if (!store.expanded) {
+                   me.displayField = 'field2';
+               }
+
+               // displayTpl config will need regenerating with the autogenerated displayField name 'field1'
+               me.setDisplayTpl(null);
+           }
+           if (!Ext.isDefined(me.valueField)) {
+               me.valueField = me.displayField;
+           }
+
+           // Add a byValue index to the store so that we can efficiently look up records by the value field
+           // when setValue passes string value(s).
+           // The two indices (Ext.util.CollectionKeys) are configured unique: false, so that if duplicate keys
+           // are found, they are all returned by the get call.
+           // This is so that findByText and findByValue are able to return the *FIRST* matching value. By default,
+           // if unique is true, CollectionKey keeps the *last* matching value.
+           extraKeySpec = {
+               byValue: {
+                   rootProperty: 'data',
+                   unique: false,
+               },
+           };
+           extraKeySpec.byValue.property = me.valueField;
+           store.setExtraKeys(extraKeySpec);
+
+           if (me.displayField === me.valueField) {
+               store.byText = store.byValue;
+           } else {
+               extraKeySpec.byText = {
+                   rootProperty: 'data',
+                   unique: false,
+               };
+               extraKeySpec.byText.property = me.displayField;
+               store.setExtraKeys(extraKeySpec);
+           }
+
+           // We hold a collection of the values which have been selected, keyed by this field's valueField.
+           // This collection also functions as the selected items collection for the BoundList's selection model
+           valueCollectionConfig = {
+               rootProperty: 'data',
+               extraKeys: {
+                   byInternalId: {
+                       property: 'internalId',
+                   },
+                   byValue: {
+                       property: me.valueField,
+                       rootProperty: 'data',
+                   },
+               },
+               // Whenever this collection is changed by anyone, whether by this field adding to it,
+               // or the BoundList operating, we must refresh our value.
+               listeners: {
+                   beginupdate: me.onValueCollectionBeginUpdate,
+                   endupdate: me.onValueCollectionEndUpdate,
+                   scope: me,
+               },
+           };
+
+           // This becomes our collection of selected records for the Field.
+           me.valueCollection = new Ext.util.Collection(valueCollectionConfig);
+
+           // We use the selected Collection as our value collection and the basis
+           // for rendering the tag list.
+
+           //proxmox override: since the picker is represented by a grid panel,
+           // we changed here the selection to RowModel
+           me.pickerSelectionModel = new Ext.selection.RowModel({
+               mode: me.multiSelect ? 'SIMPLE' : 'SINGLE',
+               // There are situations when a row is selected on mousedown but then the mouse is
+               // dragged to another row and released.  In these situations, the event target for
+               // the click event won't be the row where the mouse was released but the boundview.
+               // The view will then determine that it should fire a container click, and the
+               // DataViewModel will then deselect all prior selections. Setting
+               // `deselectOnContainerClick` here will prevent the model from deselecting.
+               deselectOnContainerClick: false,
+               enableInitialSelection: false,
+               pruneRemoved: false,
+               selected: me.valueCollection,
+               store: store,
+               listeners: {
+                   scope: me,
+                   lastselectedchanged: me.updateBindSelection,
+               },
+           });
+
+           if (!initial) {
+               me.resetToDefault();
+           }
+
+           if (picker) {
+               picker.setSelectionModel(me.pickerSelectionModel);
+               if (picker.getStore() !== store) {
+                   picker.bindStore(store);
+               }
+           }
+       }
+    },
+
+    // copied from ComboBox
+    createPicker: function() {
+        let me = this;
+        let picker;
+
+        let pickerCfg = Ext.apply({
+                // proxmox overrides: display a grid for selection
+                xtype: 'gridpanel',
+                id: me.pickerId,
+                pickerField: me,
+                floating: true,
+                hidden: true,
+                store: me.store,
+                displayField: me.displayField,
+                preserveScrollOnRefresh: true,
+                pageSize: me.pageSize,
+                tpl: me.tpl,
+                selModel: me.pickerSelectionModel,
+                focusOnToFront: false,
+            }, me.listConfig, me.defaultListConfig);
+
+        picker = me.picker || Ext.widget(pickerCfg);
+
+        if (picker.getStore() !== me.store) {
+            picker.bindStore(me.store);
+        }
+
+        if (me.pageSize) {
+            picker.pagingToolbar.on('beforechange', me.onPageChange, me);
+        }
+
+        // proxmox overrides: pass missing method in gridPanel to its view
+        picker.refresh = function() {
+            picker.getSelectionModel().select(me.valueCollection.getRange());
+            picker.getView().refresh();
+        };
+        picker.getNodeByRecord = function() {
+            picker.getView().getNodeByRecord(arguments);
+        };
+
+        // We limit the height of the picker to fit in the space above
+        // or below this field unless the picker has its own ideas about that.
+        if (!picker.initialConfig.maxHeight) {
+            picker.on({
+                beforeshow: me.onBeforePickerShow,
+                scope: me,
+            });
+        }
+        picker.getSelectionModel().on({
+            beforeselect: me.onBeforeSelect,
+            beforedeselect: me.onBeforeDeselect,
+            focuschange: me.onFocusChange,
+            selectionChange: function(sm, selectedRecords) {
+                if (selectedRecords.length) {
+                    this.setValue(selectedRecords);
+                    this.fireEvent('select', me, selectedRecords);
+                }
+            },
+            scope: me,
+        });
+
+       // hack for extjs6
+       // when the clicked item is the same as the previously selected,
+       // it does not select the item
+       // instead we hide the picker
+       if (!me.multiSelect) {
+           picker.on('itemclick', function(sm, record) {
+               if (picker.getSelection()[0] === record) {
+                   picker.hide();
+               }
+           });
+       }
+
+       // when our store is not yet loaded, we increase
+       // the height of the gridpanel, so that we can see
+       // the loading mask
+       //
+       // we save the minheight to reset it after the load
+       picker.on('show', function() {
+           if (me.enableLoadMask) {
+               me.savedMinHeight = picker.getMinHeight();
+               picker.setMinHeight(100);
+           }
+       });
+
+        picker.getNavigationModel().navigateOnSpace = false;
+
+        return picker;
+    },
+
+    clearLocalFilter: function() {
+        let me = this,
+            filter = me.queryFilter;
+
+        if (filter) {
+            me.queryFilter = null;
+            me.changingFilters = true;
+            me.store.removeFilter(filter, true);
+            me.changingFilters = false;
+        }
+    },
+
+    isValueInStore: function(value) {
+       let me = this;
+       let store = me.store;
+       let found = false;
+
+       if (!store) {
+           return found;
+       }
+
+       // Make sure the current filter is removed before checking the store
+       // to prevent false negative results when iterating over a filtered store.
+       // All store.find*() method's operate on the filtered store.
+       if (me.queryFilter && me.queryMode === 'local' && me.clearFilterOnBlur) {
+           me.clearLocalFilter();
+       }
+
+       if (Ext.isArray(value)) {
+           Ext.Array.each(value, function(v) {
+               if (store.findRecord(me.valueField, v)) {
+                   found = true;
+                   return false; // break
+               }
+               return true;
+           });
+       } else {
+           found = !!store.findRecord(me.valueField, value);
+       }
+
+       return found;
+    },
+
+    validator: function(value) {
+       let me = this;
+
+       if (!value) {
+           return true; // handled later by allowEmpty in the getErrors call chain
+       }
+
+       // we normally get here the displayField as value, but if a valueField
+       // is configured we need to get the "actual" value, to ensure it is in
+       // the store. Below check is copied from ExtJS 6.0.2 ComboBox source
+       //
+       // we also have to get the 'real' value if the we have a mulitSelect
+       // Field but got a non array value
+       if ((me.valueField && me.valueField !== me.displayField) ||
+           (me.multiSelect && !Ext.isArray(value))) {
+           value = me.getValue();
+       }
+
+       if (!(me.notFoundIsValid || me.isValueInStore(value))) {
+           return gettext('Invalid Value');
+       }
+
+       return true;
+    },
+
+    // validate after enabling a field, otherwise blank fields with !allowBlank
+    // are sometimes not marked as invalid
+    setDisabled: function(value) {
+       this.callParent([value]);
+       this.validate();
+    },
+
+    initComponent: function() {
+       let me = this;
+
+       Ext.apply(me, {
+           queryMode: 'local',
+           matchFieldWidth: false,
+       });
+
+       Ext.applyIf(me, { value: '' }); // hack: avoid ExtJS validate() bug
+
+       Ext.applyIf(me.listConfig, { width: 400 });
+
+       me.callParent();
+
+       // Create the picker at an early stage, so it is available to store the previous selection
+       if (!me.picker) {
+           me.createPicker();
+       }
+
+       me.mon(me.store, 'beforeload', function() {
+           if (!me.isDisabled()) {
+               me.enableLoadMask = true;
+           }
+       });
+
+       // hack: autoSelect does not work
+       me.mon(me.store, 'load', function(store, r, success, o) {
+           if (success) {
+               me.clearInvalid();
+
+               if (me.enableLoadMask) {
+                   delete me.enableLoadMask;
+
+                   // if the picker exists,
+                   // we reset its minheight to the saved let/0
+                   // we have to update the layout, otherwise the height
+                   // gets not recalculated
+                   if (me.picker) {
+                       me.picker.setMinHeight(me.savedMinHeight || 0);
+                       delete me.savedMinHeight;
+                       me.picker.updateLayout();
+                   }
+               }
+
+               let def = me.getValue() || me.preferredValue;
+               if (def) {
+                   me.setValue(def, true); // sync with grid
+               }
+               let found = false;
+               if (def) {
+                   found = me.isValueInStore(def);
+               }
+
+               if (!found) {
+                   let rec = me.store.first();
+                   if (me.autoSelect && rec && rec.data) {
+                       def = rec.data[me.valueField];
+                       me.setValue(def, true);
+                   } else if (!me.allowBlank && !(Ext.isArray(def) ? def.length : def)) {
+                       me.setValue(def);
+                       if (!me.notFoundIsValid && !me.isDisabled()) {
+                           me.markInvalid(me.blankText);
+                       }
+                   }
+               }
+           }
+       });
+    },
+});
diff --git a/src/form/DateTimeField.js b/src/form/DateTimeField.js
new file mode 100644 (file)
index 0000000..a061e15
--- /dev/null
@@ -0,0 +1,151 @@
+Ext.define('Proxmox.DateTimeField', {
+    extend: 'Ext.form.FieldContainer',
+    xtype: 'promxoxDateTimeField',
+
+    layout: 'hbox',
+
+    referenceHolder: true,
+
+    submitFormat: 'U',
+
+    getValue: function() {
+       let me = this;
+       let d = me.lookupReference('dateentry').getValue();
+
+       if (d === undefined || d === null) { return null; }
+
+       let t = me.lookupReference('timeentry').getValue();
+
+       if (t === undefined || t === null) { return null; }
+
+       let offset = (t.getHours() * 3600 + t.getMinutes() * 60) * 1000;
+
+       return new Date(d.getTime() + offset);
+    },
+
+    getSubmitValue: function() {
+        let me = this;
+        let format = me.submitFormat;
+        let value = me.getValue();
+
+        return value ? Ext.Date.format(value, format) : null;
+    },
+
+    items: [
+       {
+           xtype: 'datefield',
+           editable: false,
+           reference: 'dateentry',
+           flex: 1,
+           format: 'Y-m-d',
+       },
+       {
+           xtype: 'timefield',
+           reference: 'timeentry',
+           format: 'H:i',
+           width: 80,
+           value: '00:00',
+           increment: 60,
+       },
+    ],
+
+    setMinValue: function(value) {
+       let me = this;
+       let current = me.getValue();
+       if (!value || !current) {
+           return;
+       }
+
+       let minhours = value.getHours();
+       let minminutes = value.getMinutes();
+
+       let hours = current.getHours();
+       let minutes = current.getMinutes();
+
+       value.setHours(0);
+       value.setMinutes(0);
+       value.setSeconds(0);
+       current.setHours(0);
+       current.setMinutes(0);
+       current.setSeconds(0);
+
+       let time = new Date();
+       if (current-value > 0) {
+           time.setHours(0);
+           time.setMinutes(0);
+           time.setSeconds(0);
+           time.setMilliseconds(0);
+       } else {
+           time.setHours(minhours);
+           time.setMinutes(minminutes);
+       }
+       me.lookup('timeentry').setMinValue(time);
+
+       // current time is smaller than the time part of the new minimum
+       // so we have to add 1 to the day
+       if (minhours*60+minminutes > hours*60+minutes) {
+           value.setDate(value.getDate()+1);
+       }
+       me.lookup('dateentry').setMinValue(value);
+    },
+
+    setMaxValue: function(value) {
+       let me = this;
+       let current = me.getValue();
+       if (!value || !current) {
+           return;
+       }
+
+       let maxhours = value.getHours();
+       let maxminutes = value.getMinutes();
+
+       let hours = current.getHours();
+       let minutes = current.getMinutes();
+
+       value.setHours(0);
+       value.setMinutes(0);
+       current.setHours(0);
+       current.setMinutes(0);
+
+       let time = new Date();
+       if (value-current > 0) {
+           time.setHours(23);
+           time.setMinutes(59);
+           time.setSeconds(59);
+       } else {
+           time.setHours(maxhours);
+           time.setMinutes(maxminutes);
+       }
+       me.lookup('timeentry').setMaxValue(time);
+
+       // current time is biger than the time part of the new maximum
+       // so we have to subtract 1 to the day
+       if (maxhours*60+maxminutes < hours*60+minutes) {
+           value.setDate(value.getDate()-1);
+       }
+
+       me.lookup('dateentry').setMaxValue(value);
+    },
+
+    initComponent: function() {
+       let me = this;
+
+       me.callParent();
+
+       let value = me.value || new Date();
+
+       me.lookupReference('dateentry').setValue(value);
+       me.lookupReference('timeentry').setValue(value);
+
+       if (me.minValue) {
+           me.setMinValue(me.minValue);
+       }
+
+       if (me.maxValue) {
+           me.setMaxValue(me.maxValue);
+       }
+
+       me.relayEvents(me.lookupReference('dateentry'), ['change']);
+       me.relayEvents(me.lookupReference('timeentry'), ['change']);
+    },
+});
diff --git a/src/form/DisplayEdit.js b/src/form/DisplayEdit.js
new file mode 100644 (file)
index 0000000..b8060a5
--- /dev/null
@@ -0,0 +1,77 @@
+Ext.define('Proxmox.form.field.DisplayEdit', {
+    extend: 'Ext.form.FieldContainer',
+    alias: 'widget.pmxDisplayEditField',
+
+    viewModel: {
+       parent: null,
+       data: {
+           editable: false,
+           value: undefined,
+       },
+    },
+
+    displayType: 'displayfield',
+
+    editConfig: {},
+    editable: false,
+    setEditable: function(editable) {
+       let me = this;
+       let vm = me.getViewModel();
+
+       me.editable = editable;
+       vm.set('editable', editable);
+    },
+
+    layout: 'fit',
+    defaults: {
+       hideLabel: true,
+    },
+
+    initComponent: function() {
+       let me = this;
+
+       let displayConfig = {
+           xtype: me.displayType,
+           bind: {},
+       };
+       Ext.applyIf(displayConfig, me.initialConfig);
+       delete displayConfig.editConfig;
+       delete displayConfig.editable;
+
+       let editConfig = Ext.apply({}, me.editConfig);
+       Ext.applyIf(editConfig, {
+           xtype: 'textfield',
+           bind: {},
+       });
+       Ext.applyIf(editConfig, displayConfig);
+
+       Ext.applyIf(displayConfig.bind, {
+           hidden: '{editable}',
+           disabled: '{editable}',
+           value: '{value}',
+       });
+       Ext.applyIf(editConfig.bind, {
+           hidden: '{!editable}',
+           disabled: '{!editable}',
+           value: '{value}',
+       });
+
+       // avoid glitch, start off correct even before viewmodel fixes it
+       editConfig.disabled = editConfig.hidden = !me.editable;
+       displayConfig.disabled = displayConfig.hidden = !!me.editable;
+
+       editConfig.name = displayConfig.name = me.name;
+
+       Ext.apply(me, {
+           items: [
+               editConfig,
+               displayConfig,
+           ],
+       });
+
+       me.callParent();
+
+       me.getViewModel().set('editable', me.editable);
+    },
+
+});
diff --git a/src/form/ExpireDate.js b/src/form/ExpireDate.js
new file mode 100644 (file)
index 0000000..61ff96f
--- /dev/null
@@ -0,0 +1,34 @@
+// treats 0 as "never expires"
+Ext.define('Proxmox.form.field.ExpireDate', {
+    extend: 'Ext.form.field.Date',
+    alias: ['widget.pmxExpireDate'],
+
+    name: 'expire',
+    fieldLabel: gettext('Expire'),
+    emptyText: 'never',
+    format: 'Y-m-d',
+    submitFormat: 'U',
+
+    getSubmitValue: function() {
+       let me = this;
+
+       let value = me.callParent();
+       if (!value) value = 0;
+
+       return value;
+    },
+
+    setValue: function(value) {
+       let me = this;
+
+       if (Ext.isDefined(value)) {
+           if (!value) {
+               value = null;
+           } else if (!Ext.isDate(value)) {
+               value = new Date(value * 1000);
+           }
+       }
+       me.callParent([value]);
+    },
+
+});
diff --git a/src/form/IntegerField.js b/src/form/IntegerField.js
new file mode 100644 (file)
index 0000000..67dd215
--- /dev/null
@@ -0,0 +1,30 @@
+Ext.define('Proxmox.form.field.Integer', {
+    extend: 'Ext.form.field.Number',
+    alias: 'widget.proxmoxintegerfield',
+
+    config: {
+       deleteEmpty: false,
+    },
+
+    allowDecimals: false,
+    allowExponential: false,
+    step: 1,
+
+   getSubmitData: function() {
+        let me = this,
+            data = null,
+            val;
+        if (!me.disabled && me.submitValue && !me.isFileUpload()) {
+            val = me.getSubmitValue();
+            if (val !== undefined && val !== null && val !== '') {
+                data = {};
+                data[me.getName()] = val;
+            } else if (me.getDeleteEmpty()) {
+               data = {};
+                data.delete = me.getName();
+           }
+        }
+        return data;
+    },
+
+});
diff --git a/src/form/KVComboBox.js b/src/form/KVComboBox.js
new file mode 100644 (file)
index 0000000..361217e
--- /dev/null
@@ -0,0 +1,81 @@
+/* Key-Value ComboBox
+ *
+ * config properties:
+ * comboItems: an array of Key - Value pairs
+ * deleteEmpty: if set to true (default), an empty value received from the
+ * comboBox will reset the property to its default value
+ */
+Ext.define('Proxmox.form.KVComboBox', {
+    extend: 'Ext.form.field.ComboBox',
+    alias: 'widget.proxmoxKVComboBox',
+
+    config: {
+       deleteEmpty: true,
+    },
+
+    comboItems: undefined,
+    displayField: 'value',
+    valueField: 'key',
+    queryMode: 'local',
+
+    // overide framework function to implement deleteEmpty behaviour
+    getSubmitData: function() {
+        let me = this,
+            data = null,
+            val;
+        if (!me.disabled && me.submitValue) {
+            val = me.getSubmitValue();
+            if (val !== null && val !== '' && val !== '__default__') {
+                data = {};
+                data[me.getName()] = val;
+            } else if (me.getDeleteEmpty()) {
+                data = {};
+                data.delete = me.getName();
+            }
+        }
+        return data;
+    },
+
+    validator: function(val) {
+       let me = this;
+
+       if (me.editable || val === null || val === '') {
+           return true;
+       }
+
+       if (me.store.getCount() > 0) {
+           let values = me.multiSelect ? val.split(me.delimiter) : [val];
+           let items = me.store.getData().collect('value', 'data');
+           if (Ext.Array.every(values, function(value) {
+               return Ext.Array.contains(items, value);
+           })) {
+               return true;
+           }
+       }
+
+       // returns a boolean or string
+       return "value '" + val + "' not allowed!";
+    },
+
+    initComponent: function() {
+       let me = this;
+
+       me.store = Ext.create('Ext.data.ArrayStore', {
+           model: 'KeyValue',
+           data: me.comboItems,
+       });
+
+       if (me.initialConfig.editable === undefined) {
+           me.editable = false;
+       }
+
+       me.callParent();
+    },
+
+    setComboItems: function(items) {
+       let me = this;
+
+       me.getStore().setData(items);
+    },
+
+});
diff --git a/src/form/LanguageSelector.js b/src/form/LanguageSelector.js
new file mode 100644 (file)
index 0000000..92f28b1
--- /dev/null
@@ -0,0 +1,6 @@
+Ext.define('Proxmox.form.LanguageSelector', {
+    extend: 'Proxmox.form.KVComboBox',
+    xtype: 'proxmoxLanguageSelector',
+
+    comboItems: Proxmox.Utils.language_array(),
+});
diff --git a/src/form/NetworkSelector.js b/src/form/NetworkSelector.js
new file mode 100644 (file)
index 0000000..b87efc1
--- /dev/null
@@ -0,0 +1,129 @@
+Ext.define('Proxmox.form.NetworkSelectorController', {
+    extend: 'Ext.app.ViewController',
+    alias: 'controller.proxmoxNetworkSelectorController',
+
+    init: function(view) {
+       let me = this;
+
+       if (!view.nodename) {
+           throw "missing custom view config: nodename";
+       }
+       view.getStore().getProxy().setUrl('/api2/json/nodes/'+ view.nodename + '/network');
+    },
+});
+
+Ext.define('Proxmox.data.NetworkSelector', {
+    extend: 'Ext.data.Model',
+    fields: [
+       { name: 'active' },
+       { name: 'cidr' },
+       { name: 'cidr6' },
+       { name: 'address' },
+       { name: 'address6' },
+       { name: 'comments' },
+       { name: 'iface' },
+       { name: 'slaves' },
+       { name: 'type' },
+    ],
+});
+
+Ext.define('Proxmox.form.NetworkSelector', {
+    extend: 'Proxmox.form.ComboGrid',
+    alias: 'widget.proxmoxNetworkSelector',
+
+    controller: 'proxmoxNetworkSelectorController',
+
+    nodename: 'localhost',
+    setNodename: function(nodename) {
+       this.nodename = nodename;
+       let networkSelectorStore = this.getStore();
+       networkSelectorStore.removeAll();
+       // because of manual local copy of data for ip4/6
+       this.getPicker().refresh();
+       if (networkSelectorStore && typeof networkSelectorStore.getProxy === 'function') {
+           networkSelectorStore.getProxy().setUrl('/api2/json/nodes/'+ nodename + '/network');
+           networkSelectorStore.load();
+       }
+    },
+    // set default value to empty array, else it inits it with
+    // null and after the store load it is an empty array,
+    // triggering dirtychange
+    value: [],
+    valueField: 'cidr',
+    displayField: 'cidr',
+    store: {
+       autoLoad: true,
+       model: 'Proxmox.data.NetworkSelector',
+       proxy: {
+           type: 'proxmox',
+       },
+       sorters: [
+           {
+               property: 'iface',
+               direction: 'ASC',
+           },
+       ],
+       filters: [
+           function(item) {
+               return item.data.cidr;
+           },
+       ],
+       listeners: {
+           load: function(store, records, successfull) {
+               if (successfull) {
+                   records.forEach(function(record) {
+                       if (record.data.cidr6) {
+                           let dest = record.data.cidr ? record.copy(null) : record;
+                           dest.data.cidr = record.data.cidr6;
+                           dest.data.address = record.data.address6;
+                           delete record.data.cidr6;
+                           dest.data.comments = record.data.comments6;
+                           delete record.data.comments6;
+                           store.add(dest);
+                       }
+                   });
+               }
+           },
+       },
+    },
+    listConfig: {
+       width: 600,
+       columns: [
+           {
+
+               header: gettext('CIDR'),
+               dataIndex: 'cidr',
+               hideable: false,
+               flex: 1,
+           },
+           {
+
+               header: gettext('IP'),
+               dataIndex: 'address',
+               hidden: true,
+           },
+           {
+               header: gettext('Interface'),
+               width: 90,
+               dataIndex: 'iface',
+           },
+           {
+               header: gettext('Active'),
+               renderer: Proxmox.Utils.format_boolean,
+               width: 60,
+               dataIndex: 'active',
+           },
+           {
+               header: gettext('Type'),
+               width: 80,
+               hidden: true,
+               dataIndex: 'type',
+           },
+           {
+               header: gettext('Comment'),
+               flex: 2,
+               dataIndex: 'comments',
+           },
+       ],
+    },
+});
diff --git a/src/form/RRDTypeSelector.js b/src/form/RRDTypeSelector.js
new file mode 100644 (file)
index 0000000..16cac20
--- /dev/null
@@ -0,0 +1,58 @@
+Ext.define('Proxmox.form.RRDTypeSelector', {
+    extend: 'Ext.form.field.ComboBox',
+    alias: ['widget.proxmoxRRDTypeSelector'],
+
+    displayField: 'text',
+    valueField: 'id',
+    editable: false,
+    queryMode: 'local',
+    value: 'hour',
+    stateEvents: ['select'],
+    stateful: true,
+    stateId: 'proxmoxRRDTypeSelection',
+    store: {
+       type: 'array',
+       fields: ['id', 'timeframe', 'cf', 'text'],
+       data: [
+           ['hour', 'hour', 'AVERAGE',
+             gettext('Hour') + ' (' + gettext('average') +')'],
+           ['hourmax', 'hour', 'MAX',
+             gettext('Hour') + ' (' + gettext('maximum') + ')'],
+           ['day', 'day', 'AVERAGE',
+             gettext('Day') + ' (' + gettext('average') + ')'],
+           ['daymax', 'day', 'MAX',
+             gettext('Day') + ' (' + gettext('maximum') + ')'],
+           ['week', 'week', 'AVERAGE',
+             gettext('Week') + ' (' + gettext('average') + ')'],
+           ['weekmax', 'week', 'MAX',
+             gettext('Week') + ' (' + gettext('maximum') + ')'],
+           ['month', 'month', 'AVERAGE',
+             gettext('Month') + ' (' + gettext('average') + ')'],
+           ['monthmax', 'month', 'MAX',
+             gettext('Month') + ' (' + gettext('maximum') + ')'],
+           ['year', 'year', 'AVERAGE',
+             gettext('Year') + ' (' + gettext('average') + ')'],
+           ['yearmax', 'year', 'MAX',
+             gettext('Year') + ' (' + gettext('maximum') + ')'],
+       ],
+    },
+    // save current selection in the state Provider so RRDView can read it
+    getState: function() {
+       let ind = this.getStore().findExact('id', this.getValue());
+       let rec = this.getStore().getAt(ind);
+       if (!rec) {
+           return undefined;
+       }
+       return {
+           id: rec.data.id,
+           timeframe: rec.data.timeframe,
+           cf: rec.data.cf,
+       };
+    },
+    // set selection based on last saved state
+    applyState: function(state) {
+       if (state && state.id) {
+           this.setValue(state.id);
+       }
+    },
+});
diff --git a/src/form/RealmComboBox.js b/src/form/RealmComboBox.js
new file mode 100644 (file)
index 0000000..309bf4f
--- /dev/null
@@ -0,0 +1,57 @@
+Ext.define('Proxmox.form.RealmComboBox', {
+    extend: 'Ext.form.field.ComboBox',
+    alias: 'widget.pmxRealmComboBox',
+
+    controller: {
+       xclass: 'Ext.app.ViewController',
+
+       init: function(view) {
+           view.store.on('load', this.onLoad, view);
+       },
+
+       onLoad: function(store, records, success) {
+           if (!success) {
+               return;
+           }
+           let me = this;
+           let val = me.getValue();
+           if (!val || !me.store.findRecord('realm', val)) {
+               let def = 'pam';
+               Ext.each(records, function(rec) {
+                   if (rec.data && rec.data.default) {
+                       def = rec.data.realm;
+                   }
+               });
+               me.setValue(def);
+           }
+       },
+    },
+
+    fieldLabel: gettext('Realm'),
+    name: 'realm',
+    queryMode: 'local',
+    allowBlank: false,
+    editable: false,
+    forceSelection: true,
+    autoSelect: false,
+    triggerAction: 'all',
+    valueField: 'realm',
+    displayField: 'descr',
+    getState: function() {
+       return { value: this.getValue() };
+    },
+    applyState: function(state) {
+       if (state && state.value) {
+           this.setValue(state.value);
+       }
+    },
+    stateEvents: ['select'],
+    stateful: true, // last chosen auth realm is saved between page reloads
+    id: 'pveloginrealm', // We need stable ids when using stateful, not autogenerated
+    stateID: 'pveloginrealm',
+
+    store: {
+       model: 'pmx-domains',
+       autoLoad: true,
+    },
+});
diff --git a/src/form/RoleSelector.js b/src/form/RoleSelector.js
new file mode 100644 (file)
index 0000000..142cdfd
--- /dev/null
@@ -0,0 +1,41 @@
+Ext.define('pmx-roles', {
+    extend: 'Ext.data.Model',
+    fields: ['roleid', 'privs'],
+    proxy: {
+       type: 'proxmox',
+       url: "/api2/json/access/roles",
+    },
+    idProperty: 'roleid',
+});
+
+Ext.define('Proxmox.form.RoleSelector', {
+    extend: 'Proxmox.form.ComboGrid',
+    alias: 'widget.pmxRoleSelector',
+
+    allowBlank: false,
+    autoSelect: false,
+    valueField: 'roleid',
+    displayField: 'roleid',
+
+    listConfig: {
+       columns: [
+           {
+               header: gettext('Role'),
+               sortable: true,
+               dataIndex: 'roleid',
+               flex: 1,
+           },
+           {
+               header: gettext('Privileges'),
+               dataIndex: 'privs',
+               flex: 1,
+           },
+       ],
+    },
+
+    store: {
+       autoLoad: true,
+       model: 'pmx-roles',
+       sorters: 'roleid',
+    },
+});
diff --git a/src/form/TextField.js b/src/form/TextField.js
new file mode 100644 (file)
index 0000000..56e5976
--- /dev/null
@@ -0,0 +1,43 @@
+Ext.define('Proxmox.form.field.Textfield', {
+    extend: 'Ext.form.field.Text',
+    alias: ['widget.proxmoxtextfield'],
+
+    config: {
+       skipEmptyText: true,
+
+       deleteEmpty: false,
+    },
+
+    getSubmitData: function() {
+        let me = this,
+            data = null,
+            val;
+        if (!me.disabled && me.submitValue && !me.isFileUpload()) {
+            val = me.getSubmitValue();
+            if (val !== null) {
+                data = {};
+                data[me.getName()] = val;
+            } else if (me.getDeleteEmpty()) {
+               data = {};
+                data.delete = me.getName();
+           }
+        }
+        return data;
+    },
+
+    getSubmitValue: function() {
+       let me = this;
+
+        let value = this.processRawValue(this.getRawValue());
+       if (value !== '') {
+           return value;
+       }
+
+       return me.getSkipEmptyText() ? null: value;
+    },
+
+    setAllowBlank: function(allowBlank) {
+       this.allowBlank = allowBlank;
+       this.validate();
+    },
+});
diff --git a/src/grid/ObjectGrid.js b/src/grid/ObjectGrid.js
new file mode 100644 (file)
index 0000000..541cf1d
--- /dev/null
@@ -0,0 +1,326 @@
+/* Renders a list of key values objets
+
+mandatory config parameters:
+rows: an object container where each propery is a key-value object we want to render
+       let rows = {
+           keyboard: {
+               header: gettext('Keyboard Layout'),
+               editor: 'Your.KeyboardEdit',
+               required: true
+           },
+
+optional:
+disabled: setting this parameter to true will disable selection and focus on the
+proxmoxObjectGrid as well as greying out input elements.
+Useful for a readonly tabular display
+
+*/
+
+Ext.define('Proxmox.grid.ObjectGrid', {
+    extend: 'Ext.grid.GridPanel',
+    alias: ['widget.proxmoxObjectGrid'],
+    disabled: false,
+    hideHeaders: true,
+
+    monStoreErrors: false,
+
+    add_combobox_row: function(name, text, opts) {
+       let me = this;
+
+       opts = opts || {};
+       me.rows = me.rows || {};
+
+       me.rows[name] = {
+           required: true,
+           defaultValue: opts.defaultValue,
+           header: text,
+           renderer: opts.renderer,
+           editor: {
+               xtype: 'proxmoxWindowEdit',
+               subject: text,
+               onlineHelp: opts.onlineHelp,
+               fieldDefaults: {
+                   labelWidth: opts.labelWidth || 100,
+               },
+               items: {
+                   xtype: 'proxmoxKVComboBox',
+                   name: name,
+                   comboItems: opts.comboItems,
+                   value: opts.defaultValue,
+                   deleteEmpty: !!opts.deleteEmpty,
+                   emptyText: opts.defaultValue,
+                   labelWidth: Proxmox.Utils.compute_min_label_width(
+                       text, opts.labelWidth),
+                   fieldLabel: text,
+               },
+           },
+       };
+    },
+
+    add_text_row: function(name, text, opts) {
+       let me = this;
+
+       opts = opts || {};
+       me.rows = me.rows || {};
+
+       me.rows[name] = {
+           required: true,
+           defaultValue: opts.defaultValue,
+           header: text,
+           renderer: opts.renderer,
+           editor: {
+               xtype: 'proxmoxWindowEdit',
+               subject: text,
+               onlineHelp: opts.onlineHelp,
+               fieldDefaults: {
+                   labelWidth: opts.labelWidth || 100,
+               },
+               items: {
+                   xtype: 'proxmoxtextfield',
+                   name: name,
+                   deleteEmpty: !!opts.deleteEmpty,
+                   emptyText: opts.defaultValue,
+                   labelWidth: Proxmox.Utils.compute_min_label_width(
+                       text, opts.labelWidth),
+                   vtype: opts.vtype,
+                   fieldLabel: text,
+               },
+           },
+       };
+    },
+
+    add_boolean_row: function(name, text, opts) {
+       let me = this;
+
+       opts = opts || {};
+       me.rows = me.rows || {};
+
+       me.rows[name] = {
+           required: true,
+           defaultValue: opts.defaultValue || 0,
+           header: text,
+           renderer: opts.renderer || Proxmox.Utils.format_boolean,
+           editor: {
+               xtype: 'proxmoxWindowEdit',
+               subject: text,
+               onlineHelp: opts.onlineHelp,
+               fieldDefaults: {
+                   labelWidth: opts.labelWidth || 100,
+               },
+               items: {
+                   xtype: 'proxmoxcheckbox',
+                   name: name,
+                   uncheckedValue: 0,
+                   defaultValue: opts.defaultValue || 0,
+                   checked: !!opts.defaultValue,
+                   deleteDefaultValue: !!opts.deleteDefaultValue,
+                   labelWidth: Proxmox.Utils.compute_min_label_width(
+                       text, opts.labelWidth),
+                   fieldLabel: text,
+               },
+           },
+       };
+    },
+
+    add_integer_row: function(name, text, opts) {
+       let me = this;
+
+       opts = opts || {};
+       me.rows = me.rows || {};
+
+       me.rows[name] = {
+           required: true,
+           defaultValue: opts.defaultValue,
+           header: text,
+           renderer: opts.renderer,
+           editor: {
+               xtype: 'proxmoxWindowEdit',
+               subject: text,
+               onlineHelp: opts.onlineHelp,
+               fieldDefaults: {
+                   labelWidth: opts.labelWidth || 100,
+               },
+               items: {
+                   xtype: 'proxmoxintegerfield',
+                   name: name,
+                   minValue: opts.minValue,
+                   maxValue: opts.maxValue,
+                   emptyText: gettext('Default'),
+                   deleteEmpty: !!opts.deleteEmpty,
+                   value: opts.defaultValue,
+                   labelWidth: Proxmox.Utils.compute_min_label_width(
+                       text, opts.labelWidth),
+                   fieldLabel: text,
+               },
+           },
+       };
+    },
+
+    editorConfig: {}, // default config passed to editor
+
+    run_editor: function() {
+       let me = this;
+
+       let sm = me.getSelectionModel();
+       let rec = sm.getSelection()[0];
+       if (!rec) {
+           return;
+       }
+
+       let rows = me.rows;
+       let rowdef = rows[rec.data.key];
+       if (!rowdef.editor) {
+           return;
+       }
+
+       let win;
+       let config;
+       if (Ext.isString(rowdef.editor)) {
+           config = Ext.apply({
+               confid: rec.data.key,
+           }, me.editorConfig);
+           win = Ext.create(rowdef.editor, config);
+       } else {
+           config = Ext.apply({
+               confid: rec.data.key,
+           }, me.editorConfig);
+           Ext.apply(config, rowdef.editor);
+           win = Ext.createWidget(rowdef.editor.xtype, config);
+           win.load();
+       }
+
+       win.show();
+       win.on('destroy', me.reload, me);
+    },
+
+    reload: function() {
+       let me = this;
+       me.rstore.load();
+    },
+
+    getObjectValue: function(key, defaultValue) {
+       let me = this;
+       let rec = me.store.getById(key);
+       if (rec) {
+           return rec.data.value;
+       }
+       return defaultValue;
+    },
+
+    renderKey: function(key, metaData, record, rowIndex, colIndex, store) {
+       let me = this;
+       let rows = me.rows;
+       let rowdef = rows && rows[key] ? rows[key] : {};
+       return rowdef.header || key;
+    },
+
+    renderValue: function(value, metaData, record, rowIndex, colIndex, store) {
+       let me = this;
+       let rows = me.rows;
+       let key = record.data.key;
+       let rowdef = rows && rows[key] ? rows[key] : {};
+
+       let renderer = rowdef.renderer;
+       if (renderer) {
+           return renderer(value, metaData, record, rowIndex, colIndex, store);
+       }
+
+       return value;
+    },
+
+    listeners: {
+       itemkeydown: function(view, record, item, index, e) {
+           if (e.getKey() === e.ENTER) {
+               this.pressedIndex = index;
+           }
+       },
+       itemkeyup: function(view, record, item, index, e) {
+           if (e.getKey() === e.ENTER && index === this.pressedIndex) {
+               this.run_editor();
+           }
+
+           this.pressedIndex = undefined;
+       },
+    },
+
+    initComponent: function() {
+       let me = this;
+
+       let rows = me.rows;
+
+       if (!me.rstore) {
+           if (!me.url) {
+               throw "no url specified";
+           }
+
+           me.rstore = Ext.create('Proxmox.data.ObjectStore', {
+               url: me.url,
+               interval: me.interval,
+               extraParams: me.extraParams,
+               rows: me.rows,
+           });
+       }
+
+       let rstore = me.rstore;
+       let store = Ext.create('Proxmox.data.DiffStore', {
+           rstore: rstore,
+           sorters: [],
+           filters: [],
+       });
+
+       if (rows) {
+           Ext.Object.each(rows, function(key, rowdef) {
+               if (Ext.isDefined(rowdef.defaultValue)) {
+                   store.add({ key: key, value: rowdef.defaultValue });
+               } else if (rowdef.required) {
+                   store.add({ key: key, value: undefined });
+               }
+           });
+       }
+
+       if (me.sorterFn) {
+           store.sorters.add(Ext.create('Ext.util.Sorter', {
+               sorterFn: me.sorterFn,
+           }));
+       }
+
+       store.filters.add(Ext.create('Ext.util.Filter', {
+           filterFn: function(item) {
+               if (rows) {
+                   let rowdef = rows[item.data.key];
+                   if (!rowdef || rowdef.visible === false) {
+                       return false;
+                   }
+               }
+               return true;
+           },
+       }));
+
+       Proxmox.Utils.monStoreErrors(me, rstore);
+
+       Ext.applyIf(me, {
+           store: store,
+           stateful: false,
+           columns: [
+               {
+                   header: gettext('Name'),
+                   width: me.cwidth1 || 200,
+                   dataIndex: 'key',
+                   renderer: me.renderKey,
+               },
+               {
+                   flex: 1,
+                   header: gettext('Value'),
+                   dataIndex: 'value',
+                   renderer: me.renderValue,
+               },
+           ],
+       });
+
+       me.callParent();
+
+       if (me.monStoreErrors) {
+           Proxmox.Utils.monStoreErrors(me, me.store);
+       }
+   },
+});
diff --git a/src/grid/PendingObjectGrid.js b/src/grid/PendingObjectGrid.js
new file mode 100644 (file)
index 0000000..a5a640c
--- /dev/null
@@ -0,0 +1,115 @@
+Ext.define('Proxmox.grid.PendingObjectGrid', {
+    extend: 'Proxmox.grid.ObjectGrid',
+    alias: ['widget.proxmoxPendingObjectGrid'],
+
+    getObjectValue: function(key, defaultValue, pending) {
+       let me = this;
+       let rec = me.store.getById(key);
+       if (rec) {
+           let value = rec.data.value;
+           if (pending) {
+               if (Ext.isDefined(rec.data.pending) && rec.data.pending !== '') {
+                   value = rec.data.pending;
+               } else if (rec.data.delete === 1) {
+                   value = defaultValue;
+               }
+           }
+
+            if (Ext.isDefined(value) && value !== '') {
+               return value;
+            } else {
+               return defaultValue;
+            }
+       }
+       return defaultValue;
+    },
+
+    hasPendingChanges: function(key) {
+       let me = this;
+       let rows = me.rows;
+       let rowdef = rows && rows[key] ? rows[key] : {};
+       let keys = rowdef.multiKey || [key];
+       let pending = false;
+
+       Ext.Array.each(keys, function(k) {
+           let rec = me.store.getById(k);
+           if (rec && rec.data && (
+                   (Ext.isDefined(rec.data.pending) && rec.data.pending !== '') ||
+                   rec.data.delete === 1
+           )) {
+               pending = true;
+               return false; // break
+           }
+           return true;
+       });
+
+       return pending;
+    },
+
+    renderValue: function(value, metaData, record, rowIndex, colIndex, store) {
+       let me = this;
+       let rows = me.rows;
+       let key = record.data.key;
+       let rowdef = rows && rows[key] ? rows[key] : {};
+       let renderer = rowdef.renderer;
+       let current = '';
+       let pending = '';
+
+       if (renderer) {
+           current = renderer(value, metaData, record, rowIndex, colIndex, store, false);
+           if (me.hasPendingChanges(key)) {
+               pending = renderer(record.data.pending, metaData, record, rowIndex, colIndex, store, true);
+           }
+           if (pending === current) {
+               pending = undefined;
+           }
+       } else {
+           current = value || '';
+           pending = record.data.pending;
+       }
+
+       if (record.data.delete) {
+           let delete_all = true;
+           if (rowdef.multiKey) {
+               Ext.Array.each(rowdef.multiKey, function(k) {
+                   let rec = me.store.getById(k);
+                   if (rec && rec.data && rec.data.delete !== 1) {
+                       delete_all = false;
+                       return false; // break
+                   }
+                   return true;
+               });
+           }
+           if (delete_all) {
+               pending = '<div style="text-decoration: line-through;">'+ current +'</div>';
+           }
+       }
+
+       if (pending) {
+           return current + '<div style="color:darkorange">' + pending + '</div>';
+       } else {
+           return current;
+       }
+    },
+
+    initComponent: function() {
+       let me = this;
+
+       if (!me.rstore) {
+           if (!me.url) {
+               throw "no url specified";
+           }
+
+           me.rstore = Ext.create('Proxmox.data.ObjectStore', {
+               model: 'KeyValuePendingDelete',
+               readArray: true,
+               url: me.url,
+               interval: me.interval,
+               extraParams: me.extraParams,
+               rows: me.rows,
+           });
+       }
+
+       me.callParent();
+   },
+});
diff --git a/src/images/Makefile b/src/images/Makefile
new file mode 100644 (file)
index 0000000..45bc50a
--- /dev/null
@@ -0,0 +1,13 @@
+include ../defines.mk
+
+IMAGES=pmx-clear-trigger.png
+
+all:
+
+.PHONY: install
+install: ${IMAGES}
+       install -d ${WWWIMAGESDIR}
+       for i in ${IMAGES}; do install -m 0755 $$i ${WWWIMAGESDIR}/$$i; done
+
+.PHONY: clean
+clean:
diff --git a/src/images/pmx-clear-trigger.png b/src/images/pmx-clear-trigger.png
new file mode 100644 (file)
index 0000000..cc374cf
Binary files /dev/null and b/src/images/pmx-clear-trigger.png differ
diff --git a/src/mixin/CBind.js b/src/mixin/CBind.js
new file mode 100644 (file)
index 0000000..afef53a
--- /dev/null
@@ -0,0 +1,161 @@
+Ext.define('Proxmox.Mixin.CBind', {
+    extend: 'Ext.Mixin',
+
+    mixinConfig: {
+        before: {
+            initComponent: 'cloneTemplates',
+        },
+    },
+
+    cloneTemplates: function() {
+       let me = this;
+
+       if (typeof me.cbindData === "function") {
+           me.cbindData = me.cbindData(me.initialConfig);
+       }
+       me.cbindData = me.cbindData || {};
+
+       let getConfigValue = function(cname) {
+           if (cname in me.initialConfig) {
+               return me.initialConfig[cname];
+           }
+           if (cname in me.cbindData) {
+               let res = me.cbindData[cname];
+               if (typeof res === "function") {
+                   return res(me.initialConfig);
+               } else {
+                   return res;
+               }
+           }
+           if (cname in me) {
+               return me[cname];
+           }
+           throw "unable to get cbind data for '" + cname + "'";
+       };
+
+       let applyCBind = function(obj) {
+           let cbind = obj.cbind, cdata;
+           if (!cbind) return;
+
+           for (const prop in cbind) { // eslint-disable-line guard-for-in
+               let match, found;
+               cdata = cbind[prop];
+
+               found = false;
+               if (typeof cdata === 'function') {
+                   obj[prop] = cdata(getConfigValue, prop);
+                   found = true;
+               } else if ((match = /^\{(!)?([a-z_][a-z0-9_]*)\}$/i.exec(cdata))) {
+                   let cvalue = getConfigValue(match[2]);
+                   if (match[1]) cvalue = !cvalue;
+                   obj[prop] = cvalue;
+                   found = true;
+               } else if ((match = /^\{(!)?([a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)+)\}$/i.exec(cdata))) {
+                   let keys = match[2].split('.');
+                   let cvalue = getConfigValue(keys.shift());
+                   keys.forEach(function(k) {
+                       if (k in cvalue) {
+                           cvalue = cvalue[k];
+                       } else {
+                           throw "unable to get cbind data for '" + match[2] + "'";
+                       }
+                   });
+                   if (match[1]) cvalue = !cvalue;
+                   obj[prop] = cvalue;
+                   found = true;
+               } else {
+                   obj[prop] = cdata.replace(/{([a-z_][a-z0-9_]*)\}/ig, (_match, cname) => {
+                       let cvalue = getConfigValue(cname);
+                       found = true;
+                       return cvalue;
+                   });
+               }
+               if (!found) {
+                   throw "unable to parse cbind template '" + cdata + "'";
+               }
+           }
+       };
+
+       if (me.cbind) {
+           applyCBind(me);
+       }
+
+       let cloneTemplateObject;
+       let cloneTemplateArray = function(org) {
+           let copy, i, found, el, elcopy, arrayLength;
+
+           arrayLength = org.length;
+           found = false;
+           for (i = 0; i < arrayLength; i++) {
+               el = org[i];
+               if (el.constructor === Object && el.xtype) {
+                   found = true;
+                   break;
+               }
+           }
+
+           if (!found) return org; // no need to copy
+
+           copy = [];
+           for (i = 0; i < arrayLength; i++) {
+               el = org[i];
+               if (el.constructor === Object && el.xtype) {
+                   elcopy = cloneTemplateObject(el);
+                   if (elcopy.cbind) {
+                       applyCBind(elcopy);
+                   }
+                   copy.push(elcopy);
+               } else if (el.constructor === Array) {
+                   elcopy = cloneTemplateArray(el);
+                   copy.push(elcopy);
+               } else {
+                   copy.push(el);
+               }
+           }
+           return copy;
+       };
+
+       cloneTemplateObject = function(org) {
+           let res = {}, prop, el, copy;
+           for (prop in org) { // eslint-disable-line guard-for-in
+               el = org[prop];
+               if (el === undefined || el === null) {
+                   res[prop] = el;
+                   continue;
+               }
+               if (el.constructor === Object && el.xtype) {
+                   copy = cloneTemplateObject(el);
+                   if (copy.cbind) {
+                       applyCBind(copy);
+                   }
+                   res[prop] = copy;
+               } else if (el.constructor === Array) {
+                   copy = cloneTemplateArray(el);
+                   res[prop] = copy;
+               } else {
+                   res[prop] = el;
+               }
+           }
+           return res;
+       };
+
+       let condCloneProperties = function() {
+           let prop, el, tmp;
+
+           for (prop in me) { // eslint-disable-line guard-for-in
+               el = me[prop];
+               if (el === undefined || el === null) continue;
+               if (typeof el === 'object' && el.constructor === Object) {
+                   if (el.xtype && prop !== 'config') {
+                       me[prop] = cloneTemplateObject(el);
+                   }
+               } else if (el.constructor === Array) {
+                   tmp = cloneTemplateArray(el);
+                   me[prop] = tmp;
+               }
+           }
+       };
+
+       condCloneProperties();
+    },
+});
diff --git a/src/node/APT.js b/src/node/APT.js
new file mode 100644 (file)
index 0000000..1b564ef
--- /dev/null
@@ -0,0 +1,217 @@
+Ext.define('apt-pkglist', {
+    extend: 'Ext.data.Model',
+    fields: ['Package', 'Title', 'Description', 'Section', 'Arch',
+             'Priority', 'Version', 'OldVersion', 'ChangeLogUrl', 'Origin'],
+    idProperty: 'Package',
+});
+
+Ext.define('Proxmox.node.APT', {
+    extend: 'Ext.grid.GridPanel',
+
+    xtype: 'proxmoxNodeAPT',
+
+    upgradeBtn: undefined,
+
+    columns: [
+       {
+           header: gettext('Package'),
+           width: 200,
+           sortable: true,
+           dataIndex: 'Package',
+       },
+       {
+           text: gettext('Version'),
+           columns: [
+               {
+                   header: gettext('current'),
+                   width: 100,
+                   sortable: false,
+                   dataIndex: 'OldVersion',
+               },
+               {
+                   header: gettext('new'),
+                   width: 100,
+                   sortable: false,
+                   dataIndex: 'Version',
+               },
+           ],
+       },
+       {
+           header: gettext('Description'),
+           sortable: false,
+           dataIndex: 'Title',
+           flex: 1,
+       },
+    ],
+
+    initComponent: function() {
+       let me = this;
+
+       if (!me.nodename) {
+           throw "no node name specified";
+       }
+
+       let store = Ext.create('Ext.data.Store', {
+           model: 'apt-pkglist',
+           groupField: 'Origin',
+           proxy: {
+               type: 'proxmox',
+               url: "/api2/json/nodes/" + me.nodename + "/apt/update",
+           },
+           sorters: [
+               {
+                   property: 'Package',
+                   direction: 'ASC',
+               },
+           ],
+       });
+
+       let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
+            groupHeaderTpl: '{[ "Origin: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})',
+           enableGroupingMenu: false,
+       });
+
+       let rowBodyFeature = Ext.create('Ext.grid.feature.RowBody', {
+            getAdditionalData: function(data, rowIndex, record, orig) {
+               let headerCt = this.view.headerCt;
+               let colspan = headerCt.getColumnCount();
+               return {
+                   rowBody: '<div style="padding: 1em">' +
+                       Ext.String.htmlEncode(data.Description) +
+                       '</div>',
+                   rowBodyCls: me.full_description ? '' : Ext.baseCSSPrefix + 'grid-row-body-hidden',
+                   rowBodyColspan: colspan,
+               };
+           },
+       });
+
+       let reload = function() {
+           store.load();
+       };
+
+       Proxmox.Utils.monStoreErrors(me, store, true);
+
+       let apt_command = function(cmd) {
+           Proxmox.Utils.API2Request({
+               url: "/nodes/" + me.nodename + "/apt/" + cmd,
+               method: 'POST',
+               failure: function(response, opts) {
+                   Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+               },
+               success: function(response, opts) {
+                   let upid = response.result.data;
+
+                   let win = Ext.create('Proxmox.window.TaskViewer', {
+                       upid: upid,
+                   });
+                   win.show();
+                   me.mon(win, 'close', reload);
+               },
+           });
+       };
+
+       let sm = Ext.create('Ext.selection.RowModel', {});
+
+       let update_btn = new Ext.Button({
+           text: gettext('Refresh'),
+           handler: function() {
+               Proxmox.Utils.checked_command(function() { apt_command('update'); });
+           },
+       });
+
+       let show_changelog = function(rec) {
+           if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) {
+               return;
+           }
+
+           let view = Ext.createWidget('component', {
+               autoScroll: true,
+               style: {
+                   'background-color': 'white',
+                   'white-space': 'pre',
+                   'font-family': 'monospace',
+                   padding: '5px',
+               },
+           });
+
+           let win = Ext.create('Ext.window.Window', {
+               title: gettext('Changelog') + ": " + rec.data.Package,
+               width: 800,
+               height: 400,
+               layout: 'fit',
+               modal: true,
+               items: [view],
+           });
+
+           Proxmox.Utils.API2Request({
+               waitMsgTarget: me,
+               url: "/nodes/" + me.nodename + "/apt/changelog",
+               params: {
+                   name: rec.data.Package,
+                   version: rec.data.Version,
+               },
+               method: 'GET',
+               failure: function(response, opts) {
+                   win.close();
+                   Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+               },
+               success: function(response, opts) {
+                   win.show();
+                   view.update(Ext.htmlEncode(response.result.data));
+               },
+           });
+       };
+
+       let changelog_btn = new Proxmox.button.Button({
+           text: gettext('Changelog'),
+           selModel: sm,
+           disabled: true,
+           enableFn: function(rec) {
+               if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) {
+                   return false;
+               }
+               return true;
+           },
+           handler: function(b, e, rec) {
+               show_changelog(rec);
+           },
+       });
+
+       let verbose_desc_checkbox = new Ext.form.field.Checkbox({
+           boxLabel: gettext('Show details'),
+           value: false,
+           listeners: {
+               change: (f, val) => {
+                   me.full_description = val;
+                   me.getView().refresh();
+               },
+           },
+       });
+
+       if (me.upgradeBtn) {
+           me.tbar = [update_btn, me.upgradeBtn, changelog_btn, '->', verbose_desc_checkbox];
+       } else {
+           me.tbar = [update_btn, changelog_btn, '->', verbose_desc_checkbox];
+       }
+
+       Ext.apply(me, {
+           store: store,
+           stateful: true,
+           stateId: 'grid-update',
+           selModel: sm,
+            viewConfig: {
+               stripeRows: false,
+               emptyText: '<div style="display:table; width:100%; height:100%;"><div style="display:table-cell; vertical-align: middle; text-align:center;"><b>' + gettext('No updates available.') + '</div></div>',
+           },
+           features: [groupingFeature, rowBodyFeature],
+           listeners: {
+               activate: reload,
+               itemdblclick: function(v, rec) {
+                   show_changelog(rec);
+               },
+           },
+       });
+
+       me.callParent();
+    },
+});
diff --git a/src/node/DNSEdit.js b/src/node/DNSEdit.js
new file mode 100644 (file)
index 0000000..0b1135e
--- /dev/null
@@ -0,0 +1,54 @@
+Ext.define('Proxmox.node.DNSEdit', {
+    extend: 'Proxmox.window.Edit',
+    alias: ['widget.proxmoxNodeDNSEdit'],
+
+    initComponent: function() {
+       let me = this;
+
+       if (!me.nodename) {
+           throw "no node name specified";
+       }
+
+       me.items = [
+           {
+               xtype: 'textfield',
+                fieldLabel: gettext('Search domain'),
+                name: 'search',
+                allowBlank: false,
+           },
+           {
+               xtype: 'proxmoxtextfield',
+                fieldLabel: gettext('DNS server') + " 1",
+               vtype: 'IP64Address',
+               skipEmptyText: true,
+                name: 'dns1',
+           },
+           {
+               xtype: 'proxmoxtextfield',
+               fieldLabel: gettext('DNS server') + " 2",
+               vtype: 'IP64Address',
+               skipEmptyText: true,
+                name: 'dns2',
+           },
+           {
+               xtype: 'proxmoxtextfield',
+                fieldLabel: gettext('DNS server') + " 3",
+               vtype: 'IP64Address',
+               skipEmptyText: true,
+                name: 'dns3',
+           },
+       ];
+
+       Ext.applyIf(me, {
+           subject: gettext('DNS'),
+           url: "/api2/extjs/nodes/" + me.nodename + "/dns",
+           fieldDefaults: {
+               labelWidth: 120,
+           },
+       });
+
+       me.callParent();
+
+       me.load();
+    },
+});
diff --git a/src/node/DNSView.js b/src/node/DNSView.js
new file mode 100644 (file)
index 0000000..e40e5a1
--- /dev/null
@@ -0,0 +1,61 @@
+Ext.define('Proxmox.node.DNSView', {
+    extend: 'Proxmox.grid.ObjectGrid',
+    alias: ['widget.proxmoxNodeDNSView'],
+
+    initComponent: function() {
+       let me = this;
+
+       if (!me.nodename) {
+           throw "no node name specified";
+       }
+
+       let run_editor = function() {
+           let win = Ext.create('Proxmox.node.DNSEdit', {
+               nodename: me.nodename,
+           });
+           win.show();
+       };
+
+       Ext.apply(me, {
+           url: "/api2/json/nodes/" + me.nodename + "/dns",
+           cwidth1: 130,
+           interval: 1000,
+           run_editor: run_editor,
+           rows: {
+               search: {
+                   header: 'Search domain',
+                   required: true,
+                   renderer: Ext.htmlEncode,
+               },
+               dns1: {
+                   header: gettext('DNS server') + " 1",
+                   required: true,
+                   renderer: Ext.htmlEncode,
+               },
+               dns2: {
+                   header: gettext('DNS server') + " 2",
+                   renderer: Ext.htmlEncode,
+               },
+               dns3: {
+                   header: gettext('DNS server') + " 3",
+                   renderer: Ext.htmlEncode,
+               },
+           },
+           tbar: [
+               {
+                   text: gettext("Edit"),
+                   handler: run_editor,
+               },
+           ],
+           listeners: {
+               itemdblclick: run_editor,
+           },
+       });
+
+       me.callParent();
+
+       me.on('activate', me.rstore.startUpdate);
+       me.on('deactivate', me.rstore.stopUpdate);
+       me.on('destroy', me.rstore.stopUpdate);
+    },
+});
diff --git a/src/node/HostsView.js b/src/node/HostsView.js
new file mode 100644 (file)
index 0000000..9adb6b2
--- /dev/null
@@ -0,0 +1,95 @@
+Ext.define('Proxmox.node.HostsView', {
+    extend: 'Ext.panel.Panel',
+    xtype: 'proxmoxNodeHostsView',
+
+    reload: function() {
+       let me = this;
+       me.store.load();
+    },
+
+    tbar: [
+       {
+           text: gettext('Save'),
+           disabled: true,
+           itemId: 'savebtn',
+           handler: function() {
+               let view = this.up('panel');
+               Proxmox.Utils.API2Request({
+                   params: {
+                       digest: view.digest,
+                       data: view.down('#hostsfield').getValue(),
+                   },
+                   method: 'POST',
+                   url: '/nodes/' + view.nodename + '/hosts',
+                   waitMsgTarget: view,
+                   success: function(response, opts) {
+                       view.reload();
+                   },
+                   failure: function(response, opts) {
+                       Ext.Msg.alert('Error', response.htmlStatus);
+                   },
+               });
+           },
+       },
+       {
+           text: gettext('Revert'),
+           disabled: true,
+           itemId: 'resetbtn',
+           handler: function() {
+               let view = this.up('panel');
+               view.down('#hostsfield').reset();
+           },
+       },
+    ],
+
+           layout: 'fit',
+
+    items: [
+       {
+           xtype: 'textarea',
+           itemId: 'hostsfield',
+           fieldStyle: {
+               'font-family': 'monospace',
+               'white-space': 'pre',
+           },
+           listeners: {
+               dirtychange: function(ta, dirty) {
+                   let view = this.up('panel');
+                   view.down('#savebtn').setDisabled(!dirty);
+                   view.down('#resetbtn').setDisabled(!dirty);
+               },
+           },
+       },
+    ],
+
+    initComponent: function() {
+       let me = this;
+
+       if (!me.nodename) {
+           throw "no node name specified";
+       }
+
+       me.store = Ext.create('Ext.data.Store', {
+           proxy: {
+               type: 'proxmox',
+               url: "/api2/json/nodes/" + me.nodename + "/hosts",
+           },
+       });
+
+       me.callParent();
+
+       Proxmox.Utils.monStoreErrors(me, me.store);
+
+       me.mon(me.store, 'load', function(store, records, success) {
+           if (!success || records.length < 1) {
+               return;
+           }
+           me.digest = records[0].data.digest;
+           let data = records[0].data.data;
+           me.down('#hostsfield').setValue(data);
+           me.down('#hostsfield').resetOriginalValue();
+       });
+
+       me.reload();
+    },
+});
diff --git a/src/node/NetworkEdit.js b/src/node/NetworkEdit.js
new file mode 100644 (file)
index 0000000..72aab6f
--- /dev/null
@@ -0,0 +1,362 @@
+Ext.define('Proxmox.node.NetworkEdit', {
+    extend: 'Proxmox.window.Edit',
+    alias: ['widget.proxmoxNodeNetworkEdit'],
+
+    initComponent: function() {
+       let me = this;
+
+       if (!me.nodename) {
+           throw "no node name specified";
+       }
+
+       if (!me.iftype) {
+           throw "no network device type specified";
+       }
+
+       me.isCreate = !me.iface;
+
+       let iface_vtype;
+
+       if (me.iftype === 'bridge') {
+           iface_vtype = 'BridgeName';
+       } else if (me.iftype === 'bond') {
+           iface_vtype = 'BondName';
+       } else if (me.iftype === 'eth' && !me.isCreate) {
+           iface_vtype = 'InterfaceName';
+       } else if (me.iftype === 'vlan') {
+           iface_vtype = 'VlanName';
+       } else if (me.iftype === 'OVSBridge') {
+           iface_vtype = 'BridgeName';
+       } else if (me.iftype === 'OVSBond') {
+           iface_vtype = 'BondName';
+       } else if (me.iftype === 'OVSIntPort') {
+           iface_vtype = 'InterfaceName';
+       } else if (me.iftype === 'OVSPort') {
+           iface_vtype = 'InterfaceName';
+       } else {
+           console.log(me.iftype);
+           throw "unknown network device type specified";
+       }
+
+       me.subject = Proxmox.Utils.render_network_iface_type(me.iftype);
+
+       let column1 = [],
+           column2 = [],
+           columnB = [],
+           advancedColumn1 = [],
+           advancedColumn2 = [];
+
+       if (!(me.iftype === 'OVSIntPort' || me.iftype === 'OVSPort' || me.iftype === 'OVSBond')) {
+           column2.push({
+               xtype: 'proxmoxcheckbox',
+               fieldLabel: gettext('Autostart'),
+               name: 'autostart',
+               uncheckedValue: 0,
+               checked: me.isCreate ? true : undefined,
+           });
+       }
+
+       if (me.iftype === 'bridge') {
+           column2.push({
+               xtype: 'proxmoxcheckbox',
+               fieldLabel: gettext('VLAN aware'),
+               name: 'bridge_vlan_aware',
+               deleteEmpty: !me.isCreate,
+           });
+           column2.push({
+               xtype: 'textfield',
+               fieldLabel: gettext('Bridge ports'),
+               name: 'bridge_ports',
+           });
+       } else if (me.iftype === 'OVSBridge') {
+           column2.push({
+               xtype: 'textfield',
+               fieldLabel: gettext('Bridge ports'),
+               name: 'ovs_ports',
+           });
+           column2.push({
+               xtype: 'textfield',
+               fieldLabel: gettext('OVS options'),
+               name: 'ovs_options',
+           });
+       } else if (me.iftype === 'OVSPort' || me.iftype === 'OVSIntPort') {
+           column2.push({
+               xtype: me.isCreate ? 'PVE.form.BridgeSelector' : 'displayfield',
+               fieldLabel: Proxmox.Utils.render_network_iface_type('OVSBridge'),
+               allowBlank: false,
+               nodename: me.nodename,
+               bridgeType: 'OVSBridge',
+               name: 'ovs_bridge',
+           });
+           column2.push({
+               xtype: 'pveVlanField',
+               deleteEmpty: !me.isCreate,
+               name: 'ovs_tag',
+               value: '',
+           });
+           column2.push({
+               xtype: 'textfield',
+               fieldLabel: gettext('OVS options'),
+               name: 'ovs_options',
+           });
+       } else if (me.iftype === 'vlan') {
+           if (!me.isCreate) {
+               me.disablevlanid = false;
+               me.disablevlanrawdevice = false;
+               me.vlanrawdevicevalue = '';
+               me.vlanidvalue = '';
+
+               if (Proxmox.Utils.VlanInterface_match.test(me.iface)) {
+                  me.disablevlanid = true;
+                  me.disablevlanrawdevice = true;
+                  let arr = Proxmox.Utils.VlanInterface_match.exec(me.iface);
+                  me.vlanrawdevicevalue = arr[1];
+                  me.vlanidvalue = arr[2];
+               } else if (Proxmox.Utils.Vlan_match.test(me.iface)) {
+                  me.disablevlanid = true;
+                  let arr = Proxmox.Utils.Vlan_match.exec(me.iface);
+                  me.vlanidvalue = arr[1];
+               }
+           } else {
+               me.disablevlanid = true;
+               me.disablevlanrawdevice = true;
+           }
+
+           column2.push({
+               xtype: 'textfield',
+               fieldLabel: gettext('Vlan raw device'),
+               name: 'vlan-raw-device',
+               value: me.vlanrawdevicevalue,
+               disabled: me.disablevlanrawdevice,
+           });
+
+           column2.push({
+               xtype: 'pveVlanField',
+               name: 'vlan-id',
+               value: me.vlanidvalue,
+               disabled: me.disablevlanid,
+           });
+
+           columnB.push({
+               xtype: 'label',
+               userCls: 'pmx-hint',
+               text: 'Either add the VLAN number to an existing interface name, or choose your own name and set the VLAN raw device (for the latter ifupdown1 supports vlanXY naming only)',
+           });
+       } else if (me.iftype === 'bond') {
+           column2.push({
+               xtype: 'textfield',
+               fieldLabel: gettext('Slaves'),
+               name: 'slaves',
+           });
+
+           let policySelector = Ext.createWidget('bondPolicySelector', {
+               fieldLabel: gettext('Hash policy'),
+               name: 'bond_xmit_hash_policy',
+               deleteEmpty: !me.isCreate,
+               disabled: true,
+           });
+
+           let primaryfield = Ext.createWidget('textfield', {
+               fieldLabel: gettext('bond-primary'),
+               name: 'bond-primary',
+               value: '',
+               disabled: true,
+           });
+
+           column2.push({
+               xtype: 'bondModeSelector',
+               fieldLabel: gettext('Mode'),
+               name: 'bond_mode',
+               value: me.isCreate ? 'balance-rr' : undefined,
+               listeners: {
+                   change: function(f, value) {
+                       if (value === 'balance-xor' ||
+                           value === '802.3ad') {
+                           policySelector.setDisabled(false);
+                           primaryfield.setDisabled(true);
+                           primaryfield.setValue('');
+                       } else if (value === 'active-backup') {
+                           primaryfield.setDisabled(false);
+                           policySelector.setDisabled(true);
+                           policySelector.setValue('');
+                       } else {
+                           policySelector.setDisabled(true);
+                           policySelector.setValue('');
+                           primaryfield.setDisabled(true);
+                           primaryfield.setValue('');
+                       }
+                   },
+               },
+               allowBlank: false,
+           });
+
+           column2.push(policySelector);
+           column2.push(primaryfield);
+       } else if (me.iftype === 'OVSBond') {
+           column2.push({
+               xtype: me.isCreate ? 'PVE.form.BridgeSelector' : 'displayfield',
+               fieldLabel: Proxmox.Utils.render_network_iface_type('OVSBridge'),
+               allowBlank: false,
+               nodename: me.nodename,
+               bridgeType: 'OVSBridge',
+               name: 'ovs_bridge',
+           });
+           column2.push({
+               xtype: 'pveVlanField',
+               deleteEmpty: !me.isCreate,
+               name: 'ovs_tag',
+               value: '',
+           });
+           column2.push({
+               xtype: 'textfield',
+               fieldLabel: gettext('OVS options'),
+               name: 'ovs_options',
+           });
+       }
+
+       column2.push({
+           xtype: 'textfield',
+           fieldLabel: gettext('Comment'),
+           allowBlank: true,
+           nodename: me.nodename,
+           name: 'comments',
+       });
+
+       let url;
+       let method;
+
+       if (me.isCreate) {
+           url = "/api2/extjs/nodes/" + me.nodename + "/network";
+           method = 'POST';
+       } else {
+           url = "/api2/extjs/nodes/" + me.nodename + "/network/" + me.iface;
+           method = 'PUT';
+       }
+
+       column1.push({
+           xtype: 'hiddenfield',
+           name: 'type',
+           value: me.iftype,
+       },
+       {
+           xtype: me.isCreate ? 'textfield' : 'displayfield',
+           fieldLabel: gettext('Name'),
+           name: 'iface',
+           value: me.iface,
+           vtype: iface_vtype,
+           allowBlank: false,
+           listeners: {
+               change: function(f, value) {
+                   if (me.isCreate && iface_vtype === 'VlanName') {
+                       let vlanidField = me.down('field[name=vlan-id]');
+                       let vlanrawdeviceField = me.down('field[name=vlan-raw-device]');
+                       if (Proxmox.Utils.VlanInterface_match.test(value)) {
+                           vlanidField.setDisabled(true);
+                           vlanrawdeviceField.setDisabled(true);
+                       } else if (Proxmox.Utils.Vlan_match.test(value)) {
+                           vlanidField.setDisabled(true);
+                           vlanrawdeviceField.setDisabled(false);
+                       } else {
+                           vlanidField.setDisabled(false);
+                           vlanrawdeviceField.setDisabled(false);
+                       }
+                   }
+               },
+           },
+       });
+
+       if (me.iftype === 'OVSBond') {
+           column1.push(
+               {
+                   xtype: 'bondModeSelector',
+                   fieldLabel: gettext('Mode'),
+                   name: 'bond_mode',
+                   openvswitch: true,
+                   value: me.isCreate ? 'active-backup' : undefined,
+                   allowBlank: false,
+               },
+               {
+                   xtype: 'textfield',
+                   fieldLabel: gettext('Slaves'),
+                   name: 'ovs_bonds',
+               },
+           );
+       } else {
+           column1.push(
+               {
+                   xtype: 'proxmoxtextfield',
+                   deleteEmpty: !me.isCreate,
+                   fieldLabel: 'IPv4/CIDR',
+                   vtype: 'IPCIDRAddress',
+                   name: 'cidr',
+               },
+               {
+                   xtype: 'proxmoxtextfield',
+                   deleteEmpty: !me.isCreate,
+                   fieldLabel: gettext('Gateway') + ' (IPv4)',
+                   vtype: 'IPAddress',
+                   name: 'gateway',
+               },
+               {
+                   xtype: 'proxmoxtextfield',
+                   deleteEmpty: !me.isCreate,
+                   fieldLabel: 'IPv6/CIDR',
+                   vtype: 'IP6CIDRAddress',
+                   name: 'cidr6',
+               },
+               {
+                   xtype: 'proxmoxtextfield',
+                   deleteEmpty: !me.isCreate,
+                   fieldLabel: gettext('Gateway') + ' (IPv6)',
+                   vtype: 'IP6Address',
+                   name: 'gateway6',
+               },
+           );
+           advancedColumn1.push(
+               {
+                   xtype: 'proxmoxintegerfield',
+                   minValue: 1280,
+                   maxValue: 65520,
+                   deleteEmpty: !me.isCreate,
+                   emptyText: 1500,
+                   fieldLabel: 'MTU',
+                   name: 'mtu',
+               },
+           );
+       }
+
+       Ext.applyIf(me, {
+           url: url,
+           method: method,
+           items: {
+                xtype: 'inputpanel',
+               column1: column1,
+               column2: column2,
+               columnB: columnB,
+               advancedColumn1: advancedColumn1,
+               advancedColumn2: advancedColumn2,
+           },
+       });
+
+       me.callParent();
+
+       if (me.isCreate) {
+           me.down('field[name=iface]').setValue(me.iface_default);
+       } else {
+           me.load({
+               success: function(response, options) {
+                   let data = response.result.data;
+                   if (data.type !== me.iftype) {
+                       let msg = "Got unexpected device type";
+                       Ext.Msg.alert(gettext('Error'), msg, function() {
+                           me.close();
+                       });
+                       return;
+                   }
+                   me.setValues(data);
+                   me.isValid(); // trigger validation
+               },
+           });
+       }
+    },
+});
diff --git a/src/node/NetworkView.js b/src/node/NetworkView.js
new file mode 100644 (file)
index 0000000..886b8de
--- /dev/null
@@ -0,0 +1,465 @@
+Ext.define('proxmox-networks', {
+    extend: 'Ext.data.Model',
+    fields: [
+       'active',
+       'address',
+       'address6',
+       'autostart',
+       'bridge_ports',
+       'cidr',
+       'cidr6',
+       'comments',
+       'gateway',
+       'gateway6',
+       'iface',
+       'netmask',
+       'netmask6',
+       'slaves',
+       'type',
+    ],
+    idProperty: 'iface',
+});
+
+Ext.define('Proxmox.node.NetworkView', {
+    extend: 'Ext.panel.Panel',
+
+    alias: ['widget.proxmoxNodeNetworkView'],
+
+    // defines what types of network devices we want to create
+    // order is always the same
+    types: ['bridge', 'bond', 'vlan', 'ovs'],
+
+    showApplyBtn: false,
+
+    initComponent: function() {
+       let me = this;
+
+       if (!me.nodename) {
+           throw "no node name specified";
+       }
+
+       let baseUrl = '/nodes/' + me.nodename + '/network';
+
+       let store = Ext.create('Ext.data.Store', {
+           model: 'proxmox-networks',
+           proxy: {
+                type: 'proxmox',
+                url: '/api2/json' + baseUrl,
+           },
+           sorters: [
+               {
+                   property: 'iface',
+                   direction: 'ASC',
+               },
+           ],
+       });
+
+       let reload = function() {
+           let changeitem = me.down('#changes');
+           let apply_btn = me.down('#apply');
+           let revert_btn = me.down('#revert');
+           Proxmox.Utils.API2Request({
+               url: baseUrl,
+               failure: function(response, opts) {
+                   store.loadData({});
+                   Proxmox.Utils.setErrorMask(me, response.htmlStatus);
+                   changeitem.update('');
+                   changeitem.setHidden(true);
+               },
+               success: function(response, opts) {
+                   let result = Ext.decode(response.responseText);
+                   store.loadData(result.data);
+                   let changes = result.changes;
+                   if (changes === undefined || changes === '') {
+                       changes = gettext("No changes");
+                       changeitem.setHidden(true);
+                       apply_btn.setDisabled(true);
+                       revert_btn.setDisabled(true);
+                   } else {
+                       changeitem.update("<pre>" + Ext.htmlEncode(changes) + "</pre>");
+                       changeitem.setHidden(false);
+                       apply_btn.setDisabled(false);
+                       revert_btn.setDisabled(false);
+                   }
+               },
+           });
+       };
+
+       let run_editor = function() {
+           let grid = me.down('gridpanel');
+           let sm = grid.getSelectionModel();
+           let rec = sm.getSelection()[0];
+           if (!rec) {
+               return;
+           }
+
+           let win = Ext.create('Proxmox.node.NetworkEdit', {
+               nodename: me.nodename,
+               iface: rec.data.iface,
+               iftype: rec.data.type,
+           });
+           win.show();
+           win.on('destroy', reload);
+       };
+
+       let edit_btn = new Ext.Button({
+           text: gettext('Edit'),
+           disabled: true,
+           handler: run_editor,
+       });
+
+       let del_btn = new Ext.Button({
+           text: gettext('Remove'),
+           disabled: true,
+           handler: function() {
+               let grid = me.down('gridpanel');
+               let sm = grid.getSelectionModel();
+               let rec = sm.getSelection()[0];
+               if (!rec) {
+                   return;
+               }
+
+               let iface = rec.data.iface;
+
+               Proxmox.Utils.API2Request({
+                   url: baseUrl + '/' + iface,
+                   method: 'DELETE',
+                   waitMsgTarget: me,
+                   callback: function() {
+                       reload();
+                   },
+                   failure: function(response, opts) {
+                       Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+                   },
+               });
+           },
+       });
+
+       let apply_btn = Ext.create('Proxmox.button.Button', {
+           text: gettext('Apply Configuration'),
+           itemId: 'apply',
+           disabled: true,
+           confirmMsg: 'Do you want to apply pending network changes?',
+           hidden: !me.showApplyBtn,
+           handler: function() {
+               Proxmox.Utils.API2Request({
+                   url: baseUrl,
+                   method: 'PUT',
+                   waitMsgTarget: me,
+                   success: function(response, opts) {
+                       let upid = response.result.data;
+
+                       let win = Ext.create('Proxmox.window.TaskProgress', {
+                           taskDone: reload,
+                           upid: upid,
+                       });
+                       win.show();
+                   },
+                   failure: function(response, opts) {
+                       Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+                   },
+               });
+           },
+       });
+
+       let set_button_status = function() {
+           let grid = me.down('gridpanel');
+           let sm = grid.getSelectionModel();
+           let rec = sm.getSelection()[0];
+
+           edit_btn.setDisabled(!rec);
+           del_btn.setDisabled(!rec);
+       };
+
+       let render_ports = function(value, metaData, record) {
+           if (value === 'bridge') {
+               return record.data.bridge_ports;
+           } else if (value === 'bond') {
+               return record.data.slaves;
+           } else if (value === 'OVSBridge') {
+               return record.data.ovs_ports;
+           } else if (value === 'OVSBond') {
+               return record.data.ovs_bonds;
+           }
+           return '';
+       };
+
+       let find_next_iface_id = function(prefix) {
+           let next;
+           for (next = 0; next <= 9999; next++) {
+               if (!store.getById(prefix + next.toString())) {
+                   break;
+               }
+           }
+           return prefix + next.toString();
+       };
+
+       let menu_items = [];
+
+       if (me.types.indexOf('bridge') !== -1) {
+           menu_items.push({
+               text: Proxmox.Utils.render_network_iface_type('bridge'),
+               handler: function() {
+                   let win = Ext.create('Proxmox.node.NetworkEdit', {
+                       nodename: me.nodename,
+                       iftype: 'bridge',
+                       iface_default: find_next_iface_id('vmbr'),
+                       onlineHelp: 'sysadmin_network_configuration',
+                   });
+                   win.on('destroy', reload);
+                   win.show();
+               },
+           });
+       }
+
+       if (me.types.indexOf('bond') !== -1) {
+           menu_items.push({
+               text: Proxmox.Utils.render_network_iface_type('bond'),
+               handler: function() {
+                   let win = Ext.create('Proxmox.node.NetworkEdit', {
+                       nodename: me.nodename,
+                       iftype: 'bond',
+                       iface_default: find_next_iface_id('bond'),
+                       onlineHelp: 'sysadmin_network_configuration',
+                   });
+                   win.on('destroy', reload);
+                   win.show();
+               },
+           });
+       }
+
+       if (me.types.indexOf('vlan') !== -1) {
+           menu_items.push({
+               text: Proxmox.Utils.render_network_iface_type('vlan'),
+               handler: function() {
+                   let win = Ext.create('Proxmox.node.NetworkEdit', {
+                       nodename: me.nodename,
+                       iftype: 'vlan',
+                       iface_default: 'interfaceX.1',
+                       onlineHelp: 'sysadmin_network_configuration',
+                   });
+                   win.on('destroy', reload);
+                   win.show();
+               },
+           });
+       }
+
+       if (me.types.indexOf('ovs') !== -1) {
+           if (menu_items.length > 0) {
+               menu_items.push({ xtype: 'menuseparator' });
+           }
+
+           menu_items.push(
+               {
+                   text: Proxmox.Utils.render_network_iface_type('OVSBridge'),
+                   handler: function() {
+                       let win = Ext.create('Proxmox.node.NetworkEdit', {
+                           nodename: me.nodename,
+                           iftype: 'OVSBridge',
+                           iface_default: find_next_iface_id('vmbr'),
+                       });
+                       win.on('destroy', reload);
+                       win.show();
+                   },
+               },
+               {
+                   text: Proxmox.Utils.render_network_iface_type('OVSBond'),
+                   handler: function() {
+                       let win = Ext.create('Proxmox.node.NetworkEdit', {
+                           nodename: me.nodename,
+                           iftype: 'OVSBond',
+                           iface_default: find_next_iface_id('bond'),
+                       });
+                       win.on('destroy', reload);
+                       win.show();
+                   },
+               },
+               {
+                   text: Proxmox.Utils.render_network_iface_type('OVSIntPort'),
+                   handler: function() {
+                       let win = Ext.create('Proxmox.node.NetworkEdit', {
+                           nodename: me.nodename,
+                           iftype: 'OVSIntPort',
+                       });
+                       win.on('destroy', reload);
+                       win.show();
+                   },
+               },
+           );
+       }
+
+       let renderer_generator = function(fieldname) {
+           return function(val, metaData, rec) {
+               let tmp = [];
+               if (rec.data[fieldname]) {
+                   tmp.push(rec.data[fieldname]);
+               }
+               if (rec.data[fieldname + '6']) {
+                   tmp.push(rec.data[fieldname + '6']);
+               }
+               return tmp.join('<br>') || '';
+           };
+       };
+
+       Ext.apply(me, {
+           layout: 'border',
+           tbar: [
+               {
+                   text: gettext('Create'),
+                   menu: {
+                       plain: true,
+                       items: menu_items,
+                   },
+               }, '-',
+               {
+                   text: gettext('Revert'),
+                   itemId: 'revert',
+                   handler: function() {
+                       Proxmox.Utils.API2Request({
+                           url: baseUrl,
+                           method: 'DELETE',
+                           waitMsgTarget: me,
+                           callback: function() {
+                               reload();
+                           },
+                           failure: function(response, opts) {
+                               Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+                           },
+                       });
+                   },
+               },
+               edit_btn,
+               del_btn,
+               '-',
+               apply_btn,
+           ],
+           items: [
+               {
+                   xtype: 'gridpanel',
+                   stateful: true,
+                   stateId: 'grid-node-network',
+                   store: store,
+                   region: 'center',
+                   border: false,
+                   columns: [
+                       {
+                           header: gettext('Name'),
+                           sortable: true,
+                           dataIndex: 'iface',
+                       },
+                       {
+                           header: gettext('Type'),
+                           sortable: true,
+                           width: 120,
+                           renderer: Proxmox.Utils.render_network_iface_type,
+                           dataIndex: 'type',
+                       },
+                       {
+                           xtype: 'booleancolumn',
+                           header: gettext('Active'),
+                           width: 80,
+                           sortable: true,
+                           dataIndex: 'active',
+                           trueText: Proxmox.Utils.yesText,
+                           falseText: Proxmox.Utils.noText,
+                           undefinedText: Proxmox.Utils.noText,
+                       },
+                       {
+                           xtype: 'booleancolumn',
+                           header: gettext('Autostart'),
+                           width: 80,
+                           sortable: true,
+                           dataIndex: 'autostart',
+                           trueText: Proxmox.Utils.yesText,
+                           falseText: Proxmox.Utils.noText,
+                           undefinedText: Proxmox.Utils.noText,
+                       },
+                       {
+                           xtype: 'booleancolumn',
+                           header: gettext('VLAN aware'),
+                           width: 80,
+                           sortable: true,
+                           dataIndex: 'bridge_vlan_aware',
+                           trueText: Proxmox.Utils.yesText,
+                           falseText: Proxmox.Utils.noText,
+                           undefinedText: Proxmox.Utils.noText,
+                       },
+                       {
+                           header: gettext('Ports/Slaves'),
+                           dataIndex: 'type',
+                           renderer: render_ports,
+                       },
+                       {
+                           header: gettext('Bond Mode'),
+                           dataIndex: 'bond_mode',
+                           renderer: Proxmox.Utils.render_bond_mode,
+                       },
+                       {
+                           header: gettext('Hash Policy'),
+                           hidden: true,
+                           dataIndex: 'bond_xmit_hash_policy',
+                       },
+                       {
+                           header: gettext('IP address'),
+                           sortable: true,
+                           width: 120,
+                           hidden: true,
+                           dataIndex: 'address',
+                           renderer: renderer_generator('address'),
+                       },
+                       {
+                           header: gettext('Subnet mask'),
+                           width: 120,
+                           sortable: true,
+                           hidden: true,
+                           dataIndex: 'netmask',
+                           renderer: renderer_generator('netmask'),
+                       },
+                       {
+                           header: gettext('CIDR'),
+                           width: 150,
+                           sortable: true,
+                           dataIndex: 'cidr',
+                           renderer: renderer_generator('cidr'),
+                       },
+                       {
+                           header: gettext('Gateway'),
+                           width: 150,
+                           sortable: true,
+                           dataIndex: 'gateway',
+                           renderer: renderer_generator('gateway'),
+                       },
+                       {
+                           header: gettext('Comment'),
+                           dataIndex: 'comments',
+                           flex: 1,
+                           renderer: Ext.String.htmlEncode,
+                       },
+                   ],
+                   listeners: {
+                       selectionchange: set_button_status,
+                       itemdblclick: run_editor,
+                   },
+               },
+               {
+                   border: false,
+                   region: 'south',
+                   autoScroll: true,
+                   hidden: true,
+                   itemId: 'changes',
+                   tbar: [
+                       gettext('Pending changes') + ' (' +
+                           gettext("Either reboot or use 'Apply Configuration' (needs ifupdown2) to activate") + ')',
+                   ],
+                   split: true,
+                   bodyPadding: 5,
+                   flex: 0.6,
+                   html: gettext("No changes"),
+               },
+           ],
+       });
+
+       me.callParent();
+       reload();
+    },
+});
diff --git a/src/node/ServiceView.js b/src/node/ServiceView.js
new file mode 100644 (file)
index 0000000..88026a4
--- /dev/null
@@ -0,0 +1,183 @@
+Ext.define('proxmox-services', {
+    extend: 'Ext.data.Model',
+    fields: ['service', 'name', 'desc', 'state'],
+    idProperty: 'service',
+});
+
+Ext.define('Proxmox.node.ServiceView', {
+    extend: 'Ext.grid.GridPanel',
+
+    alias: ['widget.proxmoxNodeServiceView'],
+
+    startOnlyServices: {},
+
+    initComponent: function() {
+       let me = this;
+
+       if (!me.nodename) {
+           throw "no node name specified";
+       }
+
+       let rstore = Ext.create('Proxmox.data.UpdateStore', {
+           interval: 1000,
+           storeid: 'proxmox-services' + me.nodename,
+           model: 'proxmox-services',
+           proxy: {
+                type: 'proxmox',
+                url: "/api2/json/nodes/" + me.nodename + "/services",
+           },
+       });
+
+       let store = Ext.create('Proxmox.data.DiffStore', {
+           rstore: rstore,
+           sortAfterUpdate: true,
+           sorters: [
+               {
+                   property: 'name',
+                   direction: 'ASC',
+               },
+           ],
+       });
+
+       let view_service_log = function() {
+           let sm = me.getSelectionModel();
+           let rec = sm.getSelection()[0];
+           let win = Ext.create('Ext.window.Window', {
+               title: gettext('Syslog') + ': ' + rec.data.service,
+               modal: true,
+               width: 800,
+               height: 400,
+               layout: 'fit',
+               items: {
+                   xtype: 'proxmoxLogView',
+                   url: "/api2/extjs/nodes/" + me.nodename + "/syslog?service=" +
+                       rec.data.service,
+                   log_select_timespan: 1,
+               },
+           });
+           win.show();
+       };
+
+       let service_cmd = function(cmd) {
+           let sm = me.getSelectionModel();
+           let rec = sm.getSelection()[0];
+           Proxmox.Utils.API2Request({
+               url: "/nodes/" + me.nodename + "/services/" + rec.data.service + "/" + cmd,
+               method: 'POST',
+               failure: function(response, opts) {
+                   Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+                   me.loading = true;
+               },
+               success: function(response, opts) {
+                   rstore.startUpdate();
+                   let upid = response.result.data;
+
+                   let win = Ext.create('Proxmox.window.TaskProgress', {
+                       upid: upid,
+                   });
+                   win.show();
+               },
+           });
+       };
+
+       let start_btn = new Ext.Button({
+           text: gettext('Start'),
+           disabled: true,
+           handler: function() {
+               service_cmd("start");
+           },
+       });
+
+       let stop_btn = new Ext.Button({
+           text: gettext('Stop'),
+           disabled: true,
+           handler: function() {
+               service_cmd("stop");
+           },
+       });
+
+       let restart_btn = new Ext.Button({
+           text: gettext('Restart'),
+           disabled: true,
+           handler: function() {
+               service_cmd("restart");
+           },
+       });
+
+       let syslog_btn = new Ext.Button({
+           text: gettext('Syslog'),
+           disabled: true,
+           handler: view_service_log,
+       });
+
+       let set_button_status = function() {
+           let sm = me.getSelectionModel();
+           let rec = sm.getSelection()[0];
+
+           if (!rec) {
+               start_btn.disable();
+               stop_btn.disable();
+               restart_btn.disable();
+               syslog_btn.disable();
+               return;
+           }
+           let service = rec.data.service;
+           let state = rec.data.state;
+
+           syslog_btn.enable();
+
+           if (state === 'running') {
+               start_btn.disable();
+               restart_btn.enable();
+           } else {
+               start_btn.enable();
+               restart_btn.disable();
+           }
+           if (!me.startOnlyServices[service]) {
+               if (state === 'running') {
+                   stop_btn.enable();
+               } else {
+                   stop_btn.disable();
+               }
+           }
+       };
+
+       me.mon(store, 'refresh', set_button_status);
+
+       Proxmox.Utils.monStoreErrors(me, rstore);
+
+       Ext.apply(me, {
+           store: store,
+           stateful: false,
+           tbar: [start_btn, stop_btn, restart_btn, syslog_btn],
+           columns: [
+               {
+                   header: gettext('Name'),
+                   flex: 1,
+                   sortable: true,
+                   dataIndex: 'name',
+               },
+               {
+                   header: gettext('Status'),
+                   width: 100,
+                   sortable: true,
+                   dataIndex: 'state',
+               },
+               {
+                   header: gettext('Description'),
+                   renderer: Ext.String.htmlEncode,
+                   dataIndex: 'desc',
+                   flex: 2,
+               },
+           ],
+           listeners: {
+               selectionchange: set_button_status,
+               itemdblclick: view_service_log,
+               activate: rstore.startUpdate,
+               destroy: rstore.stopUpdate,
+           },
+       });
+
+       me.callParent();
+    },
+});
diff --git a/src/node/Tasks.js b/src/node/Tasks.js
new file mode 100644 (file)
index 0000000..3d9267e
--- /dev/null
@@ -0,0 +1,188 @@
+Ext.define('Proxmox.node.Tasks', {
+    extend: 'Ext.grid.GridPanel',
+
+    alias: ['widget.proxmoxNodeTasks'],
+    stateful: true,
+    stateId: 'grid-node-tasks',
+    loadMask: true,
+    sortableColumns: false,
+    vmidFilter: 0,
+
+    initComponent: function() {
+       let me = this;
+
+       if (!me.nodename) {
+           throw "no node name specified";
+       }
+
+       let store = Ext.create('Ext.data.BufferedStore', {
+           pageSize: 500,
+           autoLoad: true,
+           remoteFilter: true,
+           model: 'proxmox-tasks',
+           proxy: {
+                type: 'proxmox',
+               startParam: 'start',
+               limitParam: 'limit',
+                url: "/api2/json/nodes/" + me.nodename + "/tasks",
+           },
+       });
+
+       let userfilter = '';
+       let filter_errors = 0;
+
+       let updateProxyParams = function() {
+           let params = {
+               errors: filter_errors,
+           };
+           if (userfilter) {
+               params.userfilter = userfilter;
+           }
+           if (me.vmidFilter) {
+               params.vmid = me.vmidFilter;
+           }
+           store.proxy.extraParams = params;
+       };
+
+       updateProxyParams();
+
+       let reload_task = Ext.create('Ext.util.DelayedTask', function() {
+           updateProxyParams();
+           store.reload();
+       });
+
+       let run_task_viewer = function() {
+           let sm = me.getSelectionModel();
+           let rec = sm.getSelection()[0];
+           if (!rec) {
+               return;
+           }
+
+           let win = Ext.create('Proxmox.window.TaskViewer', {
+               upid: rec.data.upid,
+           });
+           win.show();
+       };
+
+       let view_btn = new Ext.Button({
+           text: gettext('View'),
+           disabled: true,
+           handler: run_task_viewer,
+       });
+
+       Proxmox.Utils.monStoreErrors(me, store, true);
+
+       Ext.apply(me, {
+           store: store,
+           viewConfig: {
+               trackOver: false,
+               stripeRows: false, // does not work with getRowClass()
+
+               getRowClass: function(record, index) {
+                   let status = record.get('status');
+
+                   if (status && status !== 'OK') {
+                       return "proxmox-invalid-row";
+                   }
+                   return '';
+               },
+           },
+           tbar: [
+               view_btn,
+               {
+                   text: gettext('Refresh'), // FIXME: smart-auto-refresh store
+                   handler: () => store.reload(),
+               },
+               '->',
+               gettext('User name') +':',
+               ' ',
+               {
+                   xtype: 'textfield',
+                   width: 200,
+                   value: userfilter,
+                   enableKeyEvents: true,
+                   listeners: {
+                       keyup: function(field, e) {
+                           userfilter = field.getValue();
+                           reload_task.delay(500);
+                       },
+                   },
+               }, ' ', gettext('Only Errors') + ':', ' ',
+               {
+                   xtype: 'checkbox',
+                   hideLabel: true,
+                   checked: filter_errors,
+                   listeners: {
+                       change: function(field, checked) {
+                           filter_errors = checked ? 1 : 0;
+                           reload_task.delay(10);
+                       },
+                   },
+               }, ' ',
+           ],
+           columns: [
+               {
+                   header: gettext("Start Time"),
+                   dataIndex: 'starttime',
+                   width: 130,
+                   renderer: function(value) {
+                       return Ext.Date.format(value, "M d H:i:s");
+                   },
+               },
+               {
+                   header: gettext("End Time"),
+                   dataIndex: 'endtime',
+                   width: 130,
+                   renderer: function(value, metaData, record) {
+                       if (!value) {
+                           metaData.tdCls = "x-grid-row-loading";
+                           return '';
+                       }
+                       return Ext.Date.format(value, "M d H:i:s");
+                   },
+               },
+               {
+                   header: gettext("Node"),
+                   dataIndex: 'node',
+                   width: 120,
+               },
+               {
+                   header: gettext("User name"),
+                   dataIndex: 'user',
+                   width: 150,
+               },
+               {
+                   header: gettext("Description"),
+                   dataIndex: 'upid',
+                   flex: 1,
+                   renderer: Proxmox.Utils.render_upid,
+               },
+               {
+                   header: gettext("Status"),
+                   dataIndex: 'status',
+                   width: 200,
+                   renderer: function(value, metaData, record) {
+                       if (value === 'OK') {
+                           return 'OK';
+                       }
+                       if (value === undefined && !record.data.endtime) {
+                           metaData.tdCls = "x-grid-row-loading";
+                           return '';
+                       }
+                       return "ERROR: " + value;
+                   },
+               },
+           ],
+           listeners: {
+               itemdblclick: run_task_viewer,
+               selectionchange: function(v, selections) {
+                   view_btn.setDisabled(!(selections && selections[0]));
+               },
+               show: function() { reload_task.delay(10); },
+               destroy: function() { reload_task.cancel(); },
+           },
+       });
+
+       me.callParent();
+    },
+});
diff --git a/src/node/TimeEdit.js b/src/node/TimeEdit.js
new file mode 100644 (file)
index 0000000..613a2f5
--- /dev/null
@@ -0,0 +1,38 @@
+Ext.define('Proxmox.node.TimeEdit', {
+    extend: 'Proxmox.window.Edit',
+    alias: ['widget.proxmoxNodeTimeEdit'],
+
+    subject: gettext('Time zone'),
+
+    width: 400,
+
+    autoLoad: true,
+
+    fieldDefaults: {
+       labelWidth: 70,
+    },
+
+    items: {
+       xtype: 'combo',
+       fieldLabel: gettext('Time zone'),
+       name: 'timezone',
+       queryMode: 'local',
+       store: Ext.create('Proxmox.data.TimezoneStore'),
+       displayField: 'zone',
+       editable: true,
+       anyMatch: true,
+       forceSelection: true,
+       allowBlank: false,
+    },
+
+    initComponent: function() {
+       let me = this;
+
+       if (!me.nodename) {
+           throw "no node name specified";
+       }
+       me.url = "/api2/extjs/nodes/" + me.nodename + "/time";
+
+       me.callParent();
+    },
+});
diff --git a/src/node/TimeView.js b/src/node/TimeView.js
new file mode 100644 (file)
index 0000000..9033d13
--- /dev/null
@@ -0,0 +1,58 @@
+Ext.define('Proxmox.node.TimeView', {
+    extend: 'Proxmox.grid.ObjectGrid',
+    alias: ['widget.proxmoxNodeTimeView'],
+
+    initComponent: function() {
+       let me = this;
+
+       if (!me.nodename) {
+           throw "no node name specified";
+       }
+
+       let tzoffset = new Date().getTimezoneOffset()*60000;
+       let renderlocaltime = function(value) {
+           let servertime = new Date((value * 1000) + tzoffset);
+           return Ext.Date.format(servertime, 'Y-m-d H:i:s');
+       };
+
+       let run_editor = function() {
+           let win = Ext.create('Proxmox.node.TimeEdit', {
+               nodename: me.nodename,
+           });
+           win.show();
+       };
+
+       Ext.apply(me, {
+           url: "/api2/json/nodes/" + me.nodename + "/time",
+           cwidth1: 150,
+           interval: 1000,
+           run_editor: run_editor,
+           rows: {
+               timezone: {
+                   header: gettext('Time zone'),
+                   required: true,
+               },
+               localtime: {
+                   header: gettext('Server time'),
+                   required: true,
+                   renderer: renderlocaltime,
+               },
+           },
+           tbar: [
+               {
+                   text: gettext("Edit"),
+                   handler: run_editor,
+               },
+           ],
+           listeners: {
+               itemdblclick: run_editor,
+           },
+       });
+
+       me.callParent();
+
+       me.on('activate', me.rstore.startUpdate);
+       me.on('deactivate', me.rstore.stopUpdate);
+       me.on('destroy', me.rstore.stopUpdate);
+    },
+});
diff --git a/src/panel/GaugeWidget.js b/src/panel/GaugeWidget.js
new file mode 100644 (file)
index 0000000..6cd6b60
--- /dev/null
@@ -0,0 +1,102 @@
+Ext.define('Proxmox.panel.GaugeWidget', {
+    extend: 'Ext.panel.Panel',
+    alias: 'widget.proxmoxGauge',
+
+    defaults: {
+       style: {
+           'text-align': 'center',
+       },
+    },
+    items: [
+       {
+           xtype: 'box',
+           itemId: 'title',
+           data: {
+               title: '',
+           },
+           tpl: '<h3>{title}</h3>',
+       },
+       {
+           xtype: 'polar',
+           height: 120,
+           border: false,
+           itemId: 'chart',
+           series: [{
+               type: 'gauge',
+               value: 0,
+               colors: ['#f5f5f5'],
+               sectors: [0],
+               donut: 90,
+               needleLength: 100,
+               totalAngle: Math.PI,
+           }],
+           sprites: [{
+               id: 'valueSprite',
+               type: 'text',
+               text: '',
+               textAlign: 'center',
+               textBaseline: 'bottom',
+               x: 125,
+               y: 110,
+               fontSize: 30,
+           }],
+       },
+       {
+           xtype: 'box',
+           itemId: 'text',
+       },
+    ],
+
+    header: false,
+    border: false,
+
+    warningThreshold: 0.6,
+    criticalThreshold: 0.9,
+    warningColor: '#fc0',
+    criticalColor: '#FF6C59',
+    defaultColor: '#c2ddf2',
+    backgroundColor: '#f5f5f5',
+
+    initialValue: 0,
+
+
+    updateValue: function(value, text) {
+       let me = this;
+       let color = me.defaultColor;
+       let attr = {};
+
+       if (value >= me.criticalThreshold) {
+           color = me.criticalColor;
+       } else if (value >= me.warningThreshold) {
+           color = me.warningColor;
+       }
+
+       me.chart.series[0].setColors([color, me.backgroundColor]);
+       me.chart.series[0].setValue(value*100);
+
+       me.valueSprite.setText(' '+(value*100).toFixed(0) + '%');
+       attr.x = me.chart.getWidth()/2;
+       attr.y = me.chart.getHeight()-20;
+       if (me.spriteFontSize) {
+           attr.fontSize = me.spriteFontSize;
+       }
+       me.valueSprite.setAttributes(attr, true);
+
+       if (text !== undefined) {
+           me.text.setHtml(text);
+       }
+    },
+
+    initComponent: function() {
+       let me = this;
+
+       me.callParent();
+
+       if (me.title) {
+           me.getComponent('title').update({ title: me.title });
+       }
+       me.text = me.getComponent('text');
+       me.chart = me.getComponent('chart');
+       me.valueSprite = me.chart.getSurface('chart').get('valueSprite');
+    },
+});
diff --git a/src/panel/InputPanel.js b/src/panel/InputPanel.js
new file mode 100644 (file)
index 0000000..0ac5e48
--- /dev/null
@@ -0,0 +1,241 @@
+Ext.define('Proxmox.panel.InputPanel', {
+    extend: 'Ext.panel.Panel',
+    alias: ['widget.inputpanel'],
+    listeners: {
+       activate: function() {
+           // notify owning container that it should display a help button
+           if (this.onlineHelp) {
+               Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp);
+           }
+       },
+       deactivate: function() {
+           if (this.onlineHelp) {
+               Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp);
+           }
+       },
+    },
+    border: false,
+
+    // override this with an URL to a relevant chapter of the pve manual
+    // setting this will display a help button in our parent panel
+    onlineHelp: undefined,
+
+    // will be set if the inputpanel has advanced items
+    hasAdvanced: false,
+
+    // if the panel has advanced items,
+    // this will determine if they are shown by default
+    showAdvanced: false,
+
+    // overwrite this to modify submit data
+    onGetValues: function(values) {
+       return values;
+    },
+
+    getValues: function(dirtyOnly) {
+       let me = this;
+
+       if (Ext.isFunction(me.onGetValues)) {
+           dirtyOnly = false;
+       }
+
+       let values = {};
+
+       Ext.Array.each(me.query('[isFormField]'), function(field) {
+           if (!dirtyOnly || field.isDirty()) {
+               Proxmox.Utils.assemble_field_data(values, field.getSubmitData());
+           }
+       });
+
+       return me.onGetValues(values);
+    },
+
+    setAdvancedVisible: function(visible) {
+       let me = this;
+       let advItems = me.getComponent('advancedContainer');
+       if (advItems) {
+           advItems.setVisible(visible);
+       }
+    },
+
+    setValues: function(values) {
+       let me = this;
+
+       let form = me.up('form');
+
+        Ext.iterate(values, function(fieldId, val) {
+           let fields = me.query('[isFormField][name=' + fieldId + ']');
+           for (const field of fields) {
+               if (field) {
+                   field.setValue(val);
+                   if (form.trackResetOnLoad) {
+                       field.resetOriginalValue();
+                   }
+               }
+           }
+       });
+    },
+
+    initComponent: function() {
+       let me = this;
+
+       let items;
+
+       if (me.items) {
+           me.columns = 1;
+           items = [
+               {
+                   columnWidth: 1,
+                   layout: 'anchor',
+                   items: me.items,
+               },
+           ];
+           me.items = undefined;
+       } else if (me.column4) {
+           me.columns = 4;
+           items = [
+               {
+                   columnWidth: 0.25,
+                   padding: '0 10 0 0',
+                   layout: 'anchor',
+                   items: me.column1,
+               },
+               {
+                   columnWidth: 0.25,
+                   padding: '0 10 0 0',
+                   layout: 'anchor',
+                   items: me.column2,
+               },
+               {
+                   columnWidth: 0.25,
+                   padding: '0 10 0 0',
+                   layout: 'anchor',
+                   items: me.column3,
+               },
+               {
+                   columnWidth: 0.25,
+                   padding: '0 0 0 10',
+                   layout: 'anchor',
+                   items: me.column4,
+               },
+           ];
+           if (me.columnB) {
+               items.push({
+                   columnWidth: 1,
+                   padding: '10 0 0 0',
+                   layout: 'anchor',
+                   items: me.columnB,
+               });
+           }
+       } else if (me.column1) {
+           me.columns = 2;
+           items = [
+               {
+                   columnWidth: 0.5,
+                   padding: '0 10 0 0',
+                   layout: 'anchor',
+                   items: me.column1,
+               },
+               {
+                   columnWidth: 0.5,
+                   padding: '0 0 0 10',
+                   layout: 'anchor',
+                   items: me.column2 || [], // allow empty column
+               },
+           ];
+           if (me.columnB) {
+               items.push({
+                   columnWidth: 1,
+                   padding: '10 0 0 0',
+                   layout: 'anchor',
+                   items: me.columnB,
+               });
+           }
+       } else {
+           throw "unsupported config";
+       }
+
+       let advItems;
+       if (me.advancedItems) {
+           advItems = [
+               {
+                   columnWidth: 1,
+                   layout: 'anchor',
+                   items: me.advancedItems,
+               },
+           ];
+           me.advancedItems = undefined;
+       } else if (me.advancedColumn1) {
+           advItems = [
+               {
+                   columnWidth: 0.5,
+                   padding: '0 10 0 0',
+                   layout: 'anchor',
+                   items: me.advancedColumn1,
+               },
+               {
+                   columnWidth: 0.5,
+                   padding: '0 0 0 10',
+                   layout: 'anchor',
+                   items: me.advancedColumn2 || [], // allow empty column
+               },
+           ];
+
+           me.advancedColumn1 = undefined;
+           me.advancedColumn2 = undefined;
+
+           if (me.advancedColumnB) {
+               advItems.push({
+                   columnWidth: 1,
+                   padding: '10 0 0 0',
+                   layout: 'anchor',
+                   items: me.advancedColumnB,
+               });
+               me.advancedColumnB = undefined;
+           }
+       }
+
+       if (advItems) {
+           me.hasAdvanced = true;
+           advItems.unshift({
+               columnWidth: 1,
+               xtype: 'box',
+               hidden: false,
+               border: true,
+               autoEl: {
+                   tag: 'hr',
+               },
+           });
+           items.push({
+               columnWidth: 1,
+               xtype: 'container',
+               itemId: 'advancedContainer',
+               hidden: !me.showAdvanced,
+               layout: 'column',
+               defaults: {
+                   border: false,
+               },
+               items: advItems,
+           });
+       }
+
+       if (me.useFieldContainer) {
+           Ext.apply(me, {
+               layout: 'fit',
+               items: Ext.apply(me.useFieldContainer, {
+                   layout: 'column',
+                   defaultType: 'container',
+                   items: items,
+               }),
+           });
+       } else {
+           Ext.apply(me, {
+               layout: 'column',
+               defaultType: 'container',
+               items: items,
+           });
+       }
+
+       me.callParent();
+    },
+});
diff --git a/src/panel/JournalView.js b/src/panel/JournalView.js
new file mode 100644 (file)
index 0000000..b81cc45
--- /dev/null
@@ -0,0 +1,347 @@
+/*
+ * Display log entries in a panel with scrollbar
+ * The log entries are automatically refreshed via a background task,
+ * with newest entries comming at the bottom
+ */
+Ext.define('Proxmox.panel.JournalView', {
+    extend: 'Ext.panel.Panel',
+    xtype: 'proxmoxJournalView',
+
+    numEntries: 500,
+    lineHeight: 16,
+
+    scrollToEnd: true,
+
+    controller: {
+       xclass: 'Ext.app.ViewController',
+
+       updateParams: function() {
+           let me = this;
+           let viewModel = me.getViewModel();
+           let since = viewModel.get('since');
+           let until = viewModel.get('until');
+
+           since.setHours(0, 0, 0, 0);
+           until.setHours(0, 0, 0, 0);
+           until.setDate(until.getDate()+1);
+
+           me.getView().loadTask.delay(200, undefined, undefined, [
+               false,
+               false,
+               Ext.Date.format(since, "U"),
+               Ext.Date.format(until, "U"),
+           ]);
+       },
+
+       scrollPosBottom: function() {
+           let view = this.getView();
+           let pos = view.getScrollY();
+           let maxPos = view.getScrollable().getMaxPosition().y;
+           return maxPos - pos;
+       },
+
+       scrollPosTop: function() {
+           let view = this.getView();
+           return view.getScrollY();
+       },
+
+       updateScroll: function(livemode, num, scrollPos, scrollPosTop) {
+           let me = this;
+           let view = me.getView();
+
+           if (!livemode) {
+               setTimeout(function() { view.scrollTo(0, 0); }, 10);
+           } else if (view.scrollToEnd && scrollPos <= 0) {
+               setTimeout(function() { view.scrollTo(0, Infinity); }, 10);
+           } else if (!view.scrollToEnd && scrollPosTop < 20 * view.lineHeight) {
+               setTimeout(function() { view.scrollTo(0, (num * view.lineHeight) + scrollPosTop); }, 10);
+           }
+       },
+
+       updateView: function(lines, livemode, top) {
+           let me = this;
+           let view = me.getView();
+           let viewmodel = me.getViewModel();
+           if (!viewmodel || viewmodel.get('livemode') !== livemode) {
+               return; // we switched mode, do not update the content
+           }
+           let contentEl = me.lookup('content');
+
+           // save old scrollpositions
+           let scrollPos = me.scrollPosBottom();
+           let scrollPosTop = me.scrollPosTop();
+
+           let newend = lines.shift();
+           let newstart = lines.pop();
+
+           let num = lines.length;
+           let text = lines.map(Ext.htmlEncode).join('<br>');
+
+           if (!livemode) {
+               if (num) {
+                   view.content = text;
+               } else {
+                   view.content = 'nothing logged or no timespan selected';
+               }
+           } else {
+               // update content
+               if (top && num) {
+                   view.content = view.content ? text + '<br>' + view.content : text;
+               } else if (!top && num) {
+                   view.content = view.content ? view.content + '<br>' + text : text;
+               }
+
+               // update cursors
+               if (!top || !view.startcursor) {
+                   view.startcursor = newstart;
+               }
+
+               if (top || !view.endcursor) {
+                   view.endcursor = newend;
+               }
+           }
+
+           contentEl.update(view.content);
+
+           me.updateScroll(livemode, num, scrollPos, scrollPosTop);
+       },
+
+       doLoad: function(livemode, top, since, until) {
+           let me = this;
+           if (me.running) {
+               me.requested = true;
+               return;
+           }
+           me.running = true;
+           let view = me.getView();
+           let params = {
+               lastentries: view.numEntries || 500,
+           };
+           if (livemode) {
+               if (!top && view.startcursor) {
+                   params = {
+                       startcursor: view.startcursor,
+                   };
+               } else if (view.endcursor) {
+                   params.endcursor = view.endcursor;
+               }
+           } else {
+               params = {
+                   since: since,
+                   until: until,
+               };
+           }
+           Proxmox.Utils.API2Request({
+               url: view.url,
+               params: params,
+               waitMsgTarget: !livemode ? view : undefined,
+               method: 'GET',
+               success: function(response) {
+                   Proxmox.Utils.setErrorMask(me, false);
+                   let lines = response.result.data;
+                   me.updateView(lines, livemode, top);
+                   me.running = false;
+                   if (me.requested) {
+                       me.requested = false;
+                       view.loadTask.delay(200);
+                   }
+               },
+               failure: function(response) {
+                   let msg = response.htmlStatus;
+                   Proxmox.Utils.setErrorMask(me, msg);
+                   me.running = false;
+                   if (me.requested) {
+                       me.requested = false;
+                       view.loadTask.delay(200);
+                   }
+               },
+           });
+       },
+
+       onScroll: function(x, y) {
+           let me = this;
+           let view = me.getView();
+           let viewmodel = me.getViewModel();
+           let livemode = viewmodel.get('livemode');
+           if (!livemode) {
+               return;
+           }
+
+           if (me.scrollPosTop() < 20*view.lineHeight) {
+               view.scrollToEnd = false;
+               view.loadTask.delay(200, undefined, undefined, [true, true]);
+           } else if (me.scrollPosBottom() <= 1) {
+               view.scrollToEnd = true;
+           }
+       },
+
+       init: function(view) {
+           let me = this;
+
+           if (!view.url) {
+               throw "no url specified";
+           }
+
+           let viewmodel = me.getViewModel();
+           let viewModel = this.getViewModel();
+           let since = new Date();
+           since.setDate(since.getDate() - 3);
+           viewModel.set('until', new Date());
+           viewModel.set('since', since);
+           me.lookup('content').setStyle('line-height', view.lineHeight + 'px');
+
+           view.loadTask = new Ext.util.DelayedTask(me.doLoad, me, [true, false]);
+
+           me.updateParams();
+           view.task = Ext.TaskManager.start({
+               run: function() {
+                   if (!view.isVisible() || !view.scrollToEnd || !viewmodel.get('livemode')) {
+                       return;
+                   }
+
+                   if (me.scrollPosBottom() <= 1) {
+                       view.loadTask.delay(200, undefined, undefined, [true, false]);
+                   }
+               },
+               interval: 1000,
+           });
+       },
+
+       onLiveMode: function() {
+           let me = this;
+           let view = me.getView();
+           delete view.startcursor;
+           delete view.endcursor;
+           delete view.content;
+           me.getViewModel().set('livemode', true);
+           view.scrollToEnd = true;
+           me.updateView([], true, false);
+       },
+
+       onTimespan: function() {
+           let me = this;
+           me.getViewModel().set('livemode', false);
+           me.updateView([], false);
+       },
+    },
+
+    onDestroy: function() {
+       let me = this;
+       me.loadTask.cancel();
+       Ext.TaskManager.stop(me.task);
+       delete me.content;
+    },
+
+    // for user to initiate a load from outside
+    requestUpdate: function() {
+       let me = this;
+       me.loadTask.delay(200);
+    },
+
+    viewModel: {
+       data: {
+           livemode: true,
+           until: null,
+           since: null,
+       },
+    },
+
+    layout: 'auto',
+    bodyPadding: 5,
+    scrollable: {
+       x: 'auto',
+       y: 'auto',
+       listeners: {
+           // we have to have this here, since we cannot listen to events
+           // of the scroller in the viewcontroller (extjs bug?), nor does
+           // the panel have a 'scroll' event'
+           scroll: {
+               fn: function(scroller, x, y) {
+                   let controller = this.component.getController();
+                   if (controller) { // on destroy, controller can be gone
+                       controller.onScroll(x, y);
+                   }
+               },
+               buffer: 200,
+           },
+       },
+    },
+
+    tbar: {
+
+       items: [
+           '->',
+           {
+               xtype: 'segmentedbutton',
+               items: [
+                   {
+                       text: gettext('Live Mode'),
+                       bind: {
+                           pressed: '{livemode}',
+                       },
+                       handler: 'onLiveMode',
+                   },
+                   {
+                       text: gettext('Select Timespan'),
+                       bind: {
+                           pressed: '{!livemode}',
+                       },
+                       handler: 'onTimespan',
+                   },
+               ],
+           },
+           {
+               xtype: 'box',
+               bind: { disabled: '{livemode}' },
+               autoEl: { cn: gettext('Since') + ':' },
+           },
+           {
+               xtype: 'datefield',
+               name: 'since_date',
+               reference: 'since',
+               format: 'Y-m-d',
+               bind: {
+                   disabled: '{livemode}',
+                   value: '{since}',
+                   maxValue: '{until}',
+               },
+           },
+           {
+               xtype: 'box',
+               bind: { disabled: '{livemode}' },
+               autoEl: { cn: gettext('Until') + ':' },
+           },
+           {
+               xtype: 'datefield',
+               name: 'until_date',
+               reference: 'until',
+               format: 'Y-m-d',
+               bind: {
+                   disabled: '{livemode}',
+                   value: '{until}',
+                   minValue: '{since}',
+               },
+           },
+           {
+               xtype: 'button',
+               text: 'Update',
+               reference: 'updateBtn',
+               handler: 'updateParams',
+               bind: {
+                   disabled: '{livemode}',
+               },
+           },
+       ],
+    },
+
+    items: [
+       {
+           xtype: 'box',
+           reference: 'content',
+           style: {
+               font: 'normal 11px tahoma, arial, verdana, sans-serif',
+               'white-space': 'pre',
+           },
+       },
+    ],
+});
diff --git a/src/panel/LogView.js b/src/panel/LogView.js
new file mode 100644 (file)
index 0000000..1ce83bc
--- /dev/null
@@ -0,0 +1,275 @@
+/*
+ * Display log entries in a panel with scrollbar
+ * The log entries are automatically refreshed via a background task,
+ * with newest entries comming at the bottom
+ */
+Ext.define('Proxmox.panel.LogView', {
+    extend: 'Ext.panel.Panel',
+    xtype: 'proxmoxLogView',
+
+    pageSize: 500,
+    viewBuffer: 50,
+    lineHeight: 16,
+
+    scrollToEnd: true,
+
+    // callback for load failure, used for ceph
+    failCallback: undefined,
+
+    controller: {
+       xclass: 'Ext.app.ViewController',
+
+       updateParams: function() {
+           let me = this;
+           let viewModel = me.getViewModel();
+           let since = viewModel.get('since');
+           let until = viewModel.get('until');
+           if (viewModel.get('hide_timespan')) {
+               return;
+           }
+
+           if (since > until) {
+               Ext.Msg.alert('Error', 'Since date must be less equal than Until date.');
+               return;
+           }
+
+           viewModel.set('params.since', Ext.Date.format(since, 'Y-m-d'));
+           viewModel.set('params.until', Ext.Date.format(until, 'Y-m-d') + ' 23:59:59');
+           me.getView().loadTask.delay(200);
+       },
+
+       scrollPosBottom: function() {
+           let view = this.getView();
+           let pos = view.getScrollY();
+           let maxPos = view.getScrollable().getMaxPosition().y;
+           return maxPos - pos;
+       },
+
+       updateView: function(text, first, total) {
+           let me = this;
+           let view = me.getView();
+           let viewModel = me.getViewModel();
+           let content = me.lookup('content');
+           let data = viewModel.get('data');
+
+           if (first === data.first && total === data.total && text.length === data.textlen) {
+               return; // same content, skip setting and scrolling
+           }
+           viewModel.set('data', {
+               first: first,
+               total: total,
+               textlen: text.length,
+           });
+
+           let scrollPos = me.scrollPosBottom();
+
+           content.update(text);
+
+           if (view.scrollToEnd && scrollPos <= 0) {
+               // we use setTimeout to work around scroll handling on touchscreens
+               setTimeout(function() { view.scrollTo(0, Infinity); }, 10);
+           }
+       },
+
+       doLoad: function() {
+           let me = this;
+           if (me.running) {
+               me.requested = true;
+               return;
+           }
+           me.running = true;
+           let view = me.getView();
+           let viewModel = me.getViewModel();
+           Proxmox.Utils.API2Request({
+               url: me.getView().url,
+               params: viewModel.get('params'),
+               method: 'GET',
+               success: function(response) {
+                   Proxmox.Utils.setErrorMask(me, false);
+                   let total = response.result.total;
+                   let lines = [];
+                   let first = Infinity;
+
+                   Ext.Array.each(response.result.data, function(line) {
+                       if (first > line.n) {
+                           first = line.n;
+                       }
+                       lines[line.n - 1] = Ext.htmlEncode(line.t);
+                   });
+
+                   lines.length = total;
+                   me.updateView(lines.join('<br>'), first - 1, total);
+                   me.running = false;
+                   if (me.requested) {
+                       me.requested = false;
+                       view.loadTask.delay(200);
+                   }
+               },
+               failure: function(response) {
+                   if (view.failCallback) {
+                       view.failCallback(response);
+                   } else {
+                       let msg = response.htmlStatus;
+                       Proxmox.Utils.setErrorMask(me, msg);
+                   }
+                   me.running = false;
+                   if (me.requested) {
+                       me.requested = false;
+                       view.loadTask.delay(200);
+                   }
+               },
+           });
+       },
+
+       onScroll: function(x, y) {
+           let me = this;
+           let view = me.getView();
+           let viewModel = me.getViewModel();
+
+           let lineHeight = view.lineHeight;
+           let line = view.getScrollY()/lineHeight;
+           let start = viewModel.get('params.start');
+           let limit = viewModel.get('params.limit');
+           let viewLines = view.getHeight()/lineHeight;
+
+           let viewStart = Math.max(parseInt(line - 1 - view.viewBuffer, 10), 0);
+           let viewEnd = parseInt(line + viewLines + 1 + view.viewBuffer, 10);
+
+           if (viewStart < start || viewEnd > start+limit) {
+               viewModel.set('params.start',
+                   Math.max(parseInt(line - (limit / 2) + 10, 10), 0));
+               view.loadTask.delay(200);
+           }
+       },
+
+       init: function(view) {
+           let me = this;
+
+           if (!view.url) {
+               throw "no url specified";
+           }
+
+           let viewModel = this.getViewModel();
+           let since = new Date();
+           since.setDate(since.getDate() - 3);
+           viewModel.set('until', new Date());
+           viewModel.set('since', since);
+           viewModel.set('params.limit', view.pageSize);
+           viewModel.set('hide_timespan', !view.log_select_timespan);
+           me.lookup('content').setStyle('line-height', view.lineHeight + 'px');
+
+           view.loadTask = new Ext.util.DelayedTask(me.doLoad, me);
+
+           me.updateParams();
+           view.task = Ext.TaskManager.start({
+               run: function() {
+                   if (!view.isVisible() || !view.scrollToEnd) {
+                       return;
+                   }
+
+                   if (me.scrollPosBottom() <= 1) {
+                       view.loadTask.delay(200);
+                   }
+               },
+               interval: 1000,
+           });
+       },
+    },
+
+    onDestroy: function() {
+       let me = this;
+       me.loadTask.cancel();
+       Ext.TaskManager.stop(me.task);
+    },
+
+    // for user to initiate a load from outside
+    requestUpdate: function() {
+       let me = this;
+       me.loadTask.delay(200);
+    },
+
+    viewModel: {
+       data: {
+           until: null,
+           since: null,
+           hide_timespan: false,
+           data: {
+               start: 0,
+               total: 0,
+               textlen: 0,
+           },
+           params: {
+               start: 0,
+               limit: 500,
+           },
+       },
+    },
+
+    layout: 'auto',
+    bodyPadding: 5,
+    scrollable: {
+       x: 'auto',
+       y: 'auto',
+       listeners: {
+           // we have to have this here, since we cannot listen to events
+           // of the scroller in the viewcontroller (extjs bug?), nor does
+           // the panel have a 'scroll' event'
+           scroll: {
+               fn: function(scroller, x, y) {
+                   let controller = this.component.getController();
+                   if (controller) { // on destroy, controller can be gone
+                       controller.onScroll(x, y);
+                   }
+               },
+               buffer: 200,
+           },
+       },
+    },
+
+    tbar: {
+       bind: {
+           hidden: '{hide_timespan}',
+       },
+       items: [
+           '->',
+           'Since: ',
+           {
+               xtype: 'datefield',
+               name: 'since_date',
+               reference: 'since',
+               format: 'Y-m-d',
+               bind: {
+                   value: '{since}',
+                   maxValue: '{until}',
+               },
+           },
+           'Until: ',
+           {
+               xtype: 'datefield',
+               name: 'until_date',
+               reference: 'until',
+               format: 'Y-m-d',
+               bind: {
+                   value: '{until}',
+                   minValue: '{since}',
+               },
+           },
+           {
+               xtype: 'button',
+               text: 'Update',
+               handler: 'updateParams',
+           },
+       ],
+    },
+
+    items: [
+       {
+           xtype: 'box',
+           reference: 'content',
+           style: {
+               font: 'normal 11px tahoma, arial, verdana, sans-serif',
+               'white-space': 'pre',
+           },
+       },
+    ],
+});
diff --git a/src/panel/RRDChart.js b/src/panel/RRDChart.js
new file mode 100644 (file)
index 0000000..b4f89a4
--- /dev/null
@@ -0,0 +1,186 @@
+Ext.define('Proxmox.widget.RRDChart', {
+    extend: 'Ext.chart.CartesianChart',
+    alias: 'widget.proxmoxRRDChart',
+
+    unit: undefined, // bytes, bytespersecond, percent
+
+    controller: {
+       xclass: 'Ext.app.ViewController',
+
+       convertToUnits: function(value) {
+           let units = ['', 'k', 'M', 'G', 'T', 'P'];
+           let si = 0;
+           let format = '0.##';
+           if (value < 0.1) format += '#';
+           while (value >= 1000 && si < units.length -1) {
+               value = value / 1000;
+               si++;
+           }
+
+           // javascript floating point weirdness
+           value = Ext.Number.correctFloat(value);
+
+           // limit decimal points
+           value = Ext.util.Format.number(value, format);
+
+           return value.toString() + " " + units[si];
+       },
+
+       leftAxisRenderer: function(axis, label, layoutContext) {
+           let me = this;
+           return me.convertToUnits(label);
+       },
+
+       onSeriesTooltipRender: function(tooltip, record, item) {
+           let view = this.getView();
+
+           let suffix = '';
+           if (view.unit === 'percent') {
+               suffix = '%';
+           } else if (view.unit === 'bytes') {
+               suffix = 'B';
+           } else if (view.unit === 'bytespersecond') {
+               suffix = 'B/s';
+           }
+
+           let prefix = item.field;
+           if (view.fieldTitles && view.fieldTitles[view.fields.indexOf(item.field)]) {
+               prefix = view.fieldTitles[view.fields.indexOf(item.field)];
+           }
+           let v = this.convertToUnits(record.get(item.field));
+           let t = new Date(record.get('time'));
+           tooltip.setHtml(`${prefix}: ${v}${suffix}<br>${t}`);
+       },
+
+       onAfterAnimation: function(chart, eopts) {
+           // if the undo button is disabled, disable our tool
+           let ourUndoZoomButton = chart.header.tools[0];
+           let undoButton = chart.interactions[0].getUndoButton();
+           ourUndoZoomButton.setDisabled(undoButton.isDisabled());
+       },
+    },
+
+    width: 770,
+    height: 300,
+    animation: false,
+    interactions: [
+       {
+           type: 'crosszoom',
+       },
+    ],
+    legend: {
+       padding: 0,
+    },
+    axes: [
+       {
+           type: 'numeric',
+           position: 'left',
+           grid: true,
+           renderer: 'leftAxisRenderer',
+           minimum: 0,
+       },
+       {
+           type: 'time',
+           position: 'bottom',
+           grid: true,
+           fields: ['time'],
+       },
+    ],
+    listeners: {
+       animationend: 'onAfterAnimation',
+    },
+
+    initComponent: function() {
+       let me = this;
+
+       if (!me.store) {
+           throw "cannot work without store";
+       }
+
+       if (!me.fields) {
+           throw "cannot work without fields";
+       }
+
+       me.callParent();
+
+       // add correct label for left axis
+       let axisTitle = "";
+       if (me.unit === 'percent') {
+           axisTitle = "%";
+       } else if (me.unit === 'bytes') {
+           axisTitle = "Bytes";
+       } else if (me.unit === 'bytespersecond') {
+           axisTitle = "Bytes/s";
+       } else if (me.fieldTitles && me.fieldTitles.length === 1) {
+           axisTitle = me.fieldTitles[0];
+       } else if (me.fields.length === 1) {
+           axisTitle = me.fields[0];
+       }
+
+       me.axes[0].setTitle(axisTitle);
+
+       me.updateHeader();
+
+       if (me.header && me.legend) {
+           me.header.padding = '4 9 4';
+           me.header.add(me.legend);
+       }
+
+       if (!me.noTool) {
+           me.addTool({
+               type: 'minus',
+               disabled: true,
+               tooltip: gettext('Undo Zoom'),
+               handler: function() {
+                   let undoButton = me.interactions[0].getUndoButton();
+                   if (undoButton.handler) {
+                       undoButton.handler();
+                   }
+               },
+           });
+       }
+
+       // add a series for each field we get
+       me.fields.forEach(function(item, index) {
+           let title = item;
+           if (me.fieldTitles && me.fieldTitles[index]) {
+               title = me.fieldTitles[index];
+           }
+           me.addSeries(Ext.apply(
+               {
+                   type: 'line',
+                   xField: 'time',
+                   yField: item,
+                   title: title,
+                   fill: true,
+                   style: {
+                       lineWidth: 1.5,
+                       opacity: 0.60,
+                   },
+                   marker: {
+                       opacity: 0,
+                       scaling: 0.01,
+                       fx: {
+                           duration: 200,
+                           easing: 'easeOut',
+                       },
+                   },
+                   highlightCfg: {
+                       opacity: 1,
+                       scaling: 1.5,
+                   },
+                   tooltip: {
+                       trackMouse: true,
+                       renderer: 'onSeriesTooltipRender',
+                   },
+               },
+               me.seriesConfig,
+           ));
+       });
+
+       // enable animation after the store is loaded
+       me.store.onAfter('load', function() {
+           me.setAnimation(true);
+       }, this, { single: true });
+    },
+});
diff --git a/src/window/Edit.js b/src/window/Edit.js
new file mode 100644 (file)
index 0000000..c165141
--- /dev/null
@@ -0,0 +1,389 @@
+// fixme: how can we avoid those lint errors?
+Ext.define('Proxmox.window.Edit', {
+    extend: 'Ext.window.Window',
+    alias: 'widget.proxmoxWindowEdit',
+
+    // autoLoad trigger a load() after component creation
+    autoLoad: false,
+
+    resizable: false,
+
+    // use this tio atimatically generate a title like
+    // Create: <subject>
+    subject: undefined,
+
+    // set isCreate to true if you want a Create button (instead
+    // OK and RESET)
+    isCreate: false,
+
+    // set to true if you want an Add button (instead of Create)
+    isAdd: false,
+
+    // set to true if you want an Remove button (instead of Create)
+    isRemove: false,
+
+    // custom submitText
+    submitText: undefined,
+
+    backgroundDelay: 0,
+
+    // needed for finding the reference to submitbutton
+    // because we do not have a controller
+    referenceHolder: true,
+    defaultButton: 'submitbutton',
+
+    // finds the first form field
+    defaultFocus: 'field[disabled=false][hidden=false]',
+
+    showProgress: false,
+
+    showTaskViewer: false,
+
+    // gets called if we have a progress bar or taskview and it detected that
+    // the task finished. function(success)
+    taskDone: Ext.emptyFn,
+
+    // gets called when the api call is finished, right at the beginning
+    // function(success, response, options)
+    apiCallDone: Ext.emptyFn,
+
+    // assign a reference from docs, to add a help button docked to the
+    // bottom of the window. If undefined we magically fall back to the
+    // onlineHelp of our first item, if set.
+    onlineHelp: undefined,
+
+    isValid: function() {
+       let me = this;
+
+       let form = me.formPanel.getForm();
+       return form.isValid();
+    },
+
+    getValues: function(dirtyOnly) {
+       let me = this;
+
+        let values = {};
+
+       let form = me.formPanel.getForm();
+
+        form.getFields().each(function(field) {
+            if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) {
+                Proxmox.Utils.assemble_field_data(values, field.getSubmitData());
+            }
+        });
+
+       Ext.Array.each(me.query('inputpanel'), function(panel) {
+           Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly));
+       });
+
+        return values;
+    },
+
+    setValues: function(values) {
+       let me = this;
+
+       let form = me.formPanel.getForm();
+       let formfields = form.getFields();
+
+       Ext.iterate(values, function(id, val) {
+           let fields = formfields.filterBy((f) =>
+               (f.id === id || f.name === id || f.dataIndex === id) && !f.up('inputpanel'),
+           );
+           fields.each((field) => {
+               field.setValue(val);
+               if (form.trackResetOnLoad) {
+                   field.resetOriginalValue();
+               }
+           });
+       });
+
+       Ext.Array.each(me.query('inputpanel'), function(panel) {
+           panel.setValues(values);
+       });
+    },
+
+    setSubmitText: function(text) {
+       this.lookup('submitbutton').setText(text);
+    },
+
+    submit: function() {
+       let me = this;
+
+       let form = me.formPanel.getForm();
+
+       let values = me.getValues();
+       Ext.Object.each(values, function(name, val) {
+           if (Object.prototype.hasOwnProperty.call(values, name)) {
+               if (Ext.isArray(val) && !val.length) {
+                   values[name] = '';
+               }
+           }
+       });
+
+       if (me.digest) {
+           values.digest = me.digest;
+       }
+
+       if (me.backgroundDelay) {
+           values.background_delay = me.backgroundDelay;
+       }
+
+       let url = me.url;
+       if (me.method === 'DELETE') {
+           url = url + "?" + Ext.Object.toQueryString(values);
+           values = undefined;
+       }
+
+       Proxmox.Utils.API2Request({
+           url: url,
+           waitMsgTarget: me,
+           method: me.method || (me.backgroundDelay ? 'POST' : 'PUT'),
+           params: values,
+           failure: function(response, options) {
+               me.apiCallDone(false, response, options);
+
+               if (response.result && response.result.errors) {
+                   form.markInvalid(response.result.errors);
+               }
+               Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+           },
+           success: function(response, options) {
+               let hasProgressBar =
+                   (me.backgroundDelay || me.showProgress || me.showTaskViewer) &&
+                   response.result.data;
+
+               me.apiCallDone(true, response, options);
+
+               if (hasProgressBar) {
+                   // stay around so we can trigger our close events
+                   // when background action is completed
+                   me.hide();
+
+                   let upid = response.result.data;
+                   let viewerClass = me.showTaskViewer ? 'Viewer' : 'Progress';
+                   Ext.create('Proxmox.window.Task' + viewerClass, {
+                       autoShow: true,
+                       upid: upid,
+                       taskDone: me.taskDone,
+                       listeners: {
+                           destroy: function() {
+                               me.close();
+                           },
+                       },
+                   });
+               } else {
+                   me.close();
+               }
+           },
+       });
+    },
+
+    load: function(options) {
+       let me = this;
+
+       let form = me.formPanel.getForm();
+
+       options = options || {};
+
+       let newopts = Ext.apply({
+           waitMsgTarget: me,
+       }, options);
+
+       let createWrapper = function(successFn) {
+           Ext.apply(newopts, {
+               url: me.url,
+               method: 'GET',
+               success: function(response, opts) {
+                   form.clearInvalid();
+                   me.digest = response.result.digest || response.result.data.digest;
+                   if (successFn) {
+                       successFn(response, opts);
+                   } else {
+                       me.setValues(response.result.data);
+                   }
+                   // hack: fix ExtJS bug
+                   Ext.Array.each(me.query('radiofield'), function(f) {
+                       f.resetOriginalValue();
+                   });
+               },
+               failure: function(response, opts) {
+                   Ext.Msg.alert(gettext('Error'), response.htmlStatus, function() {
+                       me.close();
+                   });
+               },
+           });
+       };
+
+       createWrapper(options.success);
+
+       Proxmox.Utils.API2Request(newopts);
+    },
+
+    initComponent: function() {
+       let me = this;
+
+       if (!me.url) {
+           throw "no url specified";
+       }
+
+       if (me.create) {throw "deprecated parameter, use isCreate";}
+
+       let items = Ext.isArray(me.items) ? me.items : [me.items];
+
+       me.items = undefined;
+
+       me.formPanel = Ext.create('Ext.form.Panel', {
+           url: me.url,
+           method: me.method || 'PUT',
+           trackResetOnLoad: true,
+           bodyPadding: me.bodyPadding !== undefined ? me.bodyPadding : 10,
+           border: false,
+           defaults: Ext.apply({}, me.defaults, {
+               border: false,
+           }),
+           fieldDefaults: Ext.apply({}, me.fieldDefaults, {
+               labelWidth: 100,
+               anchor: '100%',
+            }),
+           items: items,
+       });
+
+       let inputPanel = me.formPanel.down('inputpanel');
+
+       let form = me.formPanel.getForm();
+
+       let submitText;
+       if (me.isCreate) {
+           if (me.submitText) {
+               submitText = me.submitText;
+           } else if (me.isAdd) {
+               submitText = gettext('Add');
+           } else if (me.isRemove) {
+               submitText = gettext('Remove');
+           } else {
+               submitText = gettext('Create');
+           }
+       } else {
+           submitText = me.submitText || gettext('OK');
+       }
+
+       let submitBtn = Ext.create('Ext.Button', {
+           reference: 'submitbutton',
+           text: submitText,
+           disabled: !me.isCreate,
+           handler: function() {
+               me.submit();
+           },
+       });
+
+       let resetBtn = Ext.create('Ext.Button', {
+           text: 'Reset',
+           disabled: true,
+           handler: function() {
+               form.reset();
+           },
+       });
+
+       let set_button_status = function() {
+           let valid = form.isValid();
+           let dirty = form.isDirty();
+           submitBtn.setDisabled(!valid || !(dirty || me.isCreate));
+           resetBtn.setDisabled(!dirty);
+
+           if (inputPanel && inputPanel.hasAdvanced) {
+               // we want to show the advanced options
+               // as soon as some of it is not valid
+               let advancedItems = me.down('#advancedContainer').query('field');
+               let allAdvancedValid = true;
+               advancedItems.forEach(function(field) {
+                   if (!field.isValid()) {
+                       allAdvancedValid = false;
+                   }
+               });
+
+               if (!allAdvancedValid) {
+                   inputPanel.setAdvancedVisible(true);
+                   me.down('#advancedcb').setValue(true);
+               }
+           }
+       };
+
+       form.on('dirtychange', set_button_status);
+       form.on('validitychange', set_button_status);
+
+       let colwidth = 300;
+       if (me.fieldDefaults && me.fieldDefaults.labelWidth) {
+           colwidth += me.fieldDefaults.labelWidth - 100;
+       }
+
+       let twoColumn = inputPanel && (inputPanel.column1 || inputPanel.column2);
+
+       if (me.subject && !me.title) {
+           me.title = Proxmox.Utils.dialog_title(me.subject, me.isCreate, me.isAdd);
+       }
+
+       if (me.isCreate) {
+               me.buttons = [submitBtn];
+       } else {
+               me.buttons = [submitBtn, resetBtn];
+       }
+
+       if (inputPanel && inputPanel.hasAdvanced) {
+           let sp = Ext.state.Manager.getProvider();
+           let advchecked = sp.get('proxmox-advanced-cb');
+           inputPanel.setAdvancedVisible(advchecked);
+           me.buttons.unshift(
+              {
+                  xtype: 'proxmoxcheckbox',
+                  itemId: 'advancedcb',
+                  boxLabelAlign: 'before',
+                  boxLabel: gettext('Advanced'),
+                  stateId: 'proxmox-advanced-cb',
+                  value: advchecked,
+                  listeners: {
+                      change: function(cb, val) {
+                          inputPanel.setAdvancedVisible(val);
+                          sp.set('proxmox-advanced-cb', val);
+                      },
+                  },
+              },
+           );
+       }
+
+       let onlineHelp = me.onlineHelp;
+       if (!onlineHelp && inputPanel && inputPanel.onlineHelp) {
+           onlineHelp = inputPanel.onlineHelp;
+       }
+
+       if (onlineHelp) {
+           let helpButton = Ext.create('Proxmox.button.Help');
+           me.buttons.unshift(helpButton, '->');
+           Ext.GlobalEvents.fireEvent('proxmoxShowHelp', onlineHelp);
+       }
+
+       Ext.applyIf(me, {
+           modal: true,
+           width: twoColumn ? colwidth*2 : colwidth,
+           border: false,
+           items: [me.formPanel],
+       });
+
+       me.callParent();
+
+       // always mark invalid fields
+       me.on('afterlayout', function() {
+           // on touch devices, the isValid function
+           // triggers a layout, which triggers an isValid
+           // and so on
+           // to prevent this we disable the layouting here
+           // and enable it afterwards
+           me.suspendLayout = true;
+           me.isValid();
+           me.suspendLayout = false;
+       });
+
+       if (me.autoLoad) {
+           me.load();
+       }
+    },
+});
diff --git a/src/window/LanguageEdit.js b/src/window/LanguageEdit.js
new file mode 100644 (file)
index 0000000..9176cfd
--- /dev/null
@@ -0,0 +1,49 @@
+Ext.define('Proxmox.window.LanguageEditWindow', {
+    extend: 'Ext.window.Window',
+    alias: 'widget.pmxLanguageEditWindow',
+
+    viewModel: {
+       parent: null,
+       data: {
+           language: '__default__',
+       },
+    },
+    controller: {
+       xclass: 'Ext.app.ViewController',
+       init: function(view) {
+           let language = Ext.util.Cookies.get(view.cookieName) || '__default__';
+           this.getViewModel().set('language', language);
+       },
+       applyLanguage: function(button) {
+           let view = this.getView();
+           let vm = this.getViewModel();
+
+           let expire = Ext.Date.add(new Date(), Ext.Date.YEAR, 10);
+           Ext.util.Cookies.set(view.cookieName, vm.get('language'), expire);
+           view.mask(gettext('Please wait...'), 'x-mask-loading');
+           window.location.reload();
+       },
+    },
+
+    cookieName: 'PVELangCookie',
+
+    title: gettext('Language'),
+    modal: true,
+    bodyPadding: 10,
+    resizable: false,
+    items: [
+       {
+           xtype: 'proxmoxLanguageSelector',
+           fieldLabel: gettext('Language'),
+           bind: {
+               value: '{language}',
+           },
+       },
+    ],
+    buttons: [
+       {
+           text: gettext('Apply'),
+           handler: 'applyLanguage',
+       },
+    ],
+});
diff --git a/src/window/PasswordEdit.js b/src/window/PasswordEdit.js
new file mode 100644 (file)
index 0000000..5ab1517
--- /dev/null
@@ -0,0 +1,45 @@
+Ext.define('Proxmox.window.PasswordEdit', {
+    extend: 'Proxmox.window.Edit',
+    alias: 'proxmoxWindowPasswordEdit',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    subject: gettext('Password'),
+
+    url: '/api2/extjs/access/password',
+
+    fieldDefaults: {
+       labelWidth: 120,
+    },
+
+    items: [
+       {
+           xtype: 'textfield',
+           inputType: 'password',
+           fieldLabel: gettext('Password'),
+           minLength: 5,
+           allowBlank: false,
+           name: 'password',
+           listeners: {
+               change: (field) => field.next().validate(),
+               blur: (field) => field.next().validate(),
+           },
+       },
+       {
+           xtype: 'textfield',
+           inputType: 'password',
+           fieldLabel: gettext('Confirm password'),
+           name: 'verifypassword',
+           allowBlank: false,
+           vtype: 'password',
+           initialPassField: 'password',
+           submitValue: false,
+       },
+       {
+           xtype: 'hiddenfield',
+           name: 'userid',
+           cbind: {
+               value: '{userid}',
+           },
+       },
+    ],
+});
diff --git a/src/window/TaskViewer.js b/src/window/TaskViewer.js
new file mode 100644 (file)
index 0000000..2f31023
--- /dev/null
@@ -0,0 +1,236 @@
+Ext.define('Proxmox.window.TaskProgress', {
+    extend: 'Ext.window.Window',
+    alias: 'widget.proxmoxTaskProgress',
+
+    taskDone: Ext.emptyFn,
+
+    initComponent: function() {
+        let me = this;
+
+       if (!me.upid) {
+           throw "no task specified";
+       }
+
+       let task = Proxmox.Utils.parse_task_upid(me.upid);
+
+       let statstore = Ext.create('Proxmox.data.ObjectStore', {
+            url: "/api2/json/nodes/" + task.node + "/tasks/" + me.upid + "/status",
+           interval: 1000,
+           rows: {
+               status: { defaultValue: 'unknown' },
+               exitstatus: { defaultValue: 'unknown' },
+           },
+       });
+
+       me.on('destroy', statstore.stopUpdate);
+
+       let getObjectValue = function(key, defaultValue) {
+           let rec = statstore.getById(key);
+           if (rec) {
+               return rec.data.value;
+           }
+           return defaultValue;
+       };
+
+       let pbar = Ext.create('Ext.ProgressBar', { text: 'running...' });
+
+       me.mon(statstore, 'load', function() {
+           let status = getObjectValue('status');
+           if (status === 'stopped') {
+               let exitstatus = getObjectValue('exitstatus');
+               if (exitstatus === 'OK') {
+                   pbar.reset();
+                   pbar.updateText("Done!");
+                   Ext.Function.defer(me.close, 1000, me);
+               } else {
+                   me.close();
+                   Ext.Msg.alert('Task failed', exitstatus);
+               }
+               me.taskDone(exitstatus === 'OK');
+           }
+       });
+
+       let descr = Proxmox.Utils.format_task_description(task.type, task.id);
+
+       Ext.apply(me, {
+           title: gettext('Task') + ': ' + descr,
+           width: 300,
+           layout: 'auto',
+           modal: true,
+           bodyPadding: 5,
+           items: pbar,
+           buttons: [
+               {
+                   text: gettext('Details'),
+                   handler: function() {
+                       let win = Ext.create('Proxmox.window.TaskViewer', {
+                           taskDone: me.taskDone,
+                           upid: me.upid,
+                       });
+                       win.show();
+                       me.close();
+                   },
+               },
+           ],
+       });
+
+       me.callParent();
+
+       statstore.startUpdate();
+
+       pbar.wait();
+    },
+});
+
+// fixme: how can we avoid those lint errors?
+
+Ext.define('Proxmox.window.TaskViewer', {
+    extend: 'Ext.window.Window',
+    alias: 'widget.proxmoxTaskViewer',
+
+    extraTitle: '', // string to prepend after the generic task title
+
+    taskDone: Ext.emptyFn,
+
+    initComponent: function() {
+        let me = this;
+
+       if (!me.upid) {
+           throw "no task specified";
+       }
+
+       let task = Proxmox.Utils.parse_task_upid(me.upid);
+
+       let statgrid;
+
+       let rows = {
+           status: {
+               header: gettext('Status'),
+               defaultValue: 'unknown',
+               renderer: function(value) {
+                   if (value !== 'stopped') {
+                       return value;
+                   }
+                   let es = statgrid.getObjectValue('exitstatus');
+                   if (es) {
+                       return value + ': ' + es;
+                   }
+                   return 'unknown';
+               },
+           },
+           exitstatus: {
+               visible: false,
+           },
+           type: {
+               header: gettext('Task type'),
+               required: true,
+           },
+           user: {
+               header: gettext('User name'),
+               renderer: Ext.String.htmlEncode,
+               required: true,
+           },
+           node: {
+               header: gettext('Node'),
+               required: true,
+           },
+           pid: {
+               header: gettext('Process ID'),
+               required: true,
+           },
+           task_id: {
+               header: gettext('Task ID'),
+           },
+           starttime: {
+               header: gettext('Start Time'),
+               required: true,
+               renderer: Proxmox.Utils.render_timestamp,
+           },
+           upid: {
+               header: gettext('Unique task ID'),
+               renderer: Ext.String.htmlEncode,
+           },
+       };
+
+       let statstore = Ext.create('Proxmox.data.ObjectStore', {
+            url: "/api2/json/nodes/" + task.node + "/tasks/" + me.upid + "/status",
+           interval: 1000,
+           rows: rows,
+       });
+
+       me.on('destroy', statstore.stopUpdate);
+
+       let stop_task = function() {
+           Proxmox.Utils.API2Request({
+               url: "/nodes/" + task.node + "/tasks/" + me.upid,
+               waitMsgTarget: me,
+               method: 'DELETE',
+               failure: function(response, opts) {
+                   Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+               },
+           });
+       };
+
+       let stop_btn1 = new Ext.Button({
+           text: gettext('Stop'),
+           disabled: true,
+           handler: stop_task,
+       });
+
+       let stop_btn2 = new Ext.Button({
+           text: gettext('Stop'),
+           disabled: true,
+           handler: stop_task,
+       });
+
+       statgrid = Ext.create('Proxmox.grid.ObjectGrid', {
+           title: gettext('Status'),
+           layout: 'fit',
+           tbar: [stop_btn1],
+           rstore: statstore,
+           rows: rows,
+           border: false,
+       });
+
+       let logView = Ext.create('Proxmox.panel.LogView', {
+           title: gettext('Output'),
+           tbar: [stop_btn2],
+           border: false,
+           url: "/api2/extjs/nodes/" + task.node + "/tasks/" + me.upid + "/log",
+       });
+
+       me.mon(statstore, 'load', function() {
+           let status = statgrid.getObjectValue('status');
+
+           if (status === 'stopped') {
+               logView.scrollToEnd = false;
+               logView.requestUpdate();
+               statstore.stopUpdate();
+               me.taskDone(statgrid.getObjectValue('exitstatus') === 'OK');
+           }
+
+           stop_btn1.setDisabled(status !== 'running');
+           stop_btn2.setDisabled(status !== 'running');
+       });
+
+       statstore.startUpdate();
+
+       Ext.apply(me, {
+           title: "Task viewer: " + task.desc + me.extraTitle,
+           width: 800,
+           height: 400,
+           layout: 'fit',
+           modal: true,
+           items: [{
+               xtype: 'tabpanel',
+               region: 'center',
+               items: [logView, statgrid],
+           }],
+        });
+
+       me.callParent();
+
+       logView.fireEvent('show', logView);
+    },
+});
+
diff --git a/window/Edit.js b/window/Edit.js
deleted file mode 100644 (file)
index c165141..0000000
+++ /dev/null
@@ -1,389 +0,0 @@
-// fixme: how can we avoid those lint errors?
-Ext.define('Proxmox.window.Edit', {
-    extend: 'Ext.window.Window',
-    alias: 'widget.proxmoxWindowEdit',
-
-    // autoLoad trigger a load() after component creation
-    autoLoad: false,
-
-    resizable: false,
-
-    // use this tio atimatically generate a title like
-    // Create: <subject>
-    subject: undefined,
-
-    // set isCreate to true if you want a Create button (instead
-    // OK and RESET)
-    isCreate: false,
-
-    // set to true if you want an Add button (instead of Create)
-    isAdd: false,
-
-    // set to true if you want an Remove button (instead of Create)
-    isRemove: false,
-
-    // custom submitText
-    submitText: undefined,
-
-    backgroundDelay: 0,
-
-    // needed for finding the reference to submitbutton
-    // because we do not have a controller
-    referenceHolder: true,
-    defaultButton: 'submitbutton',
-
-    // finds the first form field
-    defaultFocus: 'field[disabled=false][hidden=false]',
-
-    showProgress: false,
-
-    showTaskViewer: false,
-
-    // gets called if we have a progress bar or taskview and it detected that
-    // the task finished. function(success)
-    taskDone: Ext.emptyFn,
-
-    // gets called when the api call is finished, right at the beginning
-    // function(success, response, options)
-    apiCallDone: Ext.emptyFn,
-
-    // assign a reference from docs, to add a help button docked to the
-    // bottom of the window. If undefined we magically fall back to the
-    // onlineHelp of our first item, if set.
-    onlineHelp: undefined,
-
-    isValid: function() {
-       let me = this;
-
-       let form = me.formPanel.getForm();
-       return form.isValid();
-    },
-
-    getValues: function(dirtyOnly) {
-       let me = this;
-
-        let values = {};
-
-       let form = me.formPanel.getForm();
-
-        form.getFields().each(function(field) {
-            if (!field.up('inputpanel') && (!dirtyOnly || field.isDirty())) {
-                Proxmox.Utils.assemble_field_data(values, field.getSubmitData());
-            }
-        });
-
-       Ext.Array.each(me.query('inputpanel'), function(panel) {
-           Proxmox.Utils.assemble_field_data(values, panel.getValues(dirtyOnly));
-       });
-
-        return values;
-    },
-
-    setValues: function(values) {
-       let me = this;
-
-       let form = me.formPanel.getForm();
-       let formfields = form.getFields();
-
-       Ext.iterate(values, function(id, val) {
-           let fields = formfields.filterBy((f) =>
-               (f.id === id || f.name === id || f.dataIndex === id) && !f.up('inputpanel'),
-           );
-           fields.each((field) => {
-               field.setValue(val);
-               if (form.trackResetOnLoad) {
-                   field.resetOriginalValue();
-               }
-           });
-       });
-
-       Ext.Array.each(me.query('inputpanel'), function(panel) {
-           panel.setValues(values);
-       });
-    },
-
-    setSubmitText: function(text) {
-       this.lookup('submitbutton').setText(text);
-    },
-
-    submit: function() {
-       let me = this;
-
-       let form = me.formPanel.getForm();
-
-       let values = me.getValues();
-       Ext.Object.each(values, function(name, val) {
-           if (Object.prototype.hasOwnProperty.call(values, name)) {
-               if (Ext.isArray(val) && !val.length) {
-                   values[name] = '';
-               }
-           }
-       });
-
-       if (me.digest) {
-           values.digest = me.digest;
-       }
-
-       if (me.backgroundDelay) {
-           values.background_delay = me.backgroundDelay;
-       }
-
-       let url = me.url;
-       if (me.method === 'DELETE') {
-           url = url + "?" + Ext.Object.toQueryString(values);
-           values = undefined;
-       }
-
-       Proxmox.Utils.API2Request({
-           url: url,
-           waitMsgTarget: me,
-           method: me.method || (me.backgroundDelay ? 'POST' : 'PUT'),
-           params: values,
-           failure: function(response, options) {
-               me.apiCallDone(false, response, options);
-
-               if (response.result && response.result.errors) {
-                   form.markInvalid(response.result.errors);
-               }
-               Ext.Msg.alert(gettext('Error'), response.htmlStatus);
-           },
-           success: function(response, options) {
-               let hasProgressBar =
-                   (me.backgroundDelay || me.showProgress || me.showTaskViewer) &&
-                   response.result.data;
-
-               me.apiCallDone(true, response, options);
-
-               if (hasProgressBar) {
-                   // stay around so we can trigger our close events
-                   // when background action is completed
-                   me.hide();
-
-                   let upid = response.result.data;
-                   let viewerClass = me.showTaskViewer ? 'Viewer' : 'Progress';
-                   Ext.create('Proxmox.window.Task' + viewerClass, {
-                       autoShow: true,
-                       upid: upid,
-                       taskDone: me.taskDone,
-                       listeners: {
-                           destroy: function() {
-                               me.close();
-                           },
-                       },
-                   });
-               } else {
-                   me.close();
-               }
-           },
-       });
-    },
-
-    load: function(options) {
-       let me = this;
-
-       let form = me.formPanel.getForm();
-
-       options = options || {};
-
-       let newopts = Ext.apply({
-           waitMsgTarget: me,
-       }, options);
-
-       let createWrapper = function(successFn) {
-           Ext.apply(newopts, {
-               url: me.url,
-               method: 'GET',
-               success: function(response, opts) {
-                   form.clearInvalid();
-                   me.digest = response.result.digest || response.result.data.digest;
-                   if (successFn) {
-                       successFn(response, opts);
-                   } else {
-                       me.setValues(response.result.data);
-                   }
-                   // hack: fix ExtJS bug
-                   Ext.Array.each(me.query('radiofield'), function(f) {
-                       f.resetOriginalValue();
-                   });
-               },
-               failure: function(response, opts) {
-                   Ext.Msg.alert(gettext('Error'), response.htmlStatus, function() {
-                       me.close();
-                   });
-               },
-           });
-       };
-
-       createWrapper(options.success);
-
-       Proxmox.Utils.API2Request(newopts);
-    },
-
-    initComponent: function() {
-       let me = this;
-
-       if (!me.url) {
-           throw "no url specified";
-       }
-
-       if (me.create) {throw "deprecated parameter, use isCreate";}
-
-       let items = Ext.isArray(me.items) ? me.items : [me.items];
-
-       me.items = undefined;
-
-       me.formPanel = Ext.create('Ext.form.Panel', {
-           url: me.url,
-           method: me.method || 'PUT',
-           trackResetOnLoad: true,
-           bodyPadding: me.bodyPadding !== undefined ? me.bodyPadding : 10,
-           border: false,
-           defaults: Ext.apply({}, me.defaults, {
-               border: false,
-           }),
-           fieldDefaults: Ext.apply({}, me.fieldDefaults, {
-               labelWidth: 100,
-               anchor: '100%',
-            }),
-           items: items,
-       });
-
-       let inputPanel = me.formPanel.down('inputpanel');
-
-       let form = me.formPanel.getForm();
-
-       let submitText;
-       if (me.isCreate) {
-           if (me.submitText) {
-               submitText = me.submitText;
-           } else if (me.isAdd) {
-               submitText = gettext('Add');
-           } else if (me.isRemove) {
-               submitText = gettext('Remove');
-           } else {
-               submitText = gettext('Create');
-           }
-       } else {
-           submitText = me.submitText || gettext('OK');
-       }
-
-       let submitBtn = Ext.create('Ext.Button', {
-           reference: 'submitbutton',
-           text: submitText,
-           disabled: !me.isCreate,
-           handler: function() {
-               me.submit();
-           },
-       });
-
-       let resetBtn = Ext.create('Ext.Button', {
-           text: 'Reset',
-           disabled: true,
-           handler: function() {
-               form.reset();
-           },
-       });
-
-       let set_button_status = function() {
-           let valid = form.isValid();
-           let dirty = form.isDirty();
-           submitBtn.setDisabled(!valid || !(dirty || me.isCreate));
-           resetBtn.setDisabled(!dirty);
-
-           if (inputPanel && inputPanel.hasAdvanced) {
-               // we want to show the advanced options
-               // as soon as some of it is not valid
-               let advancedItems = me.down('#advancedContainer').query('field');
-               let allAdvancedValid = true;
-               advancedItems.forEach(function(field) {
-                   if (!field.isValid()) {
-                       allAdvancedValid = false;
-                   }
-               });
-
-               if (!allAdvancedValid) {
-                   inputPanel.setAdvancedVisible(true);
-                   me.down('#advancedcb').setValue(true);
-               }
-           }
-       };
-
-       form.on('dirtychange', set_button_status);
-       form.on('validitychange', set_button_status);
-
-       let colwidth = 300;
-       if (me.fieldDefaults && me.fieldDefaults.labelWidth) {
-           colwidth += me.fieldDefaults.labelWidth - 100;
-       }
-
-       let twoColumn = inputPanel && (inputPanel.column1 || inputPanel.column2);
-
-       if (me.subject && !me.title) {
-           me.title = Proxmox.Utils.dialog_title(me.subject, me.isCreate, me.isAdd);
-       }
-
-       if (me.isCreate) {
-               me.buttons = [submitBtn];
-       } else {
-               me.buttons = [submitBtn, resetBtn];
-       }
-
-       if (inputPanel && inputPanel.hasAdvanced) {
-           let sp = Ext.state.Manager.getProvider();
-           let advchecked = sp.get('proxmox-advanced-cb');
-           inputPanel.setAdvancedVisible(advchecked);
-           me.buttons.unshift(
-              {
-                  xtype: 'proxmoxcheckbox',
-                  itemId: 'advancedcb',
-                  boxLabelAlign: 'before',
-                  boxLabel: gettext('Advanced'),
-                  stateId: 'proxmox-advanced-cb',
-                  value: advchecked,
-                  listeners: {
-                      change: function(cb, val) {
-                          inputPanel.setAdvancedVisible(val);
-                          sp.set('proxmox-advanced-cb', val);
-                      },
-                  },
-              },
-           );
-       }
-
-       let onlineHelp = me.onlineHelp;
-       if (!onlineHelp && inputPanel && inputPanel.onlineHelp) {
-           onlineHelp = inputPanel.onlineHelp;
-       }
-
-       if (onlineHelp) {
-           let helpButton = Ext.create('Proxmox.button.Help');
-           me.buttons.unshift(helpButton, '->');
-           Ext.GlobalEvents.fireEvent('proxmoxShowHelp', onlineHelp);
-       }
-
-       Ext.applyIf(me, {
-           modal: true,
-           width: twoColumn ? colwidth*2 : colwidth,
-           border: false,
-           items: [me.formPanel],
-       });
-
-       me.callParent();
-
-       // always mark invalid fields
-       me.on('afterlayout', function() {
-           // on touch devices, the isValid function
-           // triggers a layout, which triggers an isValid
-           // and so on
-           // to prevent this we disable the layouting here
-           // and enable it afterwards
-           me.suspendLayout = true;
-           me.isValid();
-           me.suspendLayout = false;
-       });
-
-       if (me.autoLoad) {
-           me.load();
-       }
-    },
-});
diff --git a/window/LanguageEdit.js b/window/LanguageEdit.js
deleted file mode 100644 (file)
index 9176cfd..0000000
+++ /dev/null
@@ -1,49 +0,0 @@
-Ext.define('Proxmox.window.LanguageEditWindow', {
-    extend: 'Ext.window.Window',
-    alias: 'widget.pmxLanguageEditWindow',
-
-    viewModel: {
-       parent: null,
-       data: {
-           language: '__default__',
-       },
-    },
-    controller: {
-       xclass: 'Ext.app.ViewController',
-       init: function(view) {
-           let language = Ext.util.Cookies.get(view.cookieName) || '__default__';
-           this.getViewModel().set('language', language);
-       },
-       applyLanguage: function(button) {
-           let view = this.getView();
-           let vm = this.getViewModel();
-
-           let expire = Ext.Date.add(new Date(), Ext.Date.YEAR, 10);
-           Ext.util.Cookies.set(view.cookieName, vm.get('language'), expire);
-           view.mask(gettext('Please wait...'), 'x-mask-loading');
-           window.location.reload();
-       },
-    },
-
-    cookieName: 'PVELangCookie',
-
-    title: gettext('Language'),
-    modal: true,
-    bodyPadding: 10,
-    resizable: false,
-    items: [
-       {
-           xtype: 'proxmoxLanguageSelector',
-           fieldLabel: gettext('Language'),
-           bind: {
-               value: '{language}',
-           },
-       },
-    ],
-    buttons: [
-       {
-           text: gettext('Apply'),
-           handler: 'applyLanguage',
-       },
-    ],
-});
diff --git a/window/PasswordEdit.js b/window/PasswordEdit.js
deleted file mode 100644 (file)
index 5ab1517..0000000
+++ /dev/null
@@ -1,45 +0,0 @@
-Ext.define('Proxmox.window.PasswordEdit', {
-    extend: 'Proxmox.window.Edit',
-    alias: 'proxmoxWindowPasswordEdit',
-    mixins: ['Proxmox.Mixin.CBind'],
-
-    subject: gettext('Password'),
-
-    url: '/api2/extjs/access/password',
-
-    fieldDefaults: {
-       labelWidth: 120,
-    },
-
-    items: [
-       {
-           xtype: 'textfield',
-           inputType: 'password',
-           fieldLabel: gettext('Password'),
-           minLength: 5,
-           allowBlank: false,
-           name: 'password',
-           listeners: {
-               change: (field) => field.next().validate(),
-               blur: (field) => field.next().validate(),
-           },
-       },
-       {
-           xtype: 'textfield',
-           inputType: 'password',
-           fieldLabel: gettext('Confirm password'),
-           name: 'verifypassword',
-           allowBlank: false,
-           vtype: 'password',
-           initialPassField: 'password',
-           submitValue: false,
-       },
-       {
-           xtype: 'hiddenfield',
-           name: 'userid',
-           cbind: {
-               value: '{userid}',
-           },
-       },
-    ],
-});
diff --git a/window/TaskViewer.js b/window/TaskViewer.js
deleted file mode 100644 (file)
index 2f31023..0000000
+++ /dev/null
@@ -1,236 +0,0 @@
-Ext.define('Proxmox.window.TaskProgress', {
-    extend: 'Ext.window.Window',
-    alias: 'widget.proxmoxTaskProgress',
-
-    taskDone: Ext.emptyFn,
-
-    initComponent: function() {
-        let me = this;
-
-       if (!me.upid) {
-           throw "no task specified";
-       }
-
-       let task = Proxmox.Utils.parse_task_upid(me.upid);
-
-       let statstore = Ext.create('Proxmox.data.ObjectStore', {
-            url: "/api2/json/nodes/" + task.node + "/tasks/" + me.upid + "/status",
-           interval: 1000,
-           rows: {
-               status: { defaultValue: 'unknown' },
-               exitstatus: { defaultValue: 'unknown' },
-           },
-       });
-
-       me.on('destroy', statstore.stopUpdate);
-
-       let getObjectValue = function(key, defaultValue) {
-           let rec = statstore.getById(key);
-           if (rec) {
-               return rec.data.value;
-           }
-           return defaultValue;
-       };
-
-       let pbar = Ext.create('Ext.ProgressBar', { text: 'running...' });
-
-       me.mon(statstore, 'load', function() {
-           let status = getObjectValue('status');
-           if (status === 'stopped') {
-               let exitstatus = getObjectValue('exitstatus');
-               if (exitstatus === 'OK') {
-                   pbar.reset();
-                   pbar.updateText("Done!");
-                   Ext.Function.defer(me.close, 1000, me);
-               } else {
-                   me.close();
-                   Ext.Msg.alert('Task failed', exitstatus);
-               }
-               me.taskDone(exitstatus === 'OK');
-           }
-       });
-
-       let descr = Proxmox.Utils.format_task_description(task.type, task.id);
-
-       Ext.apply(me, {
-           title: gettext('Task') + ': ' + descr,
-           width: 300,
-           layout: 'auto',
-           modal: true,
-           bodyPadding: 5,
-           items: pbar,
-           buttons: [
-               {
-                   text: gettext('Details'),
-                   handler: function() {
-                       let win = Ext.create('Proxmox.window.TaskViewer', {
-                           taskDone: me.taskDone,
-                           upid: me.upid,
-                       });
-                       win.show();
-                       me.close();
-                   },
-               },
-           ],
-       });
-
-       me.callParent();
-
-       statstore.startUpdate();
-
-       pbar.wait();
-    },
-});
-
-// fixme: how can we avoid those lint errors?
-
-Ext.define('Proxmox.window.TaskViewer', {
-    extend: 'Ext.window.Window',
-    alias: 'widget.proxmoxTaskViewer',
-
-    extraTitle: '', // string to prepend after the generic task title
-
-    taskDone: Ext.emptyFn,
-
-    initComponent: function() {
-        let me = this;
-
-       if (!me.upid) {
-           throw "no task specified";
-       }
-
-       let task = Proxmox.Utils.parse_task_upid(me.upid);
-
-       let statgrid;
-
-       let rows = {
-           status: {
-               header: gettext('Status'),
-               defaultValue: 'unknown',
-               renderer: function(value) {
-                   if (value !== 'stopped') {
-                       return value;
-                   }
-                   let es = statgrid.getObjectValue('exitstatus');
-                   if (es) {
-                       return value + ': ' + es;
-                   }
-                   return 'unknown';
-               },
-           },
-           exitstatus: {
-               visible: false,
-           },
-           type: {
-               header: gettext('Task type'),
-               required: true,
-           },
-           user: {
-               header: gettext('User name'),
-               renderer: Ext.String.htmlEncode,
-               required: true,
-           },
-           node: {
-               header: gettext('Node'),
-               required: true,
-           },
-           pid: {
-               header: gettext('Process ID'),
-               required: true,
-           },
-           task_id: {
-               header: gettext('Task ID'),
-           },
-           starttime: {
-               header: gettext('Start Time'),
-               required: true,
-               renderer: Proxmox.Utils.render_timestamp,
-           },
-           upid: {
-               header: gettext('Unique task ID'),
-               renderer: Ext.String.htmlEncode,
-           },
-       };
-
-       let statstore = Ext.create('Proxmox.data.ObjectStore', {
-            url: "/api2/json/nodes/" + task.node + "/tasks/" + me.upid + "/status",
-           interval: 1000,
-           rows: rows,
-       });
-
-       me.on('destroy', statstore.stopUpdate);
-
-       let stop_task = function() {
-           Proxmox.Utils.API2Request({
-               url: "/nodes/" + task.node + "/tasks/" + me.upid,
-               waitMsgTarget: me,
-               method: 'DELETE',
-               failure: function(response, opts) {
-                   Ext.Msg.alert(gettext('Error'), response.htmlStatus);
-               },
-           });
-       };
-
-       let stop_btn1 = new Ext.Button({
-           text: gettext('Stop'),
-           disabled: true,
-           handler: stop_task,
-       });
-
-       let stop_btn2 = new Ext.Button({
-           text: gettext('Stop'),
-           disabled: true,
-           handler: stop_task,
-       });
-
-       statgrid = Ext.create('Proxmox.grid.ObjectGrid', {
-           title: gettext('Status'),
-           layout: 'fit',
-           tbar: [stop_btn1],
-           rstore: statstore,
-           rows: rows,
-           border: false,
-       });
-
-       let logView = Ext.create('Proxmox.panel.LogView', {
-           title: gettext('Output'),
-           tbar: [stop_btn2],
-           border: false,
-           url: "/api2/extjs/nodes/" + task.node + "/tasks/" + me.upid + "/log",
-       });
-
-       me.mon(statstore, 'load', function() {
-           let status = statgrid.getObjectValue('status');
-
-           if (status === 'stopped') {
-               logView.scrollToEnd = false;
-               logView.requestUpdate();
-               statstore.stopUpdate();
-               me.taskDone(statgrid.getObjectValue('exitstatus') === 'OK');
-           }
-
-           stop_btn1.setDisabled(status !== 'running');
-           stop_btn2.setDisabled(status !== 'running');
-       });
-
-       statstore.startUpdate();
-
-       Ext.apply(me, {
-           title: "Task viewer: " + task.desc + me.extraTitle,
-           width: 800,
-           height: 400,
-           layout: 'fit',
-           modal: true,
-           items: [{
-               xtype: 'tabpanel',
-               region: 'center',
-               items: [logView, statgrid],
-           }],
-        });
-
-       me.callParent();
-
-       logView.fireEvent('show', logView);
-    },
-});
-