From: Thomas Lamprecht Date: Sat, 6 Jun 2020 15:35:28 +0000 (+0200) Subject: cleanly separate sources from package build, move to own folder X-Git-Url: https://git.proxmox.com/?p=proxmox-widget-toolkit.git;a=commitdiff_plain;h=ecabd4379cdc61d0b8d3513fc53bce082d623827 cleanly separate sources from package build, move to own folder compared result with `diffoscope`, saw now difference Signed-off-by: Thomas Lamprecht --- diff --git a/Logo.js b/Logo.js deleted file mode 100644 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(); - }, -}); diff --git a/Makefile b/Makefile index 1d1dafb..f3bd2cf 100644 --- a/Makefile +++ b/Makefile @@ -1,67 +1,18 @@ 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}: - 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} @@ -76,20 +27,8 @@ ${DSC}: ${BUILDDIR} lintian ${DSC} .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 + ${MAKE} -C src lint .PHONY: upload upload: ${DEB} @@ -97,7 +36,7 @@ upload: ${DEB} 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 diff --git a/Toolkit.js b/Toolkit.js deleted file mode 100644 index d528ea7..0000000 --- a/Toolkit.js +++ /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
' + 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
' + 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') + ': vmbrN, where 0 <= N <= 9999', - - BondName: function(v) { - return (/^bond\d{1,4}$/).test(v); - }, - BondNameText: gettext('Format') + ': bondN, where 0 <= N <= 9999', - - InterfaceName: function(v) { - return (/^[a-z][a-z0-9_]{1,20}$/).test(v); - }, - InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'
" + - gettext("Minimum characters") + ": 2
" + - gettext("Maximum characters") + ": 21
" + - 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', '-', '_', '.'
" + - gettext("Minimum characters") + ": 2
" + - gettext("Must start with") + ": 'A-Z', 'a-z'
" + - gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'
", - - ConfigId: function(v) { - return (/^[a-z][a-z0-9_]+$/i).test(v); - }, - ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'
" + - gettext("Minimum characters") + ": 2
" + - gettext("Must start with") + ": " + gettext("letter"), - - HttpProxy: function(v) { - return (/^http:\/\/.*$/).test(v); - }, - HttpProxyText: gettext('Example') + ": http://username:password@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); - // - // tell the spec runner to ignore this element when checking if the dom is clean - el.dom.setAttribute('data-sticky', true); - // - } - - 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: [ - '', - ], - 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 index 93c773c..0000000 --- a/button/Button.js +++ /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 index d97a6c4..0000000 --- a/button/HelpButton.js +++ /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 index 5054b55..0000000 --- a/css/Makefile +++ /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 index a7d446b..0000000 --- a/css/ext6-pmx.css +++ /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 index e4143f5..0000000 --- a/data/DiffStore.js +++ /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 index 860cbfd..0000000 --- a/data/ObjectStore.js +++ /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 index 53e92f3..0000000 --- a/data/ProxmoxProxy.js +++ /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 index 67ffb57..0000000 --- a/data/RRDStore.js +++ /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 index a67ad8b..0000000 --- a/data/TimezoneStore.js +++ /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 index be85e4f..0000000 --- a/data/UpdateStore.js +++ /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 index dce270d..0000000 --- a/data/model/Realm.js +++ /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 index 0489df8..0000000 --- a/data/reader/JsonObject.js +++ /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; - }, -}); - diff --git a/debian/control b/debian/control index 800192d..1f9f206 100644 --- a/debian/control +++ b/debian/control @@ -3,7 +3,8 @@ Section: web Priority: optional Maintainer: Proxmox Support Team Build-Depends: debhelper (>= 10~), - rsync + pve-eslint, + rsync, Standards-Version: 3.9.8 Homepage: http://www.proxmox.com diff --git a/defines.mk b/defines.mk deleted file mode 100644 index 99461e1..0000000 --- a/defines.mk +++ /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 index 1fe45b9..0000000 --- a/form/BondModeSelector.js +++ /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 index 2b29e1c..0000000 --- a/form/Checkbox.js +++ /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 index e5a1920..0000000 --- a/form/ComboGrid.js +++ /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 index a061e15..0000000 --- a/form/DateTimeField.js +++ /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 index b8060a5..0000000 --- a/form/DisplayEdit.js +++ /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 index 61ff96f..0000000 --- a/form/ExpireDate.js +++ /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 index 67dd215..0000000 --- a/form/IntegerField.js +++ /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 index 361217e..0000000 --- a/form/KVComboBox.js +++ /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 index 92f28b1..0000000 --- a/form/LanguageSelector.js +++ /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 index b87efc1..0000000 --- a/form/NetworkSelector.js +++ /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 index 16cac20..0000000 --- a/form/RRDTypeSelector.js +++ /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 index 309bf4f..0000000 --- a/form/RealmComboBox.js +++ /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 index 142cdfd..0000000 --- a/form/RoleSelector.js +++ /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 index 56e5976..0000000 --- a/form/TextField.js +++ /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 index 541cf1d..0000000 --- a/grid/ObjectGrid.js +++ /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 index a5a640c..0000000 --- a/grid/PendingObjectGrid.js +++ /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 = '
'+ current +'
'; - } - } - - if (pending) { - return current + '
' + pending + '
'; - } 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 index 45bc50a..0000000 --- a/images/Makefile +++ /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 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 index afef53a..0000000 --- a/mixin/CBind.js +++ /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 index 1b564ef..0000000 --- a/node/APT.js +++ /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: '
' + - Ext.String.htmlEncode(data.Description) + - '
', - 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: '
' + gettext('No updates available.') + '
', - }, - 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 index 0b1135e..0000000 --- a/node/DNSEdit.js +++ /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 index e40e5a1..0000000 --- a/node/DNSView.js +++ /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 index 9adb6b2..0000000 --- a/node/HostsView.js +++ /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 index 72aab6f..0000000 --- a/node/NetworkEdit.js +++ /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 index 886b8de..0000000 --- a/node/NetworkView.js +++ /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("
" + Ext.htmlEncode(changes) + "
"); - 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('
') || ''; - }; - }; - - 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 index 88026a4..0000000 --- a/node/ServiceView.js +++ /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 index 3d9267e..0000000 --- a/node/Tasks.js +++ /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 index 613a2f5..0000000 --- a/node/TimeEdit.js +++ /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 index 9033d13..0000000 --- a/node/TimeView.js +++ /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 index 6cd6b60..0000000 --- a/panel/GaugeWidget.js +++ /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: '

{title}

', - }, - { - 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 index 0ac5e48..0000000 --- a/panel/InputPanel.js +++ /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 index b81cc45..0000000 --- a/panel/JournalView.js +++ /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('
'); - - 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 + '
' + view.content : text; - } else if (!top && num) { - view.content = view.content ? view.content + '
' + 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 index 1ce83bc..0000000 --- a/panel/LogView.js +++ /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('
'), 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 index b4f89a4..0000000 --- a/panel/RRDChart.js +++ /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}
${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 index 0000000..78d9aad --- /dev/null +++ b/src/Logo.js @@ -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 index 0000000..659e876 --- /dev/null +++ b/src/Makefile @@ -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 index 0000000..d528ea7 --- /dev/null +++ b/src/Toolkit.js @@ -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
' + 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
' + 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') + ': vmbrN, where 0 <= N <= 9999', + + BondName: function(v) { + return (/^bond\d{1,4}$/).test(v); + }, + BondNameText: gettext('Format') + ': bondN, where 0 <= N <= 9999', + + InterfaceName: function(v) { + return (/^[a-z][a-z0-9_]{1,20}$/).test(v); + }, + InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'
" + + gettext("Minimum characters") + ": 2
" + + gettext("Maximum characters") + ": 21
" + + 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', '-', '_', '.'
" + + gettext("Minimum characters") + ": 2
" + + gettext("Must start with") + ": 'A-Z', 'a-z'
" + + gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'
", + + ConfigId: function(v) { + return (/^[a-z][a-z0-9_]+$/i).test(v); + }, + ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'
" + + gettext("Minimum characters") + ": 2
" + + gettext("Must start with") + ": " + gettext("letter"), + + HttpProxy: function(v) { + return (/^http:\/\/.*$/).test(v); + }, + HttpProxyText: gettext('Example') + ": http://username:password@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); + // + // tell the spec runner to ignore this element when checking if the dom is clean + el.dom.setAttribute('data-sticky', true); + // + } + + 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: [ + '', + ], + 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 index 0000000..c3b13f4 --- /dev/null +++ b/src/Utils.js @@ -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 www.proxmox.com 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('
'); + }, + + 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 += "
"; + Ext.Object.each(result.errors, function(prop, desc) { + msg += "
" + Ext.htmlEncode(prop) + ": " + + 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 index 0000000..93c773c --- /dev/null +++ b/src/button/Button.js @@ -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 index 0000000..d97a6c4 --- /dev/null +++ b/src/button/HelpButton.js @@ -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 index 0000000..5054b55 --- /dev/null +++ b/src/css/Makefile @@ -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 index 0000000..a7d446b --- /dev/null +++ b/src/css/ext6-pmx.css @@ -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 index 0000000..e4143f5 --- /dev/null +++ b/src/data/DiffStore.js @@ -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 index 0000000..860cbfd --- /dev/null +++ b/src/data/ObjectStore.js @@ -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 index 0000000..53e92f3 --- /dev/null +++ b/src/data/ProxmoxProxy.js @@ -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 index 0000000..67ffb57 --- /dev/null +++ b/src/data/RRDStore.js @@ -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 index 0000000..a67ad8b --- /dev/null +++ b/src/data/TimezoneStore.js @@ -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 index 0000000..be85e4f --- /dev/null +++ b/src/data/UpdateStore.js @@ -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 index 0000000..dce270d --- /dev/null +++ b/src/data/model/Realm.js @@ -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 index 0000000..0489df8 --- /dev/null +++ b/src/data/reader/JsonObject.js @@ -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 index 0000000..c07a4e1 --- /dev/null +++ b/src/defines.mk @@ -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 index 0000000..1fe45b9 --- /dev/null +++ b/src/form/BondModeSelector.js @@ -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 index 0000000..2b29e1c --- /dev/null +++ b/src/form/Checkbox.js @@ -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 index 0000000..e5a1920 --- /dev/null +++ b/src/form/ComboGrid.js @@ -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 index 0000000..a061e15 --- /dev/null +++ b/src/form/DateTimeField.js @@ -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 index 0000000..b8060a5 --- /dev/null +++ b/src/form/DisplayEdit.js @@ -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 index 0000000..61ff96f --- /dev/null +++ b/src/form/ExpireDate.js @@ -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 index 0000000..67dd215 --- /dev/null +++ b/src/form/IntegerField.js @@ -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 index 0000000..361217e --- /dev/null +++ b/src/form/KVComboBox.js @@ -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 index 0000000..92f28b1 --- /dev/null +++ b/src/form/LanguageSelector.js @@ -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 index 0000000..b87efc1 --- /dev/null +++ b/src/form/NetworkSelector.js @@ -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 index 0000000..16cac20 --- /dev/null +++ b/src/form/RRDTypeSelector.js @@ -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 index 0000000..309bf4f --- /dev/null +++ b/src/form/RealmComboBox.js @@ -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 index 0000000..142cdfd --- /dev/null +++ b/src/form/RoleSelector.js @@ -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 index 0000000..56e5976 --- /dev/null +++ b/src/form/TextField.js @@ -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 index 0000000..541cf1d --- /dev/null +++ b/src/grid/ObjectGrid.js @@ -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 index 0000000..a5a640c --- /dev/null +++ b/src/grid/PendingObjectGrid.js @@ -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 = '
'+ current +'
'; + } + } + + if (pending) { + return current + '
' + pending + '
'; + } 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 index 0000000..45bc50a --- /dev/null +++ b/src/images/Makefile @@ -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 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 index 0000000..afef53a --- /dev/null +++ b/src/mixin/CBind.js @@ -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 index 0000000..1b564ef --- /dev/null +++ b/src/node/APT.js @@ -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: '
' + + Ext.String.htmlEncode(data.Description) + + '
', + 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: '
' + gettext('No updates available.') + '
', + }, + 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 index 0000000..0b1135e --- /dev/null +++ b/src/node/DNSEdit.js @@ -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 index 0000000..e40e5a1 --- /dev/null +++ b/src/node/DNSView.js @@ -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 index 0000000..9adb6b2 --- /dev/null +++ b/src/node/HostsView.js @@ -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 index 0000000..72aab6f --- /dev/null +++ b/src/node/NetworkEdit.js @@ -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 index 0000000..886b8de --- /dev/null +++ b/src/node/NetworkView.js @@ -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("
" + Ext.htmlEncode(changes) + "
"); + 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('
') || ''; + }; + }; + + 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 index 0000000..88026a4 --- /dev/null +++ b/src/node/ServiceView.js @@ -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 index 0000000..3d9267e --- /dev/null +++ b/src/node/Tasks.js @@ -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 index 0000000..613a2f5 --- /dev/null +++ b/src/node/TimeEdit.js @@ -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 index 0000000..9033d13 --- /dev/null +++ b/src/node/TimeView.js @@ -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 index 0000000..6cd6b60 --- /dev/null +++ b/src/panel/GaugeWidget.js @@ -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: '

{title}

', + }, + { + 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 index 0000000..0ac5e48 --- /dev/null +++ b/src/panel/InputPanel.js @@ -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 index 0000000..b81cc45 --- /dev/null +++ b/src/panel/JournalView.js @@ -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('
'); + + 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 + '
' + view.content : text; + } else if (!top && num) { + view.content = view.content ? view.content + '
' + 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 index 0000000..1ce83bc --- /dev/null +++ b/src/panel/LogView.js @@ -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('
'), 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 index 0000000..b4f89a4 --- /dev/null +++ b/src/panel/RRDChart.js @@ -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}
${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 index 0000000..c165141 --- /dev/null +++ b/src/window/Edit.js @@ -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: 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 index 0000000..9176cfd --- /dev/null +++ b/src/window/LanguageEdit.js @@ -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 index 0000000..5ab1517 --- /dev/null +++ b/src/window/PasswordEdit.js @@ -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 index 0000000..2f31023 --- /dev/null +++ b/src/window/TaskViewer.js @@ -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 index c165141..0000000 --- a/window/Edit.js +++ /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: 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 index 9176cfd..0000000 --- a/window/LanguageEdit.js +++ /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 index 5ab1517..0000000 --- a/window/PasswordEdit.js +++ /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 index 2f31023..0000000 --- a/window/TaskViewer.js +++ /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); - }, -}); -