Compare commits

..

13 Commits

Author SHA1 Message Date
Thomas Lamprecht
aa976ad3e5 eol notice: escalate to warning only shortly before EOL
The EOL message is positioned quite noticeable already in all our
products, being always visible. So use a notice-style for the icon
and its color until three weeks before EOL, when we switch to a
critical warning.

As it can be OK to run a, e.g., PVE 7 setup shortly before its EOL,
if, for example, one plans to replace it completely and decommission
the old one (so upgrade before EOL would be just extra work).

Using three weeks for the cut-off has no in-depth, heavily thought
out Good Reason™, but was rather chosen as it's likely to be noticed
before the actual EOL in somewhat actively maintained setups (e.g.,
admin checking in every week or two) and can give admins further
means to escalate things with higher ups. Also weeks are always 7
days, while months aren't uniform, so the former is easier to
communicate.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-16 12:57:11 +02:00
Thomas Lamprecht
931d9bfdfa eol notice: surpress highlighting if link was visited
add a class to the whole outer div to manage the CSS rules for the
EOL widget and use this to keep the original color for links even if
they got visited already.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-16 12:44:56 +02:00
Thomas Lamprecht
ad53b6dd22 bump version to 3.7.3
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-13 12:25:36 +02:00
Fiona Ebner
03f664dd2d apt repositories: avoid potential type error in classifyOrigin helper
with undefined (or otherwise falsy) argument.

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
(cherry picked from commit a14bafeca6)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-13 12:25:21 +02:00
Fiona Ebner
13b101437c apt repositories: fix typo for getting the default unknown text
Could lead to a type error with classifyOrigin when there is a
repository that doesn't have an InRelease file and cannot be detected
as Debian/Proxmox origin from its URL. For me, it triggered with the
element.io repository after changing to bookworm (which currently
doesn't exist yet) and running apt update.

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
(cherry picked from commit 78be60a079)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-13 12:25:21 +02:00
Thomas Lamprecht
07f7518735 bump version to 3.7.2
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-09 13:00:47 +02:00
Fiona Ebner
f7b2ed4f3a apt repositories: detect mixed suites before major upgrade
Usually, differing suites already produce warnings/errors, but before
a major upgrade the current and the next suite are both valid. Mixing
them is an issue though.

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
(cherry picked from commit e6ed4498cd)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-09 13:00:04 +02:00
Fiona Ebner
083c6f1d1a apt repositories: just ignore unknown info rather than throwing an error
This will avoid breaking older UI when extending the backend.

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
(cherry picked from commit b9b1a51a2e)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-09 13:00:04 +02:00
Fiona Ebner
ae66538726 apt repositories: add classifyOrigin helper
to be used again to detect mixed repositories before upgrade.

Needed to convert into an actual function for the 'this' usage.

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 3d6b76ee2b)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-09 13:00:04 +02:00
Fiona Ebner
a5106662fb apt repositories: actually ignore ignore-pre-upgrade-warning
when upgrading is possible rather than throwing an error by reaching
the else branch.

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit fd468868dd)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-09 13:00:04 +02:00
Thomas Lamprecht
62de2763ed bump version to 3.7.1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-07 17:26:01 +02:00
Fiona Ebner
622ba1f9e2 apt repositoires: allow major upgrade
Currently, all this does is silence warnings when the suite after the
current one is configured as well as enabling the check for mixed
repositiories.

Since there is no API call as mentioned in the comment yet, just set
it manually.

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
2023-06-07 17:22:28 +02:00
Aaron Lauterer
93f8588e41 tfa: paperkey: cleanup iframes for printing after window close
similar as recently done for the PBS storage add/edit window in
pve-manager

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
[ TL: refer to same change as done in pve-manager ]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-05-28 19:34:32 +02:00
63 changed files with 573 additions and 4609 deletions

12
.gitignore vendored
View File

@ -1,10 +1,4 @@
*.buildinfo
*.changes
*.deb
*.dsc
*.tar.xz
/proxmox-widget-toolkit-[0-9]*/
src/.lint-incremental
src/proxmox-dark/theme-proxmox-dark.css
src/proxmoxlib.js
src/proxmoxlib.min.js
*.deb
*.changes
*.buildinfo

370
debian/changelog vendored
View File

@ -1,382 +1,30 @@
proxmox-widget-toolkit (4.3.7) bookworm; urgency=medium
* authentication realm edit: use correct property to derive the realm type.
* authentication view: allow downstream users to override the API path to
query available authentication realms from.
* from: realm combobox: allow downstream users to override the API path to
query available authentication realms from.
-- Proxmox Support Team <support@proxmox.com> Wed, 26 Feb 2025 19:12:24 +0100
proxmox-widget-toolkit (4.3.6) bookworm; urgency=medium
* object grid: fix onlineHelp setting from editorConfig for row editors
-- Proxmox Support Team <support@proxmox.com> Tue, 25 Feb 2025 18:08:50 +0100
proxmox-widget-toolkit (4.3.5) bookworm; urgency=medium
* add form-field component for entering certificate fingerprints
* fix #6088: notification: matcher: use more descriptive strings
-- Proxmox Support Team <support@proxmox.com> Tue, 25 Feb 2025 17:08:11 +0100
proxmox-widget-toolkit (4.3.4) bookworm; urgency=medium
* textarea field: add emptyText message to show markdown is supported
* add missing htmlEncode for some UI elements
-- Proxmox Support Team <support@proxmox.com> Mon, 20 Jan 2025 11:38:34 +0100
proxmox-widget-toolkit (4.3.3) bookworm; urgency=medium
* display-edit field: add emptyText getter and setter to support binding to
the property of the underlying edit-field directly.
-- Proxmox Support Team <support@proxmox.com> Wed, 27 Nov 2024 12:25:44 +0100
proxmox-widget-toolkit (4.3.2) bookworm; urgency=medium
* node: service state: restore original behavior
* window: add consent modal widget
* object grid: add support for multiline textarea widget
-- Proxmox Support Team <support@proxmox.com> Mon, 25 Nov 2024 18:32:21 +0100
proxmox-widget-toolkit (4.3.1) bookworm; urgency=medium
* peraration to fix #5379: panel: authentication realm view: add opt-in
column displaying whether the realm is default and allow enabling it for a
realm
* various UX improvements for the webhook edit window:
- improve layout and component hierarchy
- use type in add button text
- show empty-text to key-value fields
- display validity for added key/value fields immediately
* add Bulgarian as available language
* dark theme: make icons in the permissions tree in Proxmox VE UI dark
* fix #3892: network: add bridge VIDs field for Linux bridge and enable them
if VLAN-aware is enabled.
-- Proxmox Support Team <support@proxmox.com> Tue, 19 Nov 2024 12:40:04 +0100
proxmox-widget-toolkit (4.3.0) bookworm; urgency=medium
* css: add some conditions to the tag classes for the tag view
* utils: add base64 conversion helper
* notification: add UI for adding/updating webhook targets
* fix #5836: ui: translate systemd states in system service view
* fix #5611: node service view: hide non-installed system services by
default
* password edit: allow one to override the minimum length parameter
* fix #5831: ui: right-align s.m.a.r.t numerical table data
-- Proxmox Support Team <support@proxmox.com> Mon, 11 Nov 2024 21:57:31 +0100
proxmox-widget-toolkit (4.2.4) bookworm; urgency=medium
* notification: matcher: match-field: show known fields/values
* notification: matcher: move match-severity and match-calendar fields to
panel
* css: dark theme: fix panel borders for the Proxmox Mail Gateway's EOL
notice widget
* fix opening a link from another site to a web UI of our products by
setting the auth-cookie's 'SameSite' attribute to lax, which is the safe
default in modern browsers.
-- Proxmox Support Team <support@proxmox.com> Wed, 16 Oct 2024 18:54:37 +0200
proxmox-widget-toolkit (4.2.3) bookworm; urgency=medium
* realm edit: don't send type as extra parameter when 'useTypeInUrl' is set
* realm edit: don't send 'delete' parameter when creating new entry
-- Proxmox Support Team <support@proxmox.com> Thu, 25 Apr 2024 11:45:12 +0200
proxmox-widget-toolkit (4.2.2) bookworm; urgency=medium
* form: move network VLAN field widget over from PVE
-- Proxmox Support Team <support@proxmox.com> Wed, 24 Apr 2024 21:44:12 +0200
proxmox-widget-toolkit (4.2.1) bookworm; urgency=medium
* fix #5251: tfa: set one-time-code auto-complete hint on TOTP input field
* sendmail: smtp: allow one to override the default mail author
-- Proxmox Support Team <support@proxmox.com> Tue, 23 Apr 2024 19:25:06 +0200
proxmox-widget-toolkit (4.2.0) bookworm; urgency=medium
* window: add widget for Active Directory specific LDAP-like authentication
* window: ldap: add tooltips for firstname, lastname and email attributes
* dark-mode: set intentionally black icons to `$icon-color`
* window: edit: avoid sharing custom config objects between subclasses
* i18n: make various user-facing strings translatable
* remove button: allow one to set custom confirmation message
* notify: change 'Remove' button to 'Reset' for built-in targets
* css: correctly mask disabled elements inside headers
* fix #5277: move reset button into window header toolbar. The form reset
and the form submit button where located besides each other with the exact
same styling. This made it easy to click the wrong one by accident, while
most of the time not a huge issue, it's quite annoying and just
unnecessary to do it this way. Moving the reset functionality into the
header, besides the close tool, avoid this potential mis-click and makes
the form simpler in general. Mis-clicks between the close and the reset
tool in the header are not an issue because they both have the same level
of destructiveness w.r.t. pending data. Using the font-awesome undo icon
makes it quite clear and a tooltip, which is also exposed as ARIA label,
helps to further improve accessibility.
* notes view:
- place collapse tool on the right
- use pencil-square-o icon for opening the editor
- make opening the editor on double-click opt-in to avoid interfering with
word-boundary selection of text by default.
* network edit: allow bridges to have any valid interface name
-- Proxmox Support Team <support@proxmox.com> Sun, 21 Apr 2024 12:31:26 +0200
proxmox-widget-toolkit (4.1.5) bookworm; urgency=medium
* dns: provide option to change behavior from sending the new, full actual
state to what actually changed. This fixes deleting entries in APIs like
the one from PBS.
* edit window: add optional custom submit options
* certificates: removal prompt: don't display name if there is no name
* utils: api request: defer masking after layout
* window: password edit: add opt-in config to show a confirmation-password
field for the current user
* window: password edit: clarify labels
-- Proxmox Support Team <support@proxmox.com> Thu, 21 Mar 2024 17:40:54 +0100
proxmox-widget-toolkit (4.1.4) bookworm; urgency=medium
* fix #5074: notify: sendmail smtp: fix mailto/mailto-user parameter
deletion
* i18n: use correct ISO 639-1 code for Korean with backward compat
* form: date time: fix changing date to end of month from a longer to a
shorter month
* form: combo grid: allow one to force showing a clear trigger through a new
showClearTrigger config value
* utils: add mechanism to add and override translatable notification event
descriptions in the product specific UIs
-- Proxmox Support Team <support@proxmox.com> Wed, 28 Feb 2024 11:46:31 +0100
proxmox-widget-toolkit (4.1.3) bookworm; urgency=medium
* notification ui: change icon for match-field tree nodes to avoid clash
with use for LXC containers
* notification ui: display yellow warning triangle instead of red icon if
the repesctive entry is not valid
-- Proxmox Support Team <support@proxmox.com> Thu, 23 Nov 2023 10:12:50 +0100
proxmox-widget-toolkit (4.1.2) bookworm; urgency=medium
* notification matcher: fix inverted match modes
* notification ui: add appropriate onlineHelp anchors
* notification ui: add 'unknown' to match-severity dropdown
-- Proxmox Support Team <support@proxmox.com> Tue, 21 Nov 2023 21:35:56 +0100
proxmox-widget-toolkit (4.1.1) bookworm; urgency=medium
* api-viewer: implement basic oneOf support
* form: displaye-edit: add one of the two missing returns
* notification ui: rework for new matcher based approach, drop old filter
and grouping widgets
* notification: matcher: add UI for matcher editing
* panel: notification: add gui for SMTP endpoints
* notification ui: add enable checkbox for targets/matchers
-- Proxmox Support Team <support@proxmox.com> Fri, 17 Nov 2023 16:56:06 +0100
proxmox-widget-toolkit (4.1.0) bookworm; urgency=medium
* text field: add trimValue option to auto-trim leading and trailing
whitespace from the to be submitted value
* schema: endpoint types: don't translate endpoint type names
* adapt the date time field to be more declarative
* fix #4442: extend the log view for firewall to allow filtering by a
date-time range
* disk list: render osdid-list if present
* file-level restore: enable the download-as-tar button by default, all use
sites support that feature now.
* apt updates: drop handling the ChangeLogUrl, it's not returned anymore
* combo grid: initialize value with [] by default to avoid glitches where
ExtJS sometimes does not finishes with initializing everything
-- Proxmox Support Team <support@proxmox.com> Tue, 14 Nov 2023 09:11:23 +0100
proxmox-widget-toolkit (4.0.9) bookworm; urgency=medium
* fix using gettext with parameter in various newly added notification and
sendmail widgets
* parser: split checking IMG and A tags, make the latter more strict
-- Proxmox Support Team <support@proxmox.com> Tue, 03 Oct 2023 10:39:31 +0200
proxmox-widget-toolkit (4.0.8) bookworm; urgency=medium
* fix #4531: acme plugins: correct change detection of dirty form fields
* fix #4951: accept undefined as value for the multi-disk selector
* auth: ldap: openid: use our proxmox textfield variant for the comment
field to avoid that an empty comments are saved in the config
* utils: language map: add entry for new Croatian translation
-- Proxmox Support Team <support@proxmox.com> Wed, 13 Sep 2023 17:16:01 +0200
proxmox-widget-toolkit (4.0.7) bookworm; urgency=medium
* fix #4874: improve error message for invalid hostname
* add entry for Georgian translation
* fix the UI not refreshing after successful certificate deletion
* add missing htmlEncode calls to network selector and apt repository panel
* add panels for notification system:
- sendmail endpoint panel
- gotify endpoint panel
- notification group management panel
- notification filter management panel
-- Proxmox Support Team <support@proxmox.com> Wed, 16 Aug 2023 10:43:02 +0200
proxmox-widget-toolkit (4.0.6) bookworm; urgency=medium
* LDAP realm edit: forbid specifying a bind_dn without a password
-- Proxmox Support Team <support@proxmox.com> Mon, 26 Jun 2023 20:24:57 +0200
proxmox-widget-toolkit (4.0.5) bookworm; urgency=medium
* add Українська - Ukrainian to language map
* apt repositories: add production ready warnings for Ceph repositories
* add TOTP second factor: increase the size of the quiet zone for the QR
code, following general recommendation to make scanning it more robust
-- Proxmox Support Team <support@proxmox.com> Fri, 16 Jun 2023 15:58:27 +0200
proxmox-widget-toolkit (4.0.4) bookworm; urgency=medium
proxmox-widget-toolkit (3.7.3) bullseye; urgency=medium
* apt repositories: fix typo for getting the default unknown text
* apt repositories: avoid potential type error in classifyOrigin helper
-- Proxmox Support Team <support@proxmox.com> Fri, 09 Jun 2023 17:29:44 +0200
-- Proxmox Support Team <support@proxmox.com> Tue, 13 Jun 2023 12:25:28 +0200
proxmox-widget-toolkit (4.0.3) bookworm; urgency=medium
* date time field: fix typo in xtype name but add alias for backward compat
* set 'SameSite' attr of auth cookie to 'strict', which modern browsers
already ensured in practice
proxmox-widget-toolkit (3.7.2) bullseye; urgency=medium
* apt repositories: actually ignore ignore-pre-upgrade-warning
* apt repositories: add classifyOrigin helper
* apt repositories: just ignore unknown info rather than throwing an error
* apt repositories: detect mixed suites before major upgrade
* tfa: improve UX for recovery keys and when none are left
-- Proxmox Support Team <support@proxmox.com> Fri, 09 Jun 2023 13:00:41 +0200
* tfa: show 'Locked' in 'Enabled' column if tfa is locked
-- Proxmox Support Team <support@proxmox.com> Fri, 09 Jun 2023 08:07:01 +0200
proxmox-widget-toolkit (4.0.2) bookworm; urgency=medium
* markdown parser: allow setting "target" attribute for links
* fix #4756: markdown parser: allow any valid URL for link (a) tags,
allowing one to add short-cuts like a RDP url to quickly open the remote
viewer.
* ship a minified version of the widget-toolkit JS library
-- Proxmox Support Team <support@proxmox.com> Sat, 03 Jun 2023 13:45:27 +0200
proxmox-widget-toolkit (4.0.1) bookworm; urgency=medium
proxmox-widget-toolkit (3.7.1) bullseye; urgency=medium
* tfa: paperkey: cleanup iframes for printing after window close
* parser: adapt to calling convention of updated Markdown renderer library
"marked" has since its v4.0.0
* apt repositoires: allow major upgrade
* fix #4551: ui: translate byte unit in `format_size`
-- Proxmox Support Team <support@proxmox.com> Thu, 01 Jun 2023 16:35:32 +0200
proxmox-widget-toolkit (4.0.0) bookworm; urgency=medium
* re-build for Debian 12 Bookworm based releases
-- Proxmox Support Team <support@proxmox.com> Thu, 25 May 2023 09:13:29 +0200
-- Proxmox Support Team <support@proxmox.com> Wed, 07 Jun 2023 17:25:57 +0200
proxmox-widget-toolkit (3.7.0) bullseye; urgency=medium

5
debian/control vendored
View File

@ -2,12 +2,11 @@ Source: proxmox-widget-toolkit
Section: web
Priority: optional
Maintainer: Proxmox Support Team <support@proxmox.com>
Build-Depends: debhelper-compat (= 13),
Build-Depends: debhelper-compat (= 12),
libjs-marked,
pve-eslint (>= 7.28.0),
sassc,
uglifyjs,
Standards-Version: 4.6.2
Standards-Version: 4.5.1
Homepage: https://www.proxmox.com
Package: proxmox-widget-toolkit

View File

@ -1,12 +1,10 @@
include defines.mk
ESLINT ?= $(if $(shell command -v pve-eslint), pve-eslint, eslint)
SUBDIRS= css images proxmox-dark
# bundle it for now from the libjs-marked debian package to avoid touching our proxies file mapper,
# we could also just ship a link to the packages file and load from same path as the widget-toolkit
MARKEDJS=/usr/share/javascript/marked/marked.js
MARKEDJS=/usr/share/javascript/marked/marked.min.js
JSSRC= \
Utils.js \
@ -22,7 +20,6 @@ JSSRC= \
data/ObjectStore.js \
data/RRDStore.js \
data/TimezoneStore.js \
data/model/NotificationConfig.js \
data/model/Realm.js \
data/model/Certificates.js \
data/model/ACME.js \
@ -31,8 +28,6 @@ JSSRC= \
form/ExpireDate.js \
form/IntegerField.js \
form/TextField.js \
form/TextAreaField.js \
form/VlanField.js \
form/DateTimeField.js \
form/Checkbox.js \
form/KVComboBox.js \
@ -50,7 +45,6 @@ JSSRC= \
form/ACME.js \
form/UserSelector.js \
form/ThemeSelector.js \
form/FingerprintField.js \
button/Button.js \
button/AltText.js \
button/HelpButton.js \
@ -63,24 +57,18 @@ JSSRC= \
panel/InfoWidget.js \
panel/LogView.js \
panel/NodeInfoRepoStatus.js \
panel/NotificationConfigView.js \
panel/JournalView.js \
panel/PermissionView.js \
panel/PruneKeepPanel.js \
panel/RRDChart.js \
panel/GaugeWidget.js \
panel/GotifyEditPanel.js \
panel/Certificates.js \
panel/ACMEAccount.js \
panel/ACMEPlugin.js \
panel/ACMEDomains.js \
panel/EmailRecipientPanel.js \
panel/SendmailEditPanel.js \
panel/SmtpEditPanel.js \
panel/StatusView.js \
panel/TfaView.js \
panel/NotesView.js \
panel/WebhookEditPanel.js \
window/Edit.js \
window/PasswordEdit.js \
window/SafeDestroy.js \
@ -90,18 +78,13 @@ JSSRC= \
window/DiskSmart.js \
window/ZFSDetail.js \
window/Certificates.js \
window/ConsentModal.js \
window/ACMEAccount.js \
window/ACMEPluginEdit.js \
window/ACMEDomains.js \
window/EndpointEditBase.js \
window/NotificationMatcherEdit.js \
window/FileBrowser.js \
window/AuthEditBase.js \
window/AuthEditOpenId.js \
window/AuthEditLDAP.js \
window/AuthEditAD.js \
window/AuthEditSimple.js \
window/TfaWindow.js \
window/AddTfaRecovery.js \
window/AddTotp.js \
@ -127,34 +110,29 @@ all: $(SUBDIRS)
set -e && for i in $(SUBDIRS); do $(MAKE) -C $$i; done
.lint-incremental: $(JSSRC)
$(ESLINT) $?
eslint $?
touch "$@"
.PHONY: lint
check: lint
$(ESLINT) --strict api-viewer/APIViewer.js
eslint --strict api-viewer/APIViewer.js
lint: $(JSSRC)
$(ESLINT) --strict $(JSSRC)
eslint --strict $(JSSRC)
touch ".lint-incremental"
BUILD_TIME=$(or $(SOURCE_DATE_EPOCH),$(shell date '+%s.%N'))
BUILD_VERSION=$(or $(DEB_VERSION),$(shell git rev-parse HEAD),unknown version)
proxmoxlib.js: .lint-incremental $(JSSRC)
# add the version as comment in the file
echo "// v$(BUILD_VERSION)-t$(BUILD_TIME)" > $@.tmp
echo "// v$(BUILD_VERSION) - t$(BUILD_TIME)" > $@.tmp
cat $(JSSRC) $(MARKEDJS) >> $@.tmp
mv $@.tmp $@
proxmoxlib.min.js: proxmoxlib.js
uglifyjs $< -c -m -o $@.tmp
mv $@.tmp $@
install: proxmoxlib.js proxmoxlib.min.js
install: proxmoxlib.js
install -d -m 755 $(WWWBASEDIR)
install -m 0644 proxmoxlib.js proxmoxlib.min.js $(WWWBASEDIR)
install -m 0644 proxmoxlib.js $(WWWBASEDIR)
set -e && for i in $(SUBDIRS); do $(MAKE) -C $$i $@; done
.PHONY: clean
clean:
$(MAKE) -C proxmox-dark $@
rm -f proxmoxlib.js* proxmoxlib.min.js* .lint-incremental
rm -f proxmoxlib.js

View File

@ -24,33 +24,22 @@ Ext.define('Proxmox.Markdown', {
for (let i=node.attributes.length; i--;) {
const name = node.attributes[i].name;
const value = node.attributes[i].value;
const canonicalTagName = node.tagName.toLowerCase();
// TODO: we may want to also disallow class and id attrs
if (
!/^(class|id|name|href|src|alt|align|valign|disabled|checked|start|type|target)$/i.test(name)
!/^(class|id|name|href|src|alt|align|valign|disabled|checked|start|type)$/i.test(name)
) {
node.attributes.removeNamedItem(name);
} else if ((name === 'href' || name === 'src') && !_isHTTPLike(value)) {
let safeURL = false;
try {
let url = new URL(value, window.location.origin);
safeURL = _isHTTPLike(url.protocol);
if (canonicalTagName === 'img' && url.protocol.toLowerCase() === 'data:') {
safeURL = true;
} else if (canonicalTagName === 'a') {
// allow most link protocols so admins can use short-cuts to, e.g., RDP
safeURL = url.protocol.toLowerCase() !== 'javascript:'; // eslint-disable-line no-script-url
}
if (safeURL) {
if (_isHTTPLike(url.protocol) || (node.tagName === 'img' && url.protocol === 'data:')) {
node.attributes[i].value = url.href;
} else {
node.attributes.removeNamedItem(name);
}
} catch (e) {
node.attributes.removeNamedItem(name);
node.attributes[i].removeNamedItem(name);
}
} else if (name === 'target' && canonicalTagName !== 'a') {
node.attributes.removeNamedItem(name);
}
}
for (let i=node.childNodes.length; i--;) _sanitize(node.childNodes[i]);
@ -66,7 +55,7 @@ Ext.define('Proxmox.Markdown', {
parse: function(markdown) {
/*global marked*/
let unsafeHTML = marked.parse(markdown);
let unsafeHTML = marked(markdown);
return `<div class="pmx-md">${this.sanitizeHTML(unsafeHTML)}</div>`;
},

View File

@ -4,13 +4,10 @@ Ext.define('Proxmox.Schema', { // a singleton
authDomains: {
pam: {
name: 'Linux PAM',
ipanel: 'pmxAuthSimplePanel',
onlineHelp: 'user-realms-pam',
add: false,
edit: true,
edit: false,
pwchange: true,
sync: false,
useTypeInUrl: false,
},
openid: {
name: gettext('OpenID Connect Server'),
@ -21,7 +18,6 @@ Ext.define('Proxmox.Schema', { // a singleton
pwchange: false,
sync: false,
iconCls: 'pmx-itype-icon-openid-logo',
useTypeInUrl: true,
},
ldap: {
name: gettext('LDAP Server'),
@ -32,18 +28,6 @@ Ext.define('Proxmox.Schema', { // a singleton
tfa: true,
pwchange: false,
sync: true,
useTypeInUrl: true,
},
ad: {
name: gettext('Active Directory Server'),
ipanel: 'pmxAuthADPanel',
syncipanel: 'pmxAuthADSyncPanel',
add: true,
edit: true,
tfa: true,
pwchange: false,
sync: true,
useTypeInUrl: true,
},
},
// to add or change existing for product specific ones
@ -53,38 +37,6 @@ Ext.define('Proxmox.Schema', { // a singleton
}
},
notificationEndpointTypes: {
sendmail: {
name: 'Sendmail',
ipanel: 'pmxSendmailEditPanel',
iconCls: 'fa-envelope-o',
defaultMailAuthor: 'Proxmox VE',
},
smtp: {
name: 'SMTP',
ipanel: 'pmxSmtpEditPanel',
iconCls: 'fa-envelope-o',
defaultMailAuthor: 'Proxmox VE',
},
gotify: {
name: 'Gotify',
ipanel: 'pmxGotifyEditPanel',
iconCls: 'fa-bell-o',
},
webhook: {
name: 'Webhook',
ipanel: 'pmxWebhookEditPanel',
iconCls: 'fa-bell-o',
},
},
// to add or change existing for product specific ones
overrideEndpointTypes: function(extra) {
for (const [key, value] of Object.entries(extra)) {
Proxmox.Schema.notificationEndpointTypes[key] = value;
}
},
pxarFileTypes: {
b: { icon: 'cube', label: gettext('Block Device') },
c: { icon: 'tty', label: gettext('Character Device') },

View File

@ -21,7 +21,7 @@ Ext.apply(Ext.form.field.VTypes, {
IPCIDRAddressMask: /[\d./]/i,
IP6Address: function(v) {
return Proxmox.Utils.IP6_match.test(v);
return Proxmox.Utils.IP6_match.test(v);
},
IP6AddressText: gettext('Example') + ': 2001:DB8::42',
IP6AddressMask: /[A-Fa-f0-9:]/,
@ -42,7 +42,7 @@ Ext.apply(Ext.form.field.VTypes, {
IP6PrefixLengthMask: /[0-9]/,
IP64Address: function(v) {
return Proxmox.Utils.IP64_match.test(v);
return Proxmox.Utils.IP64_match.test(v);
},
IP64AddressText: gettext('Example') + ': 192.168.1.1 2001:DB8::42',
IP64AddressMask: /[A-Fa-f0-9.:]/,
@ -76,25 +76,25 @@ Ext.apply(Ext.form.field.VTypes, {
MacPrefixText: gettext('Example') + ': 02:8f - ' + gettext('only unicast addresses are allowed'),
BridgeName: function(v) {
return (/^[a-zA-Z][a-zA-Z0-9_]{0,9}$/).test(v);
return (/^vmbr\d{1,4}$/).test(v);
},
VlanName: function(v) {
if (Proxmox.Utils.VlanInterface_match.test(v)) {
return true;
return true;
} else if (Proxmox.Utils.Vlan_match.test(v)) {
return true;
return true;
}
return true;
},
BridgeNameText: gettext('Format') + ': alphanumeric string starting with a character',
BridgeNameText: gettext('Format') + ': vmbr<b>N</b>, where 0 <= <b>N</b> <= 9999',
BondName: function(v) {
return (/^bond\d{1,4}$/).test(v);
return (/^bond\d{1,4}$/).test(v);
},
BondNameText: gettext('Format') + ': bond<b>N</b>, where 0 <= <b>N</b> <= 9999',
InterfaceName: function(v) {
return (/^[a-z][a-z0-9_]{1,20}$/).test(v);
return (/^[a-z][a-z0-9_]{1,20}$/).test(v);
},
InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'<br />" +
gettext("Minimum characters") + ": 2<br />" +
@ -102,7 +102,7 @@ Ext.apply(Ext.form.field.VTypes, {
gettext("Must start with") + ": 'a-z'",
StorageId: function(v) {
return (/^[a-z][a-z0-9\-_.]*[a-z0-9]$/i).test(v);
return (/^[a-z][a-z0-9\-_.]*[a-z0-9]$/i).test(v);
},
StorageIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '-', '_', '.'<br />" +
gettext("Minimum characters") + ": 2<br />" +
@ -110,14 +110,14 @@ Ext.apply(Ext.form.field.VTypes, {
gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'<br />",
ConfigId: function(v) {
return (/^[a-z][a-z0-9_-]+$/i).test(v);
return (/^[a-z][a-z0-9_-]+$/i).test(v);
},
ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'<br />" +
gettext("Minimum characters") + ": 2<br />" +
gettext("Must start with") + ": " + gettext("letter"),
HttpProxy: function(v) {
return (/^http:\/\/.*$/).test(v);
return (/^http:\/\/.*$/).test(v);
},
HttpProxyText: gettext('Example') + ": http://username:password&#64;host:port/",
@ -129,16 +129,16 @@ Ext.apply(Ext.form.field.VTypes, {
DnsName: function(v) {
return Proxmox.Utils.DnsName_match.test(v);
},
DnsNameText: gettext('This is not a valid hostname'),
DnsNameText: gettext('This is not a valid DNS name'),
DnsNameOrWildcard: function(v) {
return Proxmox.Utils.DnsName_or_Wildcard_match.test(v);
},
DnsNameOrWildcardText: gettext('This is not a valid hostname'),
DnsNameOrWildcardText: gettext('This is not a valid DNS name'),
// email regex used by pve-common
proxmoxMail: function(v) {
return (/^[\w+-~]+(\.[\w+-~]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$/).test(v);
return (/^[\w+-~]+(\.[\w+-~]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$/).test(v);
},
proxmoxMailText: gettext('Example') + ": user@example.com",
@ -179,11 +179,11 @@ Ext.apply(Ext.form.field.VTypes, {
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;
if (field.initialPassField) {
let pwd = field.up('form').down(`[name=${field.initialPassField}]`);
return val === pwd.getValue();
}
return true;
},
passwordText: gettext('Passwords do not match'),
@ -216,30 +216,30 @@ Ext.define('Proxmox.UnderlayPool', {
override: 'Ext.dom.UnderlayPool',
checkOut: function() {
let cache = this.cache,
len = cache.length,
el;
let cache = this.cache,
len = cache.length,
el;
// do cleanup because some of the objects might have been destroyed
// do cleanup because some of the objects might have been destroyed
while (len--) {
if (cache[len].destroyed) {
cache.splice(len, 1);
}
}
// end do cleanup
if (cache[len].destroyed) {
cache.splice(len, 1);
}
}
// end do cleanup
el = cache.shift();
if (!el) {
el = Ext.Element.create(this.elementConfig);
el.setVisibilityMode(2);
//<debug>
// tell the spec runner to ignore this element when checking if the dom is clean
if (!el) {
el = Ext.Element.create(this.elementConfig);
el.setVisibilityMode(2);
//<debug>
// tell the spec runner to ignore this element when checking if the dom is clean
el.dom.setAttribute('data-sticky', true);
//</debug>
//</debug>
}
return el;
return el;
},
});
@ -515,28 +515,28 @@ Ext.define('Proxmox.selection.CheckboxModel', {
// [ P: optimized to remove all records at once as single remove is O(n^3) slow ]
// records can be an index, a record or an array of records
doDeselect: function(records, suppressEvent) {
var me = this,
selected = me.selected,
i = 0,
len, record,
commit;
if (me.locked || !me.store) {
return false;
}
if (typeof records === "number") {
// No matching record, jump out
record = me.store.getAt(records);
if (!record) {
return false;
}
records = [
record,
];
} else if (!Ext.isArray(records)) {
records = [
records,
];
}
var me = this,
selected = me.selected,
i = 0,
len, record,
commit;
if (me.locked || !me.store) {
return false;
}
if (typeof records === "number") {
// No matching record, jump out
record = me.store.getAt(records);
if (!record) {
return false;
}
records = [
record,
];
} else if (!Ext.isArray(records)) {
records = [
records,
];
}
// [P] a beforedeselection, triggered by me.onSelectChange below, can block removal by
// returning false, thus the original implementation removed only here in the commit fn,
// which has an abysmal performance O(n^3). As blocking removal is not the norm, go do the
@ -550,119 +550,119 @@ Ext.define('Proxmox.selection.CheckboxModel', {
}
};
let removalBlocked = [];
len = records.length;
me.suspendChanges();
for (; i < len; i++) {
record = records[i];
if (me.isSelected(record)) {
len = records.length;
me.suspendChanges();
for (; i < len; i++) {
record = records[i];
if (me.isSelected(record)) {
committed = false;
me.onSelectChange(record, false, suppressEvent, commit);
me.onSelectChange(record, false, suppressEvent, commit);
if (!committed) {
removalBlocked.push(record);
}
if (me.destroyed) {
return false;
}
}
}
if (me.destroyed) {
return false;
}
}
}
if (removalBlocked.length > 0) {
records.remove(removalBlocked);
}
selected.remove(records); // [P] FAST(er)
me.lastSelected = selected.last();
me.resumeChanges();
// fire selchange if there was a change and there is no suppressEvent flag
me.resumeChanges();
// fire selchange if there was a change and there is no suppressEvent flag
me.maybeFireSelectionChange(records.length > 0 && !suppressEvent);
return records.length;
},
doMultiSelect: function(records, keepExisting, suppressEvent) {
var me = this,
selected = me.selected,
change = false,
result, i, len, record, commit;
var me = this,
selected = me.selected,
change = false,
result, i, len, record, commit;
if (me.locked) {
return;
}
if (me.locked) {
return;
}
records = !Ext.isArray(records) ? [records] : records;
len = records.length;
if (!keepExisting && selected.getCount() > 0) {
result = me.deselectDuringSelect(records, suppressEvent);
if (me.destroyed) {
return;
}
if (result[0]) {
// We had a failure during selection, so jump out
// Fire selection change if we did deselect anything
me.maybeFireSelectionChange(result[1] > 0 && !suppressEvent);
return;
} else {
// Means something has been deselected, so we've had a change
change = result[1] > 0;
}
}
records = !Ext.isArray(records) ? [records] : records;
len = records.length;
if (!keepExisting && selected.getCount() > 0) {
result = me.deselectDuringSelect(records, suppressEvent);
if (me.destroyed) {
return;
}
if (result[0]) {
// We had a failure during selection, so jump out
// Fire selection change if we did deselect anything
me.maybeFireSelectionChange(result[1] > 0 && !suppressEvent);
return;
} else {
// Means something has been deselected, so we've had a change
change = result[1] > 0;
}
}
let gotBlocked, blockedRecords = [];
commit = function() {
if (!selected.getCount()) {
me.selectionStart = record;
}
commit = function() {
if (!selected.getCount()) {
me.selectionStart = record;
}
gotBlocked = false;
change = true;
};
change = true;
};
for (i = 0; i < len; i++) {
record = records[i];
if (me.isSelected(record)) {
continue;
}
for (i = 0; i < len; i++) {
record = records[i];
if (me.isSelected(record)) {
continue;
}
gotBlocked = true;
me.onSelectChange(record, true, suppressEvent, commit);
if (me.destroyed) {
return;
}
me.onSelectChange(record, true, suppressEvent, commit);
if (me.destroyed) {
return;
}
if (gotBlocked) {
blockedRecords.push(record);
}
}
}
if (blockedRecords.length > 0) {
records.remove(blockedRecords);
}
selected.add(records);
me.lastSelected = record;
selected.add(records);
me.lastSelected = record;
// fire selchange if there was a change and there is no suppressEvent flag
me.maybeFireSelectionChange(change && !suppressEvent);
// fire selchange if there was a change and there is no suppressEvent flag
me.maybeFireSelectionChange(change && !suppressEvent);
},
deselectDuringSelect: function(toSelect, suppressEvent) {
var me = this,
selected = me.selected.getRange(),
changed = 0,
failed = false;
// Prevent selection change events from firing, will happen during select
me.suspendChanges();
me.deselectingDuringSelect = true;
var me = this,
selected = me.selected.getRange(),
changed = 0,
failed = false;
// Prevent selection change events from firing, will happen during select
me.suspendChanges();
me.deselectingDuringSelect = true;
let toDeselect = selected.filter(item => !Ext.Array.contains(toSelect, item));
if (toDeselect.length > 0) {
changed = me.doDeselect(toDeselect, suppressEvent);
if (!changed) {
failed = true;
}
if (me.destroyed) {
failed = true;
changed = 0;
}
}
me.deselectingDuringSelect = false;
me.resumeChanges();
return [
failed,
changed,
];
}
if (me.destroyed) {
failed = true;
changed = 0;
}
}
me.deselectingDuringSelect = false;
me.resumeChanges();
return [
failed,
changed,
];
},
});
@ -678,11 +678,11 @@ Ext.define('Proxmox.view.DragZone', {
override: 'Ext.view.DragZone',
onItemMouseDown: function(view, record, item, index, e) {
// Ignore touchstart.
// For touch events, we use longpress.
if (e.pointerType !== 'touch') {
this.onTriggerGesture(view, record, item, index, e);
}
// Ignore touchstart.
// For touch events, we use longpress.
if (e.pointerType !== 'touch') {
this.onTriggerGesture(view, record, item, index, e);
}
},
});
@ -702,39 +702,6 @@ Ext.define('Proxmox.dd.DragDropManager', {
},
});
// make it possible to set the SameSite attribute on cookies
Ext.define('Proxmox.Cookies', {
override: 'Ext.util.Cookies',
set: function(name, value, expires, path, domain, secure, samesite) {
let attrs = [];
if (expires) {
attrs.push("expires=" + expires.toUTCString());
}
if (path === undefined) { // mimic original function's behaviour
attrs.push("path=/");
} else if (path) {
attrs.push("path=" + path);
}
if (domain) {
attrs.push("domain=" + domain);
}
if (secure === true) {
attrs.push("secure");
}
if (samesite && ["lax", "none", "strict"].includes(samesite.toLowerCase())) {
attrs.push("samesite=" + samesite);
}
document.cookie = name + "=" + escape(value) + "; " + attrs.join("; ");
},
});
// 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
@ -756,11 +723,6 @@ Ext.onReady(function() {
},
});
});
// add allowfullscreen to render template to allow the noVNC/xterm.js embedded UIs to go fullscreen
//
// The rest is the same as in the separate ux package (extjs/build/packages/ux/classic/ux-debug.js),
// which we do not load as it's rather big and most of the widgets there are not useful for our UIs
Ext.define('Ext.ux.IFrame', {
extend: 'Ext.Component',
@ -771,84 +733,154 @@ Ext.define('Ext.ux.IFrame', {
src: 'about:blank',
renderTpl: [
// eslint-disable-next-line max-len
'<iframe src="{src}" id="{id}-iframeEl" data-ref="iframeEl" name="{frameName}" width="100%" height="100%" frameborder="0" allowfullscreen="true"></iframe>',
'<iframe src="{src}" id="{id}-iframeEl" data-ref="iframeEl" name="{frameName}" width="100%" height="100%" frameborder="0" allowfullscreen="true"></iframe>',
],
childEls: ['iframeEl'],
initComponent: function() {
this.callParent();
this.callParent();
this.frameName = this.frameName || this.id + '-frame';
this.frameName = this.frameName || this.id + '-frame';
},
initEvents: function() {
let me = this;
me.callParent();
me.iframeEl.on('load', me.onLoad, me);
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,
});
return Ext.apply(this.callParent(), {
src: this.src,
frameName: this.frameName,
});
},
getBody: function() {
let doc = this.getDoc();
return doc.body || doc.documentElement;
let doc = this.getDoc();
return doc.body || doc.documentElement;
},
getDoc: function() {
try {
return this.getWin().document;
} catch (ex) {
return null;
}
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;
let me = this,
name = me.frameName,
win = Ext.isIE
? me.iframeEl.dom.contentWindow
: window.frames[name];
return win;
},
getFrame: function() {
let me = this;
let me = this;
return me.iframeEl.dom;
},
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();
let me = this,
doc = me.getDoc(),
fn = me.onRelayedEvent;
if (doc) {
this.el.unmask();
this.fireEvent('load', this);
} else if (me.src) {
this.el.unmask();
this.fireEvent('error', this);
}
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.
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,
},
);
} 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();
let me = this,
text = me.loadMask,
frame = me.getFrame();
if (me.fireEvent('beforeload', me, src) !== false) {
if (text && me.el) {
me.el.mask(text);
}
if (me.fireEvent('beforeload', me, src) !== false) {
if (text && me.el) {
me.el.mask(text);
}
frame.src = me.src = src || me.src;
}
frame.src = me.src = src || me.src;
}
},
});

View File

@ -64,7 +64,6 @@ utilities: {
language_map: { //language map is sorted alphabetically by iso 639-1
ar: `العربية - ${gettext("Arabic")}`,
bg: `Български - ${gettext("Bulgarian")}`,
ca: `Català - ${gettext("Catalan")}`,
da: `Dansk - ${gettext("Danish")}`,
de: `Deutsch - ${gettext("German")}`,
@ -73,12 +72,10 @@ utilities: {
eu: `Euskera (Basque) - ${gettext("Euskera (Basque)")}`,
fa: `فارسی - ${gettext("Persian (Farsi)")}`,
fr: `Français - ${gettext("French")}`,
hr: `Hrvatski - ${gettext("Croatian")}`,
he: `עברית - ${gettext("Hebrew")}`,
it: `Italiano - ${gettext("Italian")}`,
ja: `日本語 - ${gettext("Japanese")}`,
ka: `ქართული - ${gettext("Georgian")}`,
ko: `한국어 - ${gettext("Korean")}`,
kr: `한국어 - ${gettext("Korean")}`,
nb: `Bokmål - ${gettext("Norwegian (Bokmal)")}`,
nl: `Nederlands - ${gettext("Dutch")}`,
nn: `Nynorsk - ${gettext("Norwegian (Nynorsk)")}`,
@ -88,7 +85,6 @@ utilities: {
sl: `Slovenščina - ${gettext("Slovenian")}`,
sv: `Svenska - ${gettext("Swedish")}`,
tr: `Türkçe - ${gettext("Turkish")}`,
ukr: `Українська - ${gettext("Ukrainian")}`,
zh_CN: `中文(简体)- ${gettext("Chinese (Simplified)")}`,
zh_TW: `中文(繁體)- ${gettext("Chinese (Traditional)")}`,
},
@ -97,9 +93,6 @@ utilities: {
if (!value || value === '__default__') {
return Proxmox.Utils.defaultText + ' (English)';
}
if (value === 'kr') {
value = 'ko'; // fix-up wrongly used Korean code. FIXME: remove with trixie releases
}
let text = Proxmox.Utils.language_map[value];
if (text) {
return text + ' (' + value + ')';
@ -156,11 +149,8 @@ utilities: {
},
getNoSubKeyHtml: function(url) {
let html_url = Ext.String.format('<a target="_blank" href="{0}">www.proxmox.com</a>', url || 'https://www.proxmox.com');
return Ext.String.format(
gettext('You do not have a valid subscription for this server. Please visit {0} to get a list of available options.'),
html_url,
);
// url http://www.proxmox.com/products/proxmox-ve/subscription-service-plans
return Ext.String.format('You do not have a valid subscription for this server. Please visit <a target="_blank" href="{0}">www.proxmox.com</a> to get a list of available options.', url || 'https://www.proxmox.com');
},
format_boolean_with_default: function(value) {
@ -318,7 +308,7 @@ utilities: {
// that way the cookie gets deleted after the browser window is closed
if (data.ticket) {
Proxmox.CSRFPreventionToken = data.CSRFPreventionToken;
Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, data.ticket, null, '/', null, true, "lax");
Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, data.ticket, null, '/', null, true);
}
if (data.token) {
@ -344,7 +334,7 @@ utilities: {
return;
}
// ExtJS clear is basically the same, but browser may complain if any cookie isn't "secure"
Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, "", new Date(0), null, null, true, "lax");
Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, "", new Date(0), null, null, true);
window.localStorage.removeItem("ProxmoxUser");
},
@ -463,12 +453,6 @@ utilities: {
newopts.url = '/api2/extjs' + newopts.url;
}
delete newopts.callback;
let unmask = (target) => {
if (target.waitMsgTargetCount === undefined || --target.waitMsgTargetCount <= 0) {
target.setLoading(false);
delete target.waitMsgTargetCount;
}
};
let createWrapper = function(successFn, callbackFn, failureFn) {
Ext.apply(newopts, {
@ -477,7 +461,7 @@ utilities: {
if (Proxmox.Utils.toolkit === 'touch') {
options.waitMsgTarget.setMasked(false);
} else {
unmask(options.waitMsgTarget);
options.waitMsgTarget.setLoading(false);
}
}
let result = Ext.decode(response.responseText);
@ -499,7 +483,7 @@ utilities: {
if (Proxmox.Utils.toolkit === 'touch') {
options.waitMsgTarget.setMasked(false);
} else {
unmask(options.waitMsgTarget);
options.waitMsgTarget.setLoading(false);
}
}
response.result = {};
@ -529,16 +513,9 @@ utilities: {
if (target) {
if (Proxmox.Utils.toolkit === 'touch') {
target.setMasked({ xtype: 'loadmask', message: newopts.waitMsg });
} else if (target.rendered) {
target.waitMsgTargetCount = (target.waitMsgTargetCount ?? 0) + 1;
target.setLoading(newopts.waitMsg);
} else {
target.waitMsgTargetCount = (target.waitMsgTargetCount ?? 0) + 1;
target.on('afterlayout', function() {
if ((target.waitMsgTargetCount ?? 0) > 0) {
target.setLoading(newopts.waitMsg);
}
}, target, { single: true });
// Note: ExtJS bug - this does not work when component is not rendered
target.setLoading(newopts.waitMsg);
}
}
Ext.Ajax.request(newopts);
@ -548,7 +525,12 @@ utilities: {
// Proxmox.Async.api2({
// ...
// }).catch(Proxmox.Utils.alertResponseFailure);
alertResponseFailure: res => Ext.Msg.alert(gettext('Error'), res.htmlStatus || res.result.message),
alertResponseFailure: (response) => {
Ext.Msg.alert(
gettext('Error'),
response.htmlStatus || response.result.message,
);
},
checked_command: function(orig_cmd) {
Proxmox.Utils.API2Request(
@ -583,7 +565,7 @@ utilities: {
},
assemble_field_data: function(values, data) {
if (!Ext.isObject(data)) {
if (!Ext.isObject(data)) {
return;
}
Ext.Object.each(data, function(name, val) {
@ -665,37 +647,6 @@ utilities: {
Proxmox.Utils.unknownText;
},
// Only add product-agnostic fields here!
notificationFieldName: {
'type': gettext('Notification type'),
'hostname': gettext('Hostname'),
},
formatNotificationFieldName: (value) =>
Proxmox.Utils.notificationFieldName[value] || value,
// to add or change existing for product specific ones
overrideNotificationFieldName: function(extra) {
for (const [key, value] of Object.entries(extra)) {
Proxmox.Utils.notificationFieldName[key] = value;
}
},
// Only add product-agnostic fields here!
notificationFieldValue: {
'system-mail': gettext('Forwarded mails to the local root user'),
},
formatNotificationFieldValue: (value) =>
Proxmox.Utils.notificationFieldValue[value] || value,
// to add or change existing for product specific ones
overrideNotificationFieldValue: function(extra) {
for (const [key, value] of Object.entries(extra)) {
Proxmox.Utils.notificationFieldValue[key] = value;
}
},
// NOTE: only add general, product agnostic, ones here! Else use override helper in product repos
task_desc_table: {
aptupdate: ['', gettext('Update package database')],
@ -737,23 +688,21 @@ utilities: {
},
format_size: function(size, useSI) {
let unitsSI = [gettext('B'), gettext('KB'), gettext('MB'), gettext('GB'),
gettext('TB'), gettext('PB'), gettext('EB'), gettext('ZB'), gettext('YB')];
let unitsIEC = [gettext('B'), gettext('KiB'), gettext('MiB'), gettext('GiB'),
gettext('TiB'), gettext('PiB'), gettext('EiB'), gettext('ZiB'), gettext('YiB')];
let units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
let order = 0;
let commaDigits = 2;
const baseValue = useSI ? 1000 : 1024;
while (size >= baseValue && order < unitsSI.length) {
while (size >= baseValue && order < units.length) {
size = size / baseValue;
order++;
}
let unit = useSI ? unitsSI[order] : unitsIEC[order];
let unit = units[order], commaDigits = 2;
if (order === 0) {
commaDigits = 0;
} else if (!useSI) {
unit += 'i';
}
return `${size.toFixed(commaDigits)} ${unit}`;
return `${size.toFixed(commaDigits)} ${unit}B`;
},
SizeUnits: {
@ -928,7 +877,7 @@ utilities: {
let parsed = Proxmox.Utils.parse_task_status(status);
switch (parsed) {
case 'unknown': return Proxmox.Utils.unknownText;
case 'error': return Proxmox.Utils.errorText + ': ' + Ext.htmlEncode(status);
case 'error': return Proxmox.Utils.errorText + ': ' + status;
case 'warning': return status.replace('WARNINGS', Proxmox.Utils.warningsText);
case 'ok': // fall-through
default: return status;
@ -1357,24 +1306,6 @@ utilities: {
);
},
// Convert utf-8 string to base64.
// This also escapes unicode characters such as emojis.
utf8ToBase64: function(string) {
let bytes = new TextEncoder().encode(string);
const escapedString = Array.from(bytes, (byte) =>
String.fromCodePoint(byte),
).join("");
return btoa(escapedString);
},
// Converts a base64 string into a utf8 string.
// Decodes escaped unicode characters correctly.
base64ToUtf8: function(b64_string) {
let string = atob(b64_string);
let bytes = Uint8Array.from(string, (m) => m.codePointAt(0));
return new TextDecoder().decode(bytes);
},
stringToRGB: function(string) {
let hash = 0;
if (!string) {
@ -1525,26 +1456,6 @@ utilities: {
me.IP6_dotnotation_match = new RegExp("^(" + IPV6_REGEXP + ")(?:\\.(\\d+))?$");
me.Vlan_match = /^vlan(\d+)/;
me.VlanInterface_match = /(\w+)\.(\d+)/;
// Taken from proxmox-schema and ported to JS
let PORT_REGEX_STR = "(?:[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])";
let IPRE_BRACKET_STR = "(?:" + IPV4_REGEXP + "|\\[(?:" + IPV6_REGEXP + ")\\])";
let DNS_NAME_STR = "(?:(?:" + DnsName_REGEXP + "\\.)*" + DnsName_REGEXP + ")";
let HTTP_URL_REGEX = "^https?://(?:(?:(?:"
+ DNS_NAME_STR
+ "|"
+ IPRE_BRACKET_STR
+ ")(?::"
+ PORT_REGEX_STR
+ ")?)|"
+ IPV6_REGEXP
+ ")(?:/[^\x00-\x1F\x7F]*)?$";
me.httpUrlRegex = new RegExp(HTTP_URL_REGEX);
// Same as SAFE_ID_REGEX in proxmox-schema
me.safeIdRegex = /^(?:[A-Za-z0-9_][A-Za-z0-9._\\-]*)$/;
},
});

View File

@ -7,7 +7,7 @@ Ext.onReady(function() {
'name', 'type', 'typetext', 'description', 'verbose_description',
'enum', 'minimum', 'maximum', 'minLength', 'maxLength',
'pattern', 'title', 'requires', 'format', 'default',
'disallow', 'extends', 'links', 'instance-types',
'disallow', 'extends', 'links',
{
name: 'optional',
type: 'boolean',
@ -214,10 +214,6 @@ Ext.onReady(function() {
},
groupField: 'optional',
sorters: [
{
property: 'instance-types',
direction: 'ASC',
},
{
property: 'name',
direction: 'ASC',
@ -225,27 +221,9 @@ Ext.onReady(function() {
],
});
let has_type_properties = false;
Ext.Object.each(info.parameters.properties, function(name, pdef) {
if (pdef.oneOf) {
pdef.oneOf.forEach((alternative) => {
alternative.name = name;
pstore.add(alternative);
has_type_properties = true;
});
} else if (pdef['instance-types']) {
pdef['instance-types'].forEach((type) => {
let typePdef = Ext.apply({}, pdef);
typePdef.name = name;
typePdef['instance-types'] = [type];
pstore.add(typePdef);
has_type_properties = true;
});
} else {
pdef.name = name;
pstore.add(pdef);
}
pdef.name = name;
pstore.add(pdef);
});
pstore.sort();
@ -277,12 +255,6 @@ Ext.onReady(function() {
renderer: render_type,
flex: 1,
},
{
header: 'For Types',
dataIndex: 'instance-types',
hidden: !has_type_properties,
flex: 1,
},
{
header: 'Default',
dataIndex: 'default',

View File

@ -110,7 +110,6 @@ Ext.define('Proxmox.button.StdRemoveButton', {
config: {
baseurl: undefined,
customConfirmationMessage: undefined,
},
getUrl: function(rec) {
@ -134,14 +133,7 @@ Ext.define('Proxmox.button.StdRemoveButton', {
let me = this;
let name = me.getRecordName(rec);
let text;
if (me.customConfirmationMessage) {
text = me.customConfirmationMessage;
} else {
text = gettext('Are you sure you want to remove entry {0}');
}
return Ext.String.format(text, Ext.htmlEncode(`'${name}'`));
return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), `'${name}'`);
},
handler: function(btn, event, rec) {

View File

@ -15,9 +15,7 @@
}
.proxmox-tags-full .x-grid-cell-inner-treecolumn .proxmox-tag-light,
.proxmox-tags-full .x-grid-cell-inner-treecolumn .proxmox-tag-dark,
.x-grid-cell-inner-treecolumn .proxmox-tags-full .proxmox-tag-light,
.x-grid-cell-inner-treecolumn .proxmox-tags-full .proxmox-tag-dark {
.proxmox-tags-full .x-grid-cell-inner-treecolumn .proxmox-tag-dark {
display: inherit;
}
@ -27,10 +25,8 @@
}
.proxmox-tags-circle :not(span.proxmox-tags-full) > .proxmox-tag-light,
.proxmox-tags-circle :not(span.proxmox-tags-full) > .proxmox-tag-dark,
.proxmox-tags-circle > .proxmox-tag-light,
.proxmox-tags-circle > .proxmox-tag-dark {
.proxmox-tags-circle .proxmox-tag-light,
.proxmox-tags-circle .proxmox-tag-dark {
margin: 0px 1px;
position: relative;
top: 2px;
@ -42,17 +38,13 @@
overflow: hidden;
}
.proxmox-tags-none :not(span.proxmox-tags-full) > .proxmox-tag-light,
.proxmox-tags-none :not(span.proxmox-tags-full) > .proxmox-tag-dark,
.proxmox-tags-none > .proxmox-tag-light,
.proxmox-tags-none > .proxmox-tag-dark {
.proxmox-tags-none .proxmox-tag-light,
.proxmox-tags-none .proxmox-tag-dark {
display: none;
}
.proxmox-tags-dense :not(span.proxmox-tags-full) > .proxmox-tag-light,
.proxmox-tags-dense :not(span.proxmox-tags-full) > .proxmox-tag-dark,
.proxmox-tags-dense > .proxmox-tag-light,
.proxmox-tags-dense > .proxmox-tag-dark {
.proxmox-tags-dense .proxmox-tag-light,
.proxmox-tags-dense .proxmox-tag-dark {
width: 6px;
margin-right: 1px;
display: inline-block;
@ -74,9 +66,6 @@
.x-mask-msg-text {
text-align: center;
}
.x-window-header-default-top .x-mask {
background-color: #f5f5f5B4; /* ~ 0.7 opacity */
}
.proxmox-disabled-row, .proxmox-disabled-row td {
/*color: #a0a0a0;*/

View File

@ -1,29 +0,0 @@
Ext.define('proxmox-notification-endpoints', {
extend: 'Ext.data.Model',
fields: ['name', 'type', 'comment', 'disable', 'origin'],
proxy: {
type: 'proxmox',
},
idProperty: 'name',
});
Ext.define('proxmox-notification-matchers', {
extend: 'Ext.data.Model',
fields: ['name', 'comment', 'disable', 'origin'],
proxy: {
type: 'proxmox',
},
idProperty: 'name',
});
Ext.define('proxmox-notification-fields', {
extend: 'Ext.data.Model',
fields: ['name', 'description'],
idProperty: 'name',
});
Ext.define('proxmox-notification-field-values', {
extend: 'Ext.data.Model',
fields: ['value', 'comment', 'field'],
idProperty: 'value',
});

View File

@ -32,9 +32,6 @@ Ext.define('Proxmox.form.ComboGrid', {
notFoundIsValid: false,
deleteEmpty: false,
errorHeight: 100,
// NOTE: the trigger will always be shown if allowBlank is true, setting showClearTrigger
// to false cannot change that
showClearTrigger: false,
},
// needed to trigger onKeyUp etc.
@ -57,7 +54,7 @@ Ext.define('Proxmox.form.ComboGrid', {
setValue: function(value) {
let me = this;
let empty = Ext.isArray(value) ? !value.length : !value;
me.triggers.clear.setVisible(!empty && (me.allowBlank || me.showClearTrigger));
me.triggers.clear.setVisible(!empty && me.allowBlank);
return me.callParent([value]);
},
@ -403,7 +400,7 @@ Ext.define('Proxmox.form.ComboGrid', {
matchFieldWidth: false,
});
Ext.applyIf(me, { value: [] }); // hack: avoid ExtJS validate() bug
Ext.applyIf(me, { value: '' }); // hack: avoid ExtJS validate() bug
Ext.applyIf(me.listConfig, { width: 400 });

View File

@ -1,169 +1,151 @@
Ext.define('Proxmox.DateTimeField', {
extend: 'Ext.form.FieldContainer',
// FIXME: remove once all use sites upgraded (with versioned depends on new WTK!)
alias: ['widget.promxoxDateTimeField'],
xtype: 'proxmoxDateTimeField',
xtype: 'promxoxDateTimeField',
layout: 'hbox',
viewModel: {
data: {
datetime: null,
minDatetime: null,
maxDatetime: null,
},
referenceHolder: true,
formulas: {
date: {
get: function(get) {
return get('datetime');
},
set: function(date) {
if (!date) {
this.set('datetime', null);
return;
}
let datetime = new Date(this.get('datetime'));
datetime.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
this.set('datetime', datetime);
},
},
time: {
get: function(get) {
return get('datetime');
},
set: function(time) {
if (!time) {
this.set('datetime', null);
return;
}
let datetime = new Date(this.get('datetime'));
datetime.setHours(time.getHours());
datetime.setMinutes(time.getMinutes());
datetime.setSeconds(time.getSeconds());
datetime.setMilliseconds(time.getMilliseconds());
this.set('datetime', datetime);
},
},
minDate: {
get: function(get) {
let datetime = get('minDatetime');
return datetime ? new Date(datetime) : null;
},
},
maxDate: {
get: function(get) {
let datetime = get('maxDatetime');
return datetime ? new Date(datetime) : null;
},
},
minTime: {
get: function(get) {
let current = get('datetime');
let min = get('minDatetime');
if (min && current && !this.isSameDay(current, min)) {
return new Date(min).setHours('00', '00', '00', '000');
}
return min;
},
},
maxTime: {
get: function(get) {
let current = get('datetime');
let max = get('maxDatetime');
if (max && current && !this.isSameDay(current, max)) {
return new Date(max).setHours('23', '59', '59', '999');
}
return max;
},
},
},
// Helper function to check if dates are the same day of the year
isSameDay: function(date1, date2) {
return date1.getDate() === date2.getDate() &&
date1.getMonth() === date2.getMonth() &&
date1.getFullYear() === date2.getFullYear();
},
},
config: {
value: null,
submitFormat: 'U',
disabled: false,
},
setValue: function(value) {
this.getViewModel().set('datetime', value);
},
submitFormat: 'U',
getValue: function() {
return this.getViewModel().get('datetime');
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 value = me.getValue();
return value ? Ext.Date.format(value, me.submitFormat) : null;
},
let me = this;
let format = me.submitFormat;
let value = me.getValue();
setMinValue: function(value) {
this.getViewModel().set('minDatetime', value);
},
getMinValue: function() {
return this.getViewModel().get('minDatetime');
},
setMaxValue: function(value) {
this.getViewModel().set('maxDatetime', value);
},
getMaxValue: function() {
return this.getViewModel().get('maxDatetime');
},
initComponent: function() {
let me = this;
me.callParent();
let vm = me.getViewModel();
vm.set('datetime', me.config.value);
// Propagate state change to binding
vm.bind('{datetime}', function(value) {
me.publishState('value', value);
me.fireEvent('change', value);
});
return value ? Ext.Date.format(value, format) : null;
},
items: [
{
xtype: 'datefield',
editable: false,
reference: 'dateentry',
flex: 1,
format: 'Y-m-d',
bind: {
value: '{date}',
minValue: '{minDate}',
maxValue: '{maxDate}',
},
},
{
xtype: 'timefield',
reference: 'timeentry',
format: 'H:i',
width: 80,
value: '00:00',
increment: 60,
bind: {
value: '{time}',
minValue: '{minTime}',
maxValue: '{maxTime}',
},
},
],
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']);
},
});

View File

@ -24,7 +24,7 @@ Ext.define('Proxmox.form.field.DisplayEdit', {
getEditable: function() {
let me = this;
let vm = me.getViewModel();
return vm.get('editable');
vm.get('editable');
},
setValue: function(value) {
@ -37,19 +37,9 @@ Ext.define('Proxmox.form.field.DisplayEdit', {
getValue: function() {
let me = this;
let vm = me.getViewModel();
// FIXME: add return, but check all use-sites for regressions then
vm.get('value');
},
setEmptyText: function(emptyText) {
let me = this;
me.editField.setEmptyText(emptyText);
},
getEmptyText: function() {
let me = this;
return me.editField.getEmptyText();
},
layout: 'fit',
defaults: {
hideLabel: true,
@ -108,11 +98,6 @@ Ext.define('Proxmox.form.field.DisplayEdit', {
me.callParent();
// save a reference to make it easier when one needs to operate on the underlying fields,
// like when creating a passthrough getter/setter to allow easy data-binding.
me.editField = me.down(editConfig.xtype);
me.displayField = me.down(displayConfig.xtype);
me.getViewModel().set('editable', me.editable);
},

View File

@ -1,14 +0,0 @@
Ext.define('Proxmox.form.field.FingerprintField', {
extend: 'Proxmox.form.field.Textfield',
alias: ['widget.pmxFingerprintField'],
config: {
fieldLabel: gettext('Fingerprint'),
emptyText: gettext('Server certificate SHA-256 fingerprint, required for self-signed certificates'),
regex: /[A-Fa-f0-9]{2}(:[A-Fa-f0-9]{2}){31}/,
regexText: gettext('Example') + ': AB:CD:EF:...',
allowBlank: true,
},
});

View File

@ -39,8 +39,6 @@ Ext.define('Proxmox.form.MultiDiskSelector', {
setValue: function(value) {
let me = this;
value ??= [];
if (!Ext.isArray(value)) {
value = value.split(/;, /);
}

View File

@ -45,6 +45,10 @@ Ext.define('Proxmox.form.NetworkSelector', {
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: {
@ -119,7 +123,6 @@ Ext.define('Proxmox.form.NetworkSelector', {
header: gettext('Comment'),
flex: 2,
dataIndex: 'comments',
renderer: Ext.String.htmlEncode,
},
],
},

View File

@ -7,7 +7,6 @@ Ext.define('Proxmox.form.RealmComboBox', {
init: function(view) {
let store = view.getStore();
store.proxy.url = `/api2/json${view.baseUrl}`;
if (view.storeFilter) {
store.setFilters(view.storeFilter);
}
@ -46,7 +45,6 @@ Ext.define('Proxmox.form.RealmComboBox', {
triggerAction: 'all',
valueField: 'realm',
displayField: 'descr',
baseUrl: '/access/domains',
getState: function() {
return { value: this.getValue() };
},

View File

@ -1,61 +0,0 @@
Ext.define('Proxmox.form.field.Base64TextArea', {
extend: 'Ext.form.field.TextArea',
alias: ['widget.proxmoxBase64TextArea'],
config: {
skipEmptyText: false,
deleteEmpty: false,
trimValue: false,
editable: true,
width: 600,
height: 400,
scrollable: 'y',
emptyText: gettext('You can use Markdown for rich text formatting.'),
},
setValue: function(value) {
// We want to edit the decoded version of the text
this.callParent([Proxmox.Utils.base64ToUtf8(value)]);
},
processRawValue: function(value) {
// The field could contain multi-line values
return Proxmox.Utils.utf8ToBase64(value);
},
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 (me.getTrimValue() && typeof value === 'string') {
value = value.trim();
}
if (value !== '') {
return value;
}
return me.getSkipEmptyText() ? null: value;
},
setAllowBlank: function(allowBlank) {
this.allowBlank = allowBlank;
this.validate();
},
});

View File

@ -6,8 +6,6 @@ Ext.define('Proxmox.form.field.Textfield', {
skipEmptyText: true,
deleteEmpty: false,
trimValue: false,
},
getSubmitData: function() {
@ -31,9 +29,6 @@ Ext.define('Proxmox.form.field.Textfield', {
let me = this;
let value = this.processRawValue(this.getRawValue());
if (me.getTrimValue() && typeof value === 'string') {
value = value.trim();
}
if (value !== '') {
return value;
}

View File

@ -1,40 +0,0 @@
Ext.define('Proxmox.form.field.VlanField', {
extend: 'Ext.form.field.Number',
alias: ['widget.proxmoxvlanfield'],
deleteEmpty: false,
emptyText: gettext('no VLAN'),
fieldLabel: gettext('VLAN Tag'),
allowBlank: true,
getSubmitData: function() {
var me = this,
data = null,
val;
if (!me.disabled && me.submitValue) {
val = me.getSubmitValue();
if (val) {
data = {};
data[me.getName()] = val;
} else if (me.deleteEmpty) {
data = {};
data.delete = me.getName();
}
}
return data;
},
initComponent: function() {
var me = this;
Ext.apply(me, {
minValue: 1,
maxValue: 4094,
});
me.callParent();
},
});

View File

@ -67,6 +67,7 @@ Ext.define('Proxmox.grid.ObjectGrid', {
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
onlineHelp: opts.onlineHelp,
fieldDefaults: {
labelWidth: opts.labelWidth || 100,
},
@ -83,9 +84,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
},
},
};
if (opts.onlineHelp) {
me.rows[name].editor.onlineHelp = opts.onlineHelp;
}
},
add_text_row: function(name, text, opts) {
@ -102,6 +100,7 @@ Ext.define('Proxmox.grid.ObjectGrid', {
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
onlineHelp: opts.onlineHelp,
fieldDefaults: {
labelWidth: opts.labelWidth || 100,
},
@ -116,9 +115,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
},
},
};
if (opts.onlineHelp) {
me.rows[name].editor.onlineHelp = opts.onlineHelp;
}
},
add_boolean_row: function(name, text, opts) {
@ -135,6 +131,7 @@ Ext.define('Proxmox.grid.ObjectGrid', {
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
onlineHelp: opts.onlineHelp,
fieldDefaults: {
labelWidth: opts.labelWidth || 100,
},
@ -150,9 +147,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
},
},
};
if (opts.onlineHelp) {
me.rows[name].editor.onlineHelp = opts.onlineHelp;
}
},
add_integer_row: function(name, text, opts) {
@ -169,6 +163,7 @@ Ext.define('Proxmox.grid.ObjectGrid', {
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
onlineHelp: opts.onlineHelp,
fieldDefaults: {
labelWidth: opts.labelWidth || 100,
},
@ -185,40 +180,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
},
},
};
if (opts.onlineHelp) {
me.rows[name].editor.onlineHelp = opts.onlineHelp;
}
},
// adds a row that allows editing in a full TextArea that transparently de/encodes as Base64
add_textareafield_row: function(name, text, opts) {
let me = this;
opts = opts || {};
me.rows = me.rows || {};
let fieldOpts = opts.fieldOpts || {};
me.rows[name] = {
required: true,
defaultValue: "",
header: text,
renderer: value => Ext.htmlEncode(Proxmox.Utils.base64ToUtf8(value)),
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
fieldDefaults: {
labelWidth: opts.labelWidth || 600,
},
items: {
xtype: 'proxmoxBase64TextArea',
...fieldOpts,
name,
},
},
};
if (opts.onlineHelp) {
me.rows[name].editor.onlineHelp = opts.onlineHelp;
}
},
editorConfig: {}, // default config passed to editor

View File

@ -2,7 +2,7 @@ Ext.define('apt-pkglist', {
extend: 'Ext.data.Model',
fields: [
'Package', 'Title', 'Description', 'Section', 'Arch', 'Priority', 'Version', 'OldVersion',
'Origin',
'ChangeLogUrl', 'Origin',
],
idProperty: 'Package',
});
@ -108,8 +108,8 @@ Ext.define('Proxmox.node.APT', {
});
let show_changelog = function(rec) {
if (!rec?.data?.Package) {
console.debug('cannot show changelog, missing Package', rec);
if (!rec?.data?.ChangeLogUrl || !rec?.data?.Package) {
console.debug('cannot show changelog, missing Package and/or ChangeLogUrl', rec);
return;
}
@ -154,7 +154,7 @@ Ext.define('Proxmox.node.APT', {
text: gettext('Changelog'),
selModel: sm,
disabled: true,
enableFn: rec => !!rec?.data?.Package,
enableFn: rec => !!rec?.data?.ChangeLogUrl && !!rec?.data?.Package,
handler: (b, e, rec) => show_changelog(rec),
});

View File

@ -270,7 +270,7 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
let txt = [gettext('Warning')];
record.data.warnings.forEach((warning) => {
if (warning.property === 'Suites') {
txt.push(Ext.htmlEncode(warning.message));
txt.push(warning.message);
}
});
metaData.tdAttr = `data-qtip="${Ext.htmlEncode(txt.join('<br>'))}"`;
@ -305,7 +305,7 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
? gettext('The no-subscription repository is NOT production-ready')
: gettext('The test repository may contain unstable updates')
;
metaData.tdAttr = `data-qtip="${Ext.htmlEncode(Ext.htmlEncode(qtip))}"`;
metaData.tdAttr = `data-qtip="${Ext.htmlEncode(qtip)}"`;
}
}
return components.join(' ') + err;
@ -361,7 +361,6 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
header: gettext('Comment'),
dataIndex: 'Comment',
flex: 2,
renderer: Ext.String.htmlEncode,
},
],
@ -461,11 +460,6 @@ Ext.define('Proxmox.node.APTRepositories', {
let enterprise = vm.get('enterpriseRepo');
let nosubscription = vm.get('noSubscriptionRepo');
let test = vm.get('testRepo');
let cephRepos = {
enterprise: vm.get('cephEnterpriseRepo'),
nosubscription: vm.get('cephNoSubscriptionRepo'),
test: vm.get('cephTestRepo'),
};
let wrongSuites = vm.get('suitesWarning');
let mixedSuites = vm.get('mixedSuites');
@ -489,33 +483,17 @@ Ext.define('Proxmox.node.APTRepositories', {
addWarn(gettext('Detected mixed suites before upgrade'));
}
let productionReadyCheck = (repos, type, noSubAlternateName) => {
if (!activeSubscription && repos.enterprise) {
addWarn(Ext.String.format(
gettext('The {0}enterprise repository is enabled, but there is no active subscription!'),
type,
));
}
if (!activeSubscription && enterprise) {
addWarn(gettext('The enterprise repository is enabled, but there is no active subscription!'));
}
if (repos.nosubscription) {
addWarn(Ext.String.format(
gettext('The {0}no-subscription{1} repository is not recommended for production use!'),
type,
noSubAlternateName,
));
}
if (nosubscription) {
addWarn(gettext('The no-subscription repository is not recommended for production use!'));
}
if (repos.test) {
addWarn(Ext.String.format(
gettext('The {0}test repository may pull in unstable updates and is not recommended for production use!'),
type,
));
}
};
productionReadyCheck({ enterprise, nosubscription, test }, '', '');
// TODO drop alternate 'main' name when no longer relevant
productionReadyCheck(cephRepos, 'Ceph ', '/main');
if (test) {
addWarn(gettext('The test repository may pull in unstable updates and is not recommended for production use!'));
}
if (errors.length > 0) {
text = gettext('Fatal parsing error for at least one repository');
@ -540,9 +518,6 @@ Ext.define('Proxmox.node.APTRepositories', {
noSubscriptionRepo: '',
enterpriseRepo: '',
testRepo: '',
cephEnterpriseRepo: '',
cephNoSubscriptionRepo: '',
cephTestRepo: '',
selectionenabled: false,
state: {},
},
@ -611,7 +586,7 @@ Ext.define('Proxmox.node.APTRepositories', {
nodename: '{nodename}',
onlineHelp: '{onlineHelp}',
},
majorUpgradeAllowed: false, // TODO get release information from an API call?
majorUpgradeAllowed: true, // TODO get release information from an API call?
listeners: {
selectionchange: 'selectionChange',
},
@ -652,12 +627,6 @@ Ext.define('Proxmox.node.APTRepositories', {
vm.set('noSubscriptionRepo', status);
} else if (handle === 'test') {
vm.set('testRepo', status);
} else if (handle.match(/^ceph-[a-zA-Z]+-enterprise$/)) {
vm.set('cephEnterpriseRepo', status);
} else if (handle.match(/^ceph-[a-zA-Z]+-no-subscription$/)) {
vm.set('cephNoSubscriptionRepo', status);
} else if (handle.match(/^ceph-[a-zA-Z]+-test$/)) {
vm.set('cephTestRepo', status);
}
me.getController().updateState();

View File

@ -2,10 +2,6 @@ Ext.define('Proxmox.node.DNSEdit', {
extend: 'Proxmox.window.Edit',
alias: ['widget.proxmoxNodeDNSEdit'],
// Some longer existing APIs use a brittle "replace whole config" style, you can set this option
// if the DNSEdit component is used in an API that has more modern, granular update semantics.
deleteEmpty: false,
initComponent: function() {
let me = this;
@ -25,7 +21,6 @@ Ext.define('Proxmox.node.DNSEdit', {
fieldLabel: gettext('DNS server') + " 1",
vtype: 'IP64Address',
skipEmptyText: true,
deleteEmpty: me.deleteEmpty,
name: 'dns1',
},
{
@ -33,7 +28,6 @@ Ext.define('Proxmox.node.DNSEdit', {
fieldLabel: gettext('DNS server') + " 2",
vtype: 'IP64Address',
skipEmptyText: true,
deleteEmpty: me.deleteEmpty,
name: 'dns2',
},
{
@ -41,7 +35,6 @@ Ext.define('Proxmox.node.DNSEdit', {
fieldLabel: gettext('DNS server') + " 3",
vtype: 'IP64Address',
skipEmptyText: true,
deleteEmpty: me.deleteEmpty,
name: 'dns3',
},
];

View File

@ -2,10 +2,6 @@ Ext.define('Proxmox.node.DNSView', {
extend: 'Proxmox.grid.ObjectGrid',
alias: ['widget.proxmoxNodeDNSView'],
// Some longer existing APIs use a brittle "replace whole config" style, you can set this option
// if the DNSView component is used in an API that has more modern, granular update semantics.
deleteEmpty: false,
initComponent: function() {
let me = this;
@ -16,7 +12,6 @@ Ext.define('Proxmox.node.DNSView', {
let run_editor = () => Ext.create('Proxmox.node.DNSEdit', {
autoShow: true,
nodename: me.nodename,
deleteEmpty: me.deleteEmpty,
});
Ext.apply(me, {
@ -26,7 +21,7 @@ Ext.define('Proxmox.node.DNSView', {
run_editor: run_editor,
rows: {
search: {
header: gettext('Search domain'),
header: 'Search domain',
required: true,
renderer: Ext.htmlEncode,
},

View File

@ -2,9 +2,6 @@ Ext.define('Proxmox.node.NetworkEdit', {
extend: 'Proxmox.window.Edit',
alias: ['widget.proxmoxNodeNetworkEdit'],
// Enable to show the VLAN ID field
enableBridgeVlanIds: false,
initComponent: function() {
let me = this;
@ -60,53 +57,11 @@ Ext.define('Proxmox.node.NetworkEdit', {
}
if (me.iftype === 'bridge') {
let vlanIdsField = !me.enableBridgeVlanIds ? undefined : Ext.create('Ext.form.field.Text', {
fieldLabel: gettext('VLAN IDs'),
name: 'bridge_vids',
emptyText: '2-4094',
disabled: true,
autoEl: {
tag: 'div',
'data-qtip': gettext("List of VLAN IDs and ranges, useful for NICs with restricted VLAN offloading support. For example: '2 4 100-200'"),
},
validator: function(value) {
if (!value) { // empty
return true;
}
for (const vid of value.split(/\s+[,;]?/)) {
if (!vid) {
continue;
}
let res = vid.match(/^(\d+)(?:-(\d+))?$/);
if (!res) {
return Ext.String.format(gettext("not a valid bridge VLAN ID entry: {0}"), vid);
}
let start = Number(res[1]), end = Number(res[2] ?? res[1]); // end=start for single IDs
if (Number.isNaN(start) || Number.isNaN(end)) {
return Ext.String.format(gettext('VID range includes not-a-number: {0}'), vid);
} else if (start > end) {
return Ext.String.format(gettext('VID range must go from lower to higher tag: {0}'), vid);
} else if (start < 2 || end > 4094) { // check just one each, we already ensured start < end
return Ext.String.format(gettext('VID range outside of allowed 2 and 4094 limit: {0}'), vid);
}
}
return true;
},
});
column2.push({
xtype: 'proxmoxcheckbox',
fieldLabel: gettext('VLAN aware'),
name: 'bridge_vlan_aware',
deleteEmpty: !me.isCreate,
listeners: {
change: function(f, newVal) {
if (vlanIdsField) {
vlanIdsField.setDisabled(!newVal);
}
},
},
});
column2.push({
xtype: 'textfield',
@ -117,9 +72,6 @@ Ext.define('Proxmox.node.NetworkEdit', {
'data-qtip': gettext('Space-separated list of interfaces, for example: enp0s0 enp1s0'),
},
});
if (vlanIdsField) {
advancedColumn2.push(vlanIdsField);
}
} else if (me.iftype === 'OVSBridge') {
column2.push({
xtype: 'textfield',
@ -145,7 +97,7 @@ Ext.define('Proxmox.node.NetworkEdit', {
name: 'ovs_bridge',
});
column2.push({
xtype: 'proxmoxvlanfield',
xtype: 'pveVlanField',
deleteEmpty: !me.isCreate,
name: 'ovs_tag',
value: '',
@ -188,7 +140,7 @@ Ext.define('Proxmox.node.NetworkEdit', {
});
column2.push({
xtype: 'proxmoxvlanfield',
xtype: 'pveVlanField',
name: 'vlan-id',
value: me.vlanidvalue,
disabled: me.disablevlanid,
@ -259,7 +211,7 @@ Ext.define('Proxmox.node.NetworkEdit', {
name: 'ovs_bridge',
});
column2.push({
xtype: 'proxmoxvlanfield',
xtype: 'pveVlanField',
deleteEmpty: !me.isCreate,
name: 'ovs_tag',
value: '',
@ -302,7 +254,7 @@ Ext.define('Proxmox.node.NetworkEdit', {
value: me.iface,
vtype: iface_vtype,
allowBlank: false,
maxLength: iface_vtype === 'BridgeName' ? 10 : 15,
maxLength: 15,
autoEl: {
tag: 'div',
'data-qtip': gettext('For example, vmbr0.100, vmbr0, vlan0.100, vlan0'),

View File

@ -33,9 +33,6 @@ Ext.define('Proxmox.node.NetworkView', {
showApplyBtn: false,
// for options passed down to the network edit window
editOptions: {},
initComponent: function() {
let me = this;
@ -103,7 +100,6 @@ Ext.define('Proxmox.node.NetworkView', {
nodename: me.nodename,
iface: rec.data.iface,
iftype: rec.data.type,
...me.editOptions,
listeners: {
destroy: () => reload(),
},
@ -174,7 +170,6 @@ Ext.define('Proxmox.node.NetworkView', {
nodename: me.nodename,
iftype: iType,
iface_default: findNextFreeInterfaceId(iDefault ?? iType),
...me.editOptions,
onlineHelp: 'sysadmin_network_configuration',
listeners: {
destroy: () => reload(),

View File

@ -29,8 +29,6 @@ Ext.define('Proxmox.node.ServiceView', {
},
});
let filterInstalledOnly = record => record.get('unit-state') !== 'not-found';
let store = Ext.create('Proxmox.data.DiffStore', {
rstore: rstore,
sortAfterUpdate: true,
@ -40,24 +38,6 @@ Ext.define('Proxmox.node.ServiceView', {
direction: 'ASC',
},
],
filters: [
filterInstalledOnly,
],
});
let unHideCB = Ext.create('Ext.form.field.Checkbox', {
boxLabel: gettext('Show only installed services'),
value: true,
boxLabelAlign: 'before',
listeners: {
change: function(_cb, value) {
if (value) {
store.addFilter([filterInstalledOnly]);
} else {
store.clearFilter();
}
},
},
});
let view_service_log = function() {
@ -186,8 +166,6 @@ Ext.define('Proxmox.node.ServiceView', {
restart_btn,
'-',
syslog_btn,
'->',
unHideCB,
],
columns: [
{

View File

@ -1,9 +1,7 @@
Ext.define('Proxmox.panel.AuthView', {
extend: 'Ext.grid.GridPanel',
alias: 'widget.pmxAuthView',
mixins: ['Proxmox.Mixin.CBind'],
showDefaultRealm: false,
alias: 'widget.pmxAuthView',
stateful: true,
stateId: 'grid-authrealms',
@ -13,7 +11,7 @@ Ext.define('Proxmox.panel.AuthView', {
},
baseUrl: '/access/domains',
storeBaseUrl: '/access/domains',
useTypeInUrl: false,
columns: [
{
@ -28,17 +26,6 @@ Ext.define('Proxmox.panel.AuthView', {
sortable: true,
dataIndex: 'type',
},
{
header: gettext('Default'),
width: 80,
sortable: true,
dataIndex: 'default',
renderer: isDefault => isDefault ? Proxmox.Utils.renderEnabledIcon(true) : '',
align: 'center',
cbind: {
hidden: '{!showDefaultRealm}',
},
},
{
header: gettext('Comment'),
sortable: false,
@ -48,17 +35,21 @@ Ext.define('Proxmox.panel.AuthView', {
},
],
store: {
model: 'pmx-domains',
sorters: {
property: 'realm',
direction: 'ASC',
},
},
openEditWindow: function(authType, realm) {
let me = this;
const { useTypeInUrl, onlineHelp } = Proxmox.Schema.authDomains[authType];
Ext.create('Proxmox.window.AuthEditBase', {
baseUrl: me.baseUrl,
useTypeInUrl,
onlineHelp,
useTypeInUrl: me.useTypeInUrl,
authType,
realm,
showDefaultRealm: me.showDefaultRealm,
listeners: {
destroy: () => me.reload(),
},
@ -104,18 +95,6 @@ Ext.define('Proxmox.panel.AuthView', {
initComponent: function() {
var me = this;
me.store = {
model: 'pmx-domains',
sorters: {
property: 'realm',
direction: 'ASC',
},
proxy: {
type: 'proxmox',
url: `/api2/json${me.storeBaseUrl}`,
},
};
let menuitems = [];
for (const [authType, config] of Object.entries(Proxmox.Schema.authDomains).sort()) {
if (!config.add) { continue; }
@ -144,7 +123,7 @@ Ext.define('Proxmox.panel.AuthView', {
xtype: 'proxmoxStdRemoveButton',
getUrl: (rec) => {
let url = me.baseUrl;
if (Proxmox.Schema.authDomains[rec.data.type].useTypeInUrl) {
if (me.useTypeInUrl) {
url += `/${rec.get('type')}`;
}
url += `/${rec.getId()}`;

View File

@ -85,7 +85,7 @@ Ext.define('Proxmox.panel.Certificates', {
url: `/api2/extjs/${url}?restart=1`,
method: 'DELETE',
success: function(response, opt) {
if (cert.reloadUi) {
if (cert.reloadUid) {
Ext.getBody().mask(
gettext('API server will be restarted to use new certificates, please reload web-interface!'),
['pve-static-mask'],
@ -237,16 +237,10 @@ Ext.define('Proxmox.panel.Certificates', {
{
xtype: 'proxmoxButton',
text: gettext('Delete Custom Certificate'),
confirmMsg: rec => {
let cert = me.certById[rec.id];
if (cert.name) {
return Ext.String.format(
gettext('Are you sure you want to remove the certificate used for {0}'),
cert.name,
);
}
return gettext('Are you sure you want to remove the certificate');
},
confirmMsg: rec => Ext.String.format(
gettext('Are you sure you want to remove the certificate used for {0}'),
me.certById[rec.id].name,
),
callback: () => me.reload(),
selModel: me.selModel,
disabled: true,

View File

@ -220,11 +220,7 @@ Ext.define('Proxmox.DiskList', {
let extendedInfo = '';
if (rec) {
let types = [];
if (rec.data['osdid-list'] && rec.data['osdid-list'].length > 0) {
for (const id of rec.data['osdid-list'].sort()) {
types.push(`OSD.${id.toString()}`);
}
} else if (rec.data.osdid !== undefined && rec.data.osdid >= 0) {
if (rec.data.osdid !== undefined && rec.data.osdid >= 0) {
types.push(`OSD.${rec.data.osdid.toString()}`);
}
if (rec.data.journals > 0) {
@ -325,14 +321,14 @@ Ext.define('Proxmox.DiskList', {
dataIndex: 'status',
},
{
header: gettext('Mounted'),
header: 'Mounted',
width: 60,
align: 'right',
renderer: Proxmox.Utils.format_boolean,
dataIndex: 'mounted',
},
{
header: gettext('Wearout'),
header: 'Wearout',
width: 90,
sortable: true,
align: 'right',
@ -341,7 +337,7 @@ Ext.define('Proxmox.DiskList', {
if (Ext.isNumeric(value)) {
return (100 - value).toString() + '%';
}
return gettext('N/A');
return 'N/A';
},
},
],

View File

@ -1,91 +0,0 @@
Ext.define('Proxmox.panel.EmailRecipientPanel', {
extend: 'Ext.panel.Panel',
xtype: 'pmxEmailRecipientPanel',
mixins: ['Proxmox.Mixin.CBind'],
border: false,
mailValidator: function() {
let mailto_user = this.down(`[name=mailto-user]`);
let mailto = this.down(`[name=mailto]`);
if (!mailto_user.getValue()?.length && !mailto.getValue()) {
return gettext('Either mailto or mailto-user must be set');
}
return true;
},
items: [
{
layout: 'anchor',
border: false,
cbind: {
isCreate: '{isCreate}',
},
items: [
{
xtype: 'pmxUserSelector',
name: 'mailto-user',
multiSelect: true,
allowBlank: true,
editable: false,
skipEmptyText: true,
fieldLabel: gettext('Recipient(s)'),
cbind: {
deleteEmpty: '{!isCreate}',
},
validator: function() {
return this.up('pmxEmailRecipientPanel').mailValidator();
},
autoEl: {
tag: 'div',
'data-qtip': gettext('The notification will be sent to the user\'s configured mail address'),
},
listConfig: {
width: 600,
columns: [
{
header: gettext('User'),
sortable: true,
dataIndex: 'userid',
renderer: Ext.String.htmlEncode,
flex: 1,
},
{
header: gettext('E-Mail'),
sortable: true,
dataIndex: 'email',
renderer: Ext.String.htmlEncode,
flex: 1,
},
{
header: gettext('Comment'),
sortable: false,
dataIndex: 'comment',
renderer: Ext.String.htmlEncode,
flex: 1,
},
],
},
},
{
xtype: 'proxmoxtextfield',
fieldLabel: gettext('Additional Recipient(s)'),
name: 'mailto',
allowBlank: true,
emptyText: 'user@example.com, ...',
cbind: {
deleteEmpty: '{!isCreate}',
},
autoEl: {
tag: 'div',
'data-qtip': gettext('Multiple recipients must be separated by spaces, commas or semicolons'),
},
validator: function() {
return this.up('pmxEmailRecipientPanel').mailValidator();
},
},
],
},
],
});

View File

@ -1,75 +0,0 @@
Ext.define('Proxmox.panel.GotifyEditPanel', {
extend: 'Proxmox.panel.InputPanel',
xtype: 'pmxGotifyEditPanel',
mixins: ['Proxmox.Mixin.CBind'],
onlineHelp: 'notification_targets_gotify',
type: 'gotify',
items: [
{
xtype: 'pmxDisplayEditField',
name: 'name',
cbind: {
value: '{name}',
editable: '{isCreate}',
},
fieldLabel: gettext('Endpoint Name'),
allowBlank: false,
},
{
xtype: 'proxmoxcheckbox',
name: 'enable',
fieldLabel: gettext('Enable'),
allowBlank: false,
checked: true,
},
{
xtype: 'proxmoxtextfield',
fieldLabel: gettext('Server URL'),
name: 'server',
allowBlank: false,
},
{
xtype: 'proxmoxtextfield',
inputType: 'password',
fieldLabel: gettext('API Token'),
name: 'token',
cbind: {
emptyText: get => !get('isCreate') ? gettext('Unchanged') : '',
allowBlank: '{!isCreate}',
},
},
{
xtype: 'proxmoxtextfield',
name: 'comment',
fieldLabel: gettext('Comment'),
cbind: {
deleteEmpty: '{!isCreate}',
},
},
],
onSetValues: (values) => {
values.enable = !values.disable;
delete values.disable;
return values;
},
onGetValues: function(values) {
let me = this;
if (values.enable) {
if (!me.isCreate) {
Proxmox.Utils.assemble_field_data(values, { 'delete': 'disable' });
}
} else {
values.disable = 1;
}
delete values.enable;
return values;
},
});

View File

@ -22,28 +22,19 @@ Ext.define('Proxmox.panel.LogView', {
updateParams: function() {
let me = this;
let viewModel = me.getViewModel();
if (viewModel.get('hide_timespan') || viewModel.get('livemode')) {
return;
}
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;
}
let submitFormat = viewModel.get('submitFormat');
viewModel.set('params.since', Ext.Date.format(since, submitFormat));
if (submitFormat === 'Y-m-d') {
viewModel.set('params.until', Ext.Date.format(until, submitFormat) + ' 23:59:59');
} else {
viewModel.set('params.until', Ext.Date.format(until, submitFormat));
}
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);
},
@ -184,27 +175,6 @@ Ext.define('Proxmox.panel.LogView', {
}
},
onLiveMode: function() {
let me = this;
let viewModel = me.getViewModel();
viewModel.set('livemode', true);
viewModel.set('params', { start: 0, limit: 510 });
let view = me.getView();
delete view.content;
view.scrollToEnd = true;
me.updateView([], true, false);
},
onTimespan: function() {
let me = this;
me.getViewModel().set('livemode', false);
me.updateView([], false);
// Directly apply currently selected values without update
// button click.
me.updateParams();
},
init: function(view) {
let me = this;
@ -219,7 +189,6 @@ Ext.define('Proxmox.panel.LogView', {
viewModel.set('since', since);
viewModel.set('params.limit', view.pageSize);
viewModel.set('hide_timespan', !view.log_select_timespan);
viewModel.set('submitFormat', view.submitFormat);
me.lookup('content').setStyle('line-height', `${view.lineHeight}px`);
view.loadTask = new Ext.util.DelayedTask(me.doLoad, me);
@ -255,8 +224,6 @@ Ext.define('Proxmox.panel.LogView', {
data: {
until: null,
since: null,
submitFormat: 'Y-m-d',
livemode: true,
hide_timespan: false,
data: {
start: 0,
@ -296,70 +263,32 @@ Ext.define('Proxmox.panel.LogView', {
},
items: [
'->',
'Since: ',
{
xtype: 'segmentedbutton',
items: [
{
text: gettext('Live Mode'),
bind: {
pressed: '{livemode}',
},
handler: 'onLiveMode',
},
{
text: gettext('Select Timespan'),
bind: {
pressed: '{!livemode}',
},
handler: 'onTimespan',
},
],
},
{
xtype: 'box',
autoEl: { cn: gettext('Since') + ':' },
bind: {
disabled: '{livemode}',
},
},
{
xtype: 'proxmoxDateTimeField',
xtype: 'datefield',
name: 'since_date',
reference: 'since',
format: 'Y-m-d',
bind: {
disabled: '{livemode}',
value: '{since}',
maxValue: '{until}',
submitFormat: '{submitFormat}',
},
},
'Until: ',
{
xtype: 'box',
autoEl: { cn: gettext('Until') + ':' },
bind: {
disabled: '{livemode}',
},
},
{
xtype: 'proxmoxDateTimeField',
xtype: 'datefield',
name: 'until_date',
reference: 'until',
format: 'Y-m-d',
bind: {
disabled: '{livemode}',
value: '{until}',
minValue: '{since}',
submitFormat: '{submitFormat}',
},
},
{
xtype: 'button',
text: 'Update',
handler: 'updateParams',
bind: {
disabled: '{livemode}',
},
},
],
},

View File

@ -7,8 +7,6 @@ Ext.define('Proxmox.panel.NotesView', {
bodyPadding: 10,
scrollable: true,
animCollapse: false,
collapseFirst: false,
maxLength: 64 * 1024,
enableTBar: false,
onlineHelp: 'markdown_basics',
@ -19,7 +17,6 @@ Ext.define('Proxmox.panel.NotesView', {
items: [
{
text: gettext('Edit'),
iconCls: 'fa fa-pencil-square-o',
handler: function() {
let view = this.up('panel');
view.run_editor();
@ -112,23 +109,7 @@ Ext.define('Proxmox.panel.NotesView', {
listeners: {
render: function(c) {
let me = this;
let sp = Ext.state.Manager.getProvider();
// to cover live changes to the browser setting
me.mon(sp, 'statechange', function(provider, key, value) {
if (value === null || key !== 'edit-notes-on-double-click') {
return;
}
if (value) {
me.getEl().on('dblclick', me.run_editor, me);
} else {
// there's only the me.run_editor listener, and removing just that did not work
me.getEl().clearListeners();
}
});
// to cover initial setting value
if (sp.get('edit-notes-on-double-click', false)) {
me.getEl().on('dblclick', me.run_editor, me);
}
me.getEl().on('dblclick', me.run_editor, me);
},
afterlayout: function() {
let me = this;
@ -141,11 +122,10 @@ Ext.define('Proxmox.panel.NotesView', {
tools: [
{
glyph: 'xf044@FontAwesome', // fa-pencil-square-o
tooltip: gettext('Edit notes'),
callback: view => view.run_editor(),
style: {
paddingRight: '5px',
type: 'gear',
handler: function() {
let view = this.up('panel');
view.run_editor();
},
},
],

View File

@ -1,410 +0,0 @@
Ext.define('Proxmox.panel.NotificationConfigViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.pmxNotificationConfigPanel',
formulas: {
builtinSelected: function(get) {
let origin = get('selection')?.get('origin');
return origin === 'modified-builtin' || origin === 'builtin';
},
removeButtonText: get => get('builtinSelected') ? gettext('Reset') : gettext('Remove'),
removeButtonConfirmMessage: function(get) {
if (get('builtinSelected')) {
return gettext('Do you want to reset {0} to its default settings?');
} else {
// Use default message provided by the button
return undefined;
}
},
},
});
Ext.define('Proxmox.panel.NotificationConfigView', {
extend: 'Ext.panel.Panel',
alias: 'widget.pmxNotificationConfigView',
mixins: ['Proxmox.Mixin.CBind'],
onlineHelp: 'chapter_notifications',
layout: {
type: 'border',
},
items: [
{
region: 'center',
border: false,
xtype: 'pmxNotificationEndpointView',
cbind: {
baseUrl: '{baseUrl}',
},
},
{
region: 'south',
height: '50%',
border: false,
collapsible: true,
animCollapse: false,
xtype: 'pmxNotificationMatcherView',
cbind: {
baseUrl: '{baseUrl}',
},
},
],
});
Ext.define('Proxmox.panel.NotificationEndpointView', {
extend: 'Ext.grid.Panel',
alias: 'widget.pmxNotificationEndpointView',
title: gettext('Notification Targets'),
viewModel: {
type: 'pmxNotificationConfigPanel',
},
bind: {
selection: '{selection}',
},
controller: {
xclass: 'Ext.app.ViewController',
openEditWindow: function(endpointType, endpoint) {
let me = this;
Ext.create('Proxmox.window.EndpointEditBase', {
baseUrl: me.getView().baseUrl,
type: endpointType,
name: endpoint,
autoShow: true,
listeners: {
destroy: () => me.reload(),
},
});
},
openEditForSelectedItem: function() {
let me = this;
let view = me.getView();
let selection = view.getSelection();
if (selection.length < 1) {
return;
}
me.openEditWindow(selection[0].data.type, selection[0].data.name);
},
reload: function() {
let me = this;
let view = me.getView();
view.getStore().rstore.load();
this.getView().setSelection(null);
},
testEndpoint: function() {
let me = this;
let view = me.getView();
let selection = view.getSelection();
if (selection.length < 1) {
return;
}
let target = selection[0].data.name;
Ext.Msg.confirm(
gettext("Notification Target Test"),
Ext.String.format(gettext("Do you want to send a test notification to '{0}'?"), target),
function(decision) {
if (decision !== "yes") {
return;
}
Proxmox.Utils.API2Request({
method: 'POST',
url: `${view.baseUrl}/targets/${target}/test`,
success: function(response, opt) {
Ext.Msg.show({
title: gettext('Notification Target Test'),
message: Ext.String.format(
gettext("Sent test notification to '{0}'."),
target,
),
buttons: Ext.Msg.OK,
icon: Ext.Msg.INFO,
});
},
autoErrorAlert: true,
});
});
},
},
listeners: {
itemdblclick: 'openEditForSelectedItem',
activate: 'reload',
},
emptyText: gettext('No notification targets configured'),
columns: [
{
dataIndex: 'disable',
text: gettext('Enable'),
renderer: (disable) => Proxmox.Utils.renderEnabledIcon(!disable),
align: 'center',
},
{
dataIndex: 'name',
text: gettext('Target Name'),
renderer: Ext.String.htmlEncode,
flex: 2,
},
{
dataIndex: 'type',
text: gettext('Type'),
renderer: Ext.String.htmlEncode,
flex: 1,
},
{
dataIndex: 'comment',
text: gettext('Comment'),
renderer: Ext.String.htmlEncode,
flex: 3,
},
{
dataIndex: 'origin',
text: gettext('Origin'),
renderer: (origin) => {
switch (origin) {
case 'user-created': return gettext('Custom');
case 'modified-builtin': return gettext('Built-In (modified)');
case 'builtin': return gettext('Built-In');
}
// Should not happen...
return 'unknown';
},
},
],
store: {
type: 'diff',
autoDestroy: true,
autoDestroyRstore: true,
rstore: {
type: 'update',
storeid: 'proxmox-notification-endpoints',
model: 'proxmox-notification-endpoints',
autoStart: true,
},
sorters: 'name',
},
initComponent: function() {
let me = this;
if (!me.baseUrl) {
throw "baseUrl is not set!";
}
let menuItems = [];
for (const [endpointType, config] of Object.entries(
Proxmox.Schema.notificationEndpointTypes).sort()) {
menuItems.push({
text: config.name,
iconCls: 'fa fa-fw ' + (config.iconCls || 'fa-bell-o'),
handler: () => me.controller.openEditWindow(endpointType),
});
}
Ext.apply(me, {
tbar: [
{
text: gettext('Add'),
menu: menuItems,
},
{
xtype: 'proxmoxButton',
text: gettext('Modify'),
handler: 'openEditForSelectedItem',
disabled: true,
},
{
xtype: 'proxmoxStdRemoveButton',
callback: 'reload',
bind: {
text: '{removeButtonText}',
customConfirmationMessage: '{removeButtonConfirmMessage}',
},
getUrl: function(rec) {
return `${me.baseUrl}/endpoints/${rec.data.type}/${rec.getId()}`;
},
enableFn: (rec) => {
let origin = rec.get('origin');
return origin === 'user-created' || origin === 'modified-builtin';
},
},
'-',
{
xtype: 'proxmoxButton',
text: gettext('Test'),
handler: 'testEndpoint',
disabled: true,
},
],
});
me.callParent();
me.store.rstore.proxy.setUrl(`/api2/json/${me.baseUrl}/targets`);
},
});
Ext.define('Proxmox.panel.NotificationMatcherView', {
extend: 'Ext.grid.Panel',
alias: 'widget.pmxNotificationMatcherView',
title: gettext('Notification Matchers'),
controller: {
xclass: 'Ext.app.ViewController',
openEditWindow: function(matcher) {
let me = this;
Ext.create('Proxmox.window.NotificationMatcherEdit', {
baseUrl: me.getView().baseUrl,
name: matcher,
autoShow: true,
listeners: {
destroy: () => me.reload(),
},
});
},
openEditForSelectedItem: function() {
let me = this;
let view = me.getView();
let selection = view.getSelection();
if (selection.length < 1) {
return;
}
me.openEditWindow(selection[0].data.name);
},
reload: function() {
this.getView().getStore().rstore.load();
this.getView().setSelection(null);
},
},
viewModel: {
type: 'pmxNotificationConfigPanel',
},
bind: {
selection: '{selection}',
},
listeners: {
itemdblclick: 'openEditForSelectedItem',
activate: 'reload',
},
emptyText: gettext('No notification matchers configured'),
columns: [
{
dataIndex: 'disable',
text: gettext('Enable'),
renderer: (disable) => Proxmox.Utils.renderEnabledIcon(!disable),
align: 'center',
},
{
dataIndex: 'name',
text: gettext('Matcher Name'),
renderer: Ext.String.htmlEncode,
flex: 1,
},
{
dataIndex: 'comment',
text: gettext('Comment'),
renderer: Ext.String.htmlEncode,
flex: 2,
},
{
dataIndex: 'origin',
text: gettext('Origin'),
renderer: (origin) => {
switch (origin) {
case 'user-created': return gettext('Custom');
case 'modified-builtin': return gettext('Built-In (modified)');
case 'builtin': return gettext('Built-In');
}
// Should not happen...
return 'unknown';
},
},
],
store: {
type: 'diff',
autoDestroy: true,
autoDestroyRstore: true,
rstore: {
type: 'update',
storeid: 'proxmox-notification-matchers',
model: 'proxmox-notification-matchers',
autoStart: true,
},
sorters: 'name',
},
initComponent: function() {
let me = this;
if (!me.baseUrl) {
throw "baseUrl is not set!";
}
Ext.apply(me, {
tbar: [
{
xtype: 'proxmoxButton',
text: gettext('Add'),
handler: () => me.getController().openEditWindow(),
selModel: false,
},
{
xtype: 'proxmoxButton',
text: gettext('Modify'),
handler: 'openEditForSelectedItem',
disabled: true,
},
{
xtype: 'proxmoxStdRemoveButton',
callback: 'reload',
bind: {
text: '{removeButtonText}',
customConfirmationMessage: '{removeButtonConfirmMessage}',
},
baseurl: `${me.baseUrl}/matchers`,
enableFn: (rec) => {
let origin = rec.get('origin');
return origin === 'user-created' || origin === 'modified-builtin';
},
},
],
});
me.callParent();
me.store.rstore.proxy.setUrl(`/api2/json/${me.baseUrl}/matchers`);
},
});

View File

@ -1,103 +0,0 @@
Ext.define('Proxmox.panel.SendmailEditPanel', {
extend: 'Proxmox.panel.InputPanel',
xtype: 'pmxSendmailEditPanel',
mixins: ['Proxmox.Mixin.CBind'],
type: 'sendmail',
onlineHelp: 'notification_targets_sendmail',
mailValidator: function() {
let mailto_user = this.down(`[name=mailto-user]`);
let mailto = this.down(`[name=mailto]`);
if (!mailto_user.getValue()?.length && !mailto.getValue()) {
return gettext('Either mailto or mailto-user must be set');
}
return true;
},
items: [
{
xtype: 'pmxDisplayEditField',
name: 'name',
cbind: {
value: '{name}',
editable: '{isCreate}',
},
fieldLabel: gettext('Endpoint Name'),
allowBlank: false,
},
{
xtype: 'proxmoxcheckbox',
name: 'enable',
fieldLabel: gettext('Enable'),
allowBlank: false,
checked: true,
},
{
// provides 'mailto' and 'mailto-user' fields
xtype: 'pmxEmailRecipientPanel',
cbind: {
isCreate: '{isCreate}',
},
},
{
xtype: 'proxmoxtextfield',
name: 'comment',
fieldLabel: gettext('Comment'),
cbind: {
deleteEmpty: '{!isCreate}',
},
},
],
advancedItems: [
{
xtype: 'proxmoxtextfield',
fieldLabel: gettext('Author'),
name: 'author',
allowBlank: true,
cbind: {
emptyText: '{defaultMailAuthor}',
deleteEmpty: '{!isCreate}',
},
},
{
xtype: 'proxmoxtextfield',
fieldLabel: gettext('From Address'),
name: 'from-address',
allowBlank: true,
emptyText: gettext('Defaults to datacenter configuration, or root@$hostname'),
cbind: {
deleteEmpty: '{!isCreate}',
},
},
],
onSetValues: (values) => {
values.enable = !values.disable;
delete values.disable;
return values;
},
onGetValues: function(values) {
let me = this;
if (values.enable) {
if (!me.isCreate) {
Proxmox.Utils.assemble_field_data(values, { 'delete': 'disable' });
}
} else {
values.disable = 1;
}
delete values.enable;
if (values.mailto) {
values.mailto = values.mailto.split(/[\s,;]+/);
}
return values;
},
});

View File

@ -1,205 +0,0 @@
Ext.define('Proxmox.panel.SmtpEditPanel', {
extend: 'Proxmox.panel.InputPanel',
xtype: 'pmxSmtpEditPanel',
mixins: ['Proxmox.Mixin.CBind'],
onlineHelp: 'notification_targets_smtp',
type: 'smtp',
viewModel: {
xtype: 'viewmodel',
cbind: {
isCreate: "{isCreate}",
},
data: {
mode: 'tls',
authentication: true,
},
formulas: {
portEmptyText: function(get) {
let port;
switch (get('mode')) {
case 'insecure':
port = 25;
break;
case 'starttls':
port = 587;
break;
case 'tls':
port = 465;
break;
}
return `${Proxmox.Utils.defaultText} (${port})`;
},
passwordEmptyText: function(get) {
let isCreate = this.isCreate;
return get('authentication') && !isCreate ? gettext('Unchanged') : '';
},
},
},
columnT: [
{
xtype: 'pmxDisplayEditField',
name: 'name',
cbind: {
value: '{name}',
editable: '{isCreate}',
},
fieldLabel: gettext('Endpoint Name'),
allowBlank: false,
},
{
xtype: 'proxmoxcheckbox',
name: 'enable',
fieldLabel: gettext('Enable'),
allowBlank: false,
checked: true,
},
],
column1: [
{
xtype: 'proxmoxtextfield',
fieldLabel: gettext('Server'),
name: 'server',
allowBlank: false,
emptyText: gettext('mail.example.com'),
},
{
xtype: 'proxmoxKVComboBox',
name: 'mode',
fieldLabel: gettext('Encryption'),
editable: false,
comboItems: [
['insecure', Proxmox.Utils.noneText + ' (' + gettext('insecure') + ')'],
['starttls', 'STARTTLS'],
['tls', 'TLS'],
],
bind: "{mode}",
cbind: {
deleteEmpty: '{!isCreate}',
},
},
{
xtype: 'proxmoxintegerfield',
name: 'port',
fieldLabel: gettext('Port'),
minValue: 1,
maxValue: 65535,
bind: {
emptyText: "{portEmptyText}",
},
submitEmptyText: false,
cbind: {
deleteEmpty: '{!isCreate}',
},
},
],
column2: [
{
xtype: 'proxmoxcheckbox',
fieldLabel: gettext('Authenticate'),
name: 'authentication',
bind: {
value: '{authentication}',
},
},
{
xtype: 'proxmoxtextfield',
fieldLabel: gettext('Username'),
name: 'username',
allowBlank: false,
cbind: {
deleteEmpty: '{!isCreate}',
},
bind: {
disabled: '{!authentication}',
},
},
{
xtype: 'proxmoxtextfield',
inputType: 'password',
fieldLabel: gettext('Password'),
name: 'password',
allowBlank: true,
bind: {
disabled: '{!authentication}',
emptyText: '{passwordEmptyText}',
},
},
],
columnB: [
{
xtype: 'proxmoxtextfield',
fieldLabel: gettext('From Address'),
name: 'from-address',
allowBlank: false,
emptyText: gettext('user@example.com'),
},
{
// provides 'mailto' and 'mailto-user' fields
xtype: 'pmxEmailRecipientPanel',
cbind: {
isCreate: '{isCreate}',
},
},
{
xtype: 'proxmoxtextfield',
name: 'comment',
fieldLabel: gettext('Comment'),
cbind: {
deleteEmpty: '{!isCreate}',
},
},
],
advancedColumnB: [
{
xtype: 'proxmoxtextfield',
fieldLabel: gettext('Author'),
name: 'author',
allowBlank: true,
cbind: {
emptyText: '{defaultMailAuthor}',
deleteEmpty: '{!isCreate}',
},
},
],
onGetValues: function(values) {
let me = this;
if (values.mailto) {
values.mailto = values.mailto.split(/[\s,;]+/);
}
if (!values.authentication && !me.isCreate) {
Proxmox.Utils.assemble_field_data(values, { 'delete': 'username' });
Proxmox.Utils.assemble_field_data(values, { 'delete': 'password' });
}
if (values.enable) {
if (!me.isCreate) {
Proxmox.Utils.assemble_field_data(values, { 'delete': 'disable' });
}
} else {
values.disable = 1;
}
delete values.enable;
delete values.authentication;
return values;
},
onSetValues: function(values) {
values.authentication = !!values.username;
values.enable = !values.disable;
delete values.disable;
return values;
},
});

View File

@ -67,11 +67,8 @@ Ext.define('Proxmox.panel.TfaView', {
onLoad: function(store, data, success) {
if (!success) return;
let now = new Date().getTime() / 1000;
let records = [];
Ext.Array.each(data, user => {
let tfa_locked = (user.data['tfa-locked-until'] || 0) > now;
let totp_locked = user.data['totp-locked'];
Ext.Array.each(user.data.entries, entry => {
records.push({
fullid: `${user.id}/${entry.id}`,
@ -80,7 +77,6 @@ Ext.define('Proxmox.panel.TfaView', {
description: entry.description,
created: entry.created,
enable: entry.enable,
locked: tfa_locked || (entry.type === 'totp' && totp_locked),
});
});
});
@ -158,10 +154,8 @@ Ext.define('Proxmox.panel.TfaView', {
renderUser: fullid => fullid.split('/')[0],
renderEnabled: function(enabled, metaData, record) {
if (record.data.locked) {
return gettext("Locked");
} else if (enabled === undefined) {
renderEnabled: enabled => {
if (enabled === undefined) {
return Proxmox.Utils.yesText;
} else {
return Proxmox.Utils.format_boolean(enabled);

View File

@ -1,423 +0,0 @@
Ext.define('Proxmox.panel.WebhookEditPanel', {
extend: 'Proxmox.panel.InputPanel',
xtype: 'pmxWebhookEditPanel',
mixins: ['Proxmox.Mixin.CBind'],
onlineHelp: 'notification_targets_webhook',
type: 'webhook',
columnT: [
],
column1: [
{
xtype: 'pmxDisplayEditField',
name: 'name',
cbind: {
value: '{name}',
editable: '{isCreate}',
},
fieldLabel: gettext('Endpoint Name'),
regex: Proxmox.Utils.safeIdRegex,
allowBlank: false,
},
],
column2: [
{
xtype: 'proxmoxcheckbox',
name: 'enable',
fieldLabel: gettext('Enable'),
allowBlank: false,
checked: true,
},
],
columnB: [
{
xtype: 'fieldcontainer',
fieldLabel: gettext('Method/URL'),
layout: 'hbox',
border: false,
margin: '0 0 5 0',
items: [
{
xtype: 'proxmoxKVComboBox',
name: 'method',
editable: false,
value: 'post',
comboItems: [
['post', 'POST'],
['put', 'PUT'],
['get', 'GET'],
],
width: 80,
margin: '0 5 0 0',
},
{
xtype: 'proxmoxtextfield',
name: 'url',
allowBlank: false,
emptyText: "https://example.com/hook",
regex: Proxmox.Utils.httpUrlRegex,
regexText: gettext('Must be a valid URL'),
flex: 4,
},
],
},
{
xtype: 'pmxWebhookKeyValueList',
name: 'header',
fieldLabel: gettext('Headers'),
addLabel: gettext('Add Header'),
maskValues: false,
cbind: {
isCreate: '{isCreate}',
},
margin: '0 0 10 0',
},
{
xtype: 'textarea',
fieldLabel: gettext('Body'),
name: 'body',
allowBlank: true,
minHeight: '150',
fieldStyle: {
'font-family': 'monospace',
},
margin: '0 0 5 0',
},
{
xtype: 'pmxWebhookKeyValueList',
name: 'secret',
fieldLabel: gettext('Secrets'),
addLabel: gettext('Add Secret'),
maskValues: true,
cbind: {
isCreate: '{isCreate}',
},
margin: '0 0 10 0',
},
{
xtype: 'proxmoxtextfield',
name: 'comment',
fieldLabel: gettext('Comment'),
cbind: {
deleteEmpty: '{!isCreate}',
},
},
],
onSetValues: (values) => {
values.enable = !values.disable;
if (values.body) {
values.body = Proxmox.Utils.base64ToUtf8(values.body);
}
delete values.disable;
return values;
},
onGetValues: function(values) {
let me = this;
if (values.enable) {
if (!me.isCreate) {
Proxmox.Utils.assemble_field_data(values, { 'delete': 'disable' });
}
} else {
values.disable = 1;
}
if (values.body) {
values.body = Proxmox.Utils.utf8ToBase64(values.body);
} else {
delete values.body;
if (!me.isCreate) {
Proxmox.Utils.assemble_field_data(values, { 'delete': 'body' });
}
}
if (Ext.isArray(values.header) && !values.header.length) {
delete values.header;
if (!me.isCreate) {
Proxmox.Utils.assemble_field_data(values, { 'delete': 'header' });
}
}
if (Ext.isArray(values.secret) && !values.secret.length) {
delete values.secret;
if (!me.isCreate) {
Proxmox.Utils.assemble_field_data(values, { 'delete': 'secret' });
}
}
delete values.enable;
return values;
},
});
Ext.define('Proxmox.form.WebhookKeyValueList', {
extend: 'Ext.form.FieldContainer',
alias: 'widget.pmxWebhookKeyValueList',
mixins: [
'Ext.form.field.Field',
],
// override for column header
fieldTitle: gettext('Item'),
// label displayed in the "Add" button
addLabel: undefined,
// will be applied to the textfields
maskRe: undefined,
allowBlank: true,
selectAll: false,
isFormField: true,
deleteEmpty: false,
config: {
deleteEmpty: false,
maskValues: false,
},
setValue: function(list) {
let me = this;
list = Ext.isArray(list) ? list : (list ?? '').split(';').filter(t => t !== '');
let store = me.lookup('grid').getStore();
if (list.length > 0) {
store.setData(list.map(item => {
let properties = Proxmox.Utils.parsePropertyString(item);
// decode base64
let value = me.maskValues ? '' : Proxmox.Utils.base64ToUtf8(properties.value);
let obj = {
headerName: properties.name,
headerValue: value,
};
if (!me.isCreate && me.maskValues) {
obj.emptyText = gettext('Unchanged');
}
return obj;
}));
} else {
store.removeAll();
}
me.checkChange();
return me;
},
getValue: function() {
let me = this;
let values = [];
me.lookup('grid').getStore().each((rec) => {
if (rec.data.headerName) {
let obj = {
name: rec.data.headerName,
value: Proxmox.Utils.utf8ToBase64(rec.data.headerValue),
};
values.push(Proxmox.Utils.printPropertyString(obj));
}
});
return values;
},
getErrors: function(value) {
let me = this;
let empty = false;
me.lookup('grid').getStore().each((rec) => {
if (!rec.data.headerName) {
empty = true;
}
if (!rec.data.headerValue && rec.data.newValue) {
empty = true;
}
if (!rec.data.headerValue && !me.maskValues) {
empty = true;
}
});
if (empty) {
return [gettext('Name/value must not be empty.')];
}
return [];
},
// override framework function to implement deleteEmpty behaviour
getSubmitData: function() {
let me = this,
data = null,
val;
if (!me.disabled && me.submitValue) {
val = me.getValue();
if (val !== null && val !== '') {
data = {};
data[me.getName()] = val;
} else if (me.getDeleteEmpty()) {
data = {};
data.delete = me.getName();
}
}
return data;
},
controller: {
xclass: 'Ext.app.ViewController',
addLine: function() {
let me = this;
me.lookup('grid').getStore().add({
headerName: '',
headerValue: '',
emptyText: gettext('Value'),
newValue: true,
});
},
removeSelection: function(field) {
let me = this;
let view = me.getView();
let grid = me.lookup('grid');
let record = field.getWidgetRecord();
if (record === undefined) {
// this is sometimes called before a record/column is initialized
return;
}
grid.getStore().remove(record);
view.checkChange();
view.validate();
},
itemChange: function(field, newValue) {
let rec = field.getWidgetRecord();
if (!rec) {
return;
}
let column = field.getWidgetColumn();
rec.set(column.dataIndex, newValue);
let list = field.up('pmxWebhookKeyValueList');
list.checkChange();
list.validate();
},
control: {
'grid button': {
click: 'removeSelection',
},
},
},
initComponent: function() {
let me = this;
let items = [
{
xtype: 'grid',
reference: 'grid',
minHeight: 100,
maxHeight: 100,
scrollable: 'vertical',
viewConfig: {
deferEmptyText: false,
},
store: {
listeners: {
update: function() {
this.commitChanges();
},
},
},
margin: '5 0 5 0',
columns: [
{
header: me.fieldTtitle,
dataIndex: 'headerName',
xtype: 'widgetcolumn',
widget: {
xtype: 'textfield',
isFormField: false,
maskRe: me.maskRe,
allowBlank: false,
queryMode: 'local',
emptyText: gettext('Key'),
listeners: {
change: 'itemChange',
},
},
onWidgetAttach: function(_col, widget) {
widget.isValid();
},
flex: 1,
},
{
header: me.fieldTtitle,
dataIndex: 'headerValue',
xtype: 'widgetcolumn',
widget: {
xtype: 'proxmoxtextfield',
inputType: me.maskValues ? 'password' : 'text',
isFormField: false,
maskRe: me.maskRe,
queryMode: 'local',
listeners: {
change: 'itemChange',
},
allowBlank: !me.isCreate && me.maskValues,
bind: {
emptyText: '{record.emptyText}',
},
},
onWidgetAttach: function(_col, widget) {
widget.isValid();
},
flex: 1,
},
{
xtype: 'widgetcolumn',
width: 40,
widget: {
xtype: 'button',
iconCls: 'fa fa-trash-o',
},
},
],
},
{
xtype: 'button',
text: me.addLabel ? me.addLabel : gettext('Add'),
iconCls: 'fa fa-plus-circle',
handler: 'addLine',
},
];
for (const [key, value] of Object.entries(me.gridConfig ?? {})) {
items[0][key] = value;
}
Ext.apply(me, {
items,
});
me.callParent();
me.initField();
},
});

View File

@ -12,12 +12,6 @@
// make the color transparent so the border doesn't appear visibly,
// like in crisp, but keep to keep the layout intact
border-color: transparent;
// relevant for (disabled) elements that are rendered directly on the header bar like tools
// As otherwise, a too light mask is used, which then makes the element stand out in an odd way
.x-mask {
background-color: rgba($background-darker, 0.7);
}
}
.x-window-header-title-default {

View File

@ -33,13 +33,6 @@
color: $icon-color;
}
// pve needs to set `color` on this pseudo element, but the api viewer and pbs
// add a filter to the parent. so invert it here again, to fix this for pve.
.x-tree-icon-parent:not(.x-tree-icon-custom)::before,
.x-tree-icon-parent-expanded:not(.x-tree-icon-custom)::before {
filter: invert(90%);
}
// but some are implement as background SVGs or PNGs. invert them via
// filters:
.fa-ceph::before,
@ -111,9 +104,6 @@
}
// pbs show task log in longest task list column
.fa.black,
.fa.black::after,
.fa.black::before,
.x-action-col-icon.fa-chevron-right::before {
filter: none;
}
@ -232,12 +222,6 @@
}
}
// set icon color of intentional black icons (e.g.: pencil icon for
// quickly changing the ACME account)
.fa.black {
color: $icon-color;
}
// The usage icons dynamically displaying how full a storage is
.usage-wrapper {
border: 1px solid $icon-color;

View File

@ -4,8 +4,7 @@ img[id^="proxmoxlogo-"][id$="-img"] {
}
// removes the gray line in the header of the mail gateway
div[id^="versioninfo-"] + div[id^="panel-"] > div[id^="panel-"][id$="-bodyWrap"] > div,
div.eol-notice + div[id^="panel-"] > div[id^="panel-"][id$="-bodyWrap"] > div {
div[id^="versioninfo-"] + div[id^="panel-"] > div[id^="panel-"][id$="-bodyWrap"] > div {
background-color: transparent;
border-color: transparent;
}

View File

@ -95,14 +95,14 @@ Ext.define('Proxmox.window.ACMEPluginEdit', {
let field = Ext.create({
xtype,
name: `custom_${name}`,
fieldLabel: Ext.htmlEncode(label),
fieldLabel: label,
width: '100%',
labelWidth: 150,
labelSeparator: '=',
emptyText: definition.default || '',
autoEl: definition.description ? {
tag: 'div',
'data-qtip': Ext.htmlEncode(Ext.htmlEncode(definition.description)),
'data-qtip': definition.description,
} : undefined,
});
@ -127,7 +127,6 @@ Ext.define('Proxmox.window.ACMEPluginEdit', {
if (me.createdFields[key]) {
me.createdFields[key].setValue(value);
me.createdFields[key].originalValue = me.originalValues[key];
me.createdFields[key].checkDirty();
} else {
extradata.push(`${key}=${value}`);
}

View File

@ -224,10 +224,10 @@ Ext.define('Proxmox.window.AddTotp', {
visible: '{!secretEmpty}',
},
style: {
margin: '16px auto',
padding: '16px',
width: '288px',
height: '288px',
margin: '5px auto',
padding: '5px',
width: '266px',
height: '266px',
'background-color': 'white',
},
},

View File

@ -1,14 +0,0 @@
Ext.define('Proxmox.panel.ADInputPanel', {
extend: 'Proxmox.panel.LDAPInputPanel',
xtype: 'pmxAuthADPanel',
type: 'ad',
onlineHelp: 'user-realms-ad',
});
Ext.define('Proxmox.panel.ADSyncInputPanel', {
extend: 'Proxmox.panel.LDAPSyncInputPanel',
xtype: 'pmxAuthADSyncPanel',
type: 'ad',
});

View File

@ -1,8 +1,5 @@
Ext.define('Proxmox.window.AuthEditBase', {
extend: 'Proxmox.window.Edit',
mixins: ['Proxmox.Mixin.CBind'],
showDefaultRealm: false,
isAdd: true,
@ -32,9 +29,9 @@ Ext.define('Proxmox.window.AuthEditBase', {
let authConfig = Proxmox.Schema.authDomains[me.authType];
if (!authConfig) {
throw `unknown auth type ${me.authType}`;
throw 'unknown auth type';
} else if (!authConfig.add && me.isCreate) {
throw `trying to add non addable realm of type ${me.authType}`;
throw 'trying to add non addable realm';
}
me.subject = authConfig.name;
@ -54,9 +51,7 @@ Ext.define('Proxmox.window.AuthEditBase', {
realm: me.realm,
xtype: authConfig.ipanel,
isCreate: me.isCreate,
useTypeInUrl: me.useTypeInUrl,
type: me.authType,
showDefaultRealm: me.showDefaultRealm,
},
{
title: gettext('Sync Options'),
@ -72,9 +67,7 @@ Ext.define('Proxmox.window.AuthEditBase', {
realm: me.realm,
xtype: authConfig.ipanel,
isCreate: me.isCreate,
useTypeInUrl: me.useTypeInUrl,
type: me.authType,
showDefaultRealm: me.showDefaultRealm,
}];
}
@ -93,7 +86,7 @@ Ext.define('Proxmox.window.AuthEditBase', {
// only check this when the type is not in the api path
if (!me.useTypeInUrl && data.type !== me.authType) {
me.close();
throw `got wrong auth type '${me.authType}' for realm '${data.type}'`;
throw "got wrong auth type";
}
me.setValues(data);
},

View File

@ -1,5 +1,7 @@
Ext.define('Proxmox.panel.LDAPInputPanelViewModel', {
extend: 'Ext.app.ViewModel',
alias: 'viewmodel.pmxAuthLDAPPanel',
data: {
@ -21,8 +23,6 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
xtype: 'pmxAuthLDAPPanel',
mixins: ['Proxmox.Mixin.CBind'],
showDefaultRealm: false,
viewModel: {
type: 'pmxAuthLDAPPanel',
},
@ -32,11 +32,11 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
onlineHelp: 'user-realms-ldap',
onGetValues: function(values) {
if (this.isCreate && !this.useTypeInUrl) {
if (this.isCreate) {
values.type = this.type;
}
if (values.anonymous_search && !this.isCreate) {
if (values.anonymous_search) {
if (!values.delete) {
values.delete = [];
}
@ -64,12 +64,6 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
return values;
},
cbindData: function(config) {
return {
isLdap: this.type === 'ldap',
isAd: this.type === 'ad',
};
},
column1: [
{
@ -82,40 +76,19 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
fieldLabel: gettext('Realm'),
allowBlank: false,
},
{
xtype: 'proxmoxcheckbox',
fieldLabel: gettext('Default realm'),
name: 'default',
value: 0,
cbind: {
deleteEmpty: '{!isCreate}',
hidden: '{!showDefaultRealm}',
disabled: '{!showDefaultRealm}',
},
autoEl: {
tag: 'div',
'data-qtip': gettext('Set realm as default for login'),
},
},
{
xtype: 'proxmoxtextfield',
fieldLabel: gettext('Base Domain Name'),
name: 'base-dn',
allowBlank: false,
emptyText: 'cn=Users,dc=company,dc=net',
cbind: {
hidden: '{!isLdap}',
allowBlank: '{!isLdap}',
},
},
{
xtype: 'proxmoxtextfield',
fieldLabel: gettext('User Attribute Name'),
name: 'user-attr',
allowBlank: false,
emptyText: 'uid / sAMAccountName',
cbind: {
hidden: '{!isLdap}',
allowBlank: '{!isLdap}',
},
},
{
xtype: 'proxmoxcheckbox',
@ -130,14 +103,7 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
fieldLabel: gettext('Bind Domain Name'),
name: 'bind-dn',
allowBlank: false,
cbind: {
emptyText: get => get('isAd') ? 'user@company.net' : 'cn=user,dc=company,dc=net',
autoEl: get => get('isAd') ? {
tag: 'div',
'data-qtip':
gettext('LDAP DN syntax can be used as well, e.g. cn=user,dc=company,dc=net'),
} : {},
},
emptyText: 'cn=user,dc=company,dc=net',
bind: {
disabled: "{anonymous_search}",
},
@ -147,9 +113,9 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
inputType: 'password',
fieldLabel: gettext('Bind Password'),
name: 'password',
allowBlank: true,
cbind: {
emptyText: get => !get('isCreate') ? gettext('Unchanged') : '',
allowBlank: '{!isCreate}',
},
bind: {
disabled: "{anonymous_search}",
@ -181,9 +147,7 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
maxValue: 65535,
emptyText: gettext('Default'),
submitEmptyText: false,
cbind: {
deleteEmpty: '{!isCreate}',
},
deleteEmpty: true,
},
{
xtype: 'proxmoxKVComboBox',
@ -223,7 +187,7 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
columnB: [
{
xtype: 'proxmoxtextfield',
xtype: 'textfield',
name: 'comment',
fieldLabel: gettext('Comment'),
cbind: {
@ -231,6 +195,7 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
},
},
],
});
@ -330,30 +295,16 @@ Ext.define('Proxmox.panel.LDAPSyncInputPanel', {
xtype: 'proxmoxtextfield',
name: 'firstname',
fieldLabel: gettext('First Name attribute'),
autoEl: {
tag: 'div',
'data-qtip': Ext.String.format(gettext('Often called {0}'), '`givenName`'),
},
},
{
xtype: 'proxmoxtextfield',
name: 'lastname',
fieldLabel: gettext('Last Name attribute'),
autoEl: {
tag: 'div',
'data-qtip': Ext.String.format(gettext('Often called {0}'), '`sn`'),
},
},
{
xtype: 'proxmoxtextfield',
name: 'email',
fieldLabel: gettext('E-Mail attribute'),
autoEl: {
tag: 'div',
'data-qtip': get => get('isAd')
? Ext.String.format(gettext('Often called {0} or {1}'), '`userPrincipalName`', '`mail`')
: Ext.String.format(gettext('Often called {0}'), '`mail`'),
},
},
{
xtype: 'displayfield',
@ -385,9 +336,7 @@ Ext.define('Proxmox.panel.LDAPSyncInputPanel', {
xtype: 'proxmoxtextfield',
name: 'user-classes',
fieldLabel: gettext('User classes'),
cbind: {
deleteEmpty: '{!isCreate}',
},
deleteEmpty: true,
emptyText: 'inetorgperson, posixaccount, person, user',
autoEl: {
tag: 'div',
@ -398,9 +347,7 @@ Ext.define('Proxmox.panel.LDAPSyncInputPanel', {
xtype: 'proxmoxtextfield',
name: 'filter',
fieldLabel: gettext('User Filter'),
cbind: {
deleteEmpty: '{!isCreate}',
},
deleteEmpty: true,
},
],

View File

@ -3,14 +3,12 @@ Ext.define('Proxmox.panel.OpenIDInputPanel', {
xtype: 'pmxAuthOpenIDPanel',
mixins: ['Proxmox.Mixin.CBind'],
showDefaultRealm: false,
type: 'openid',
onGetValues: function(values) {
let me = this;
if (me.isCreate && !me.useTypeInUrl) {
if (me.isCreate) {
values.type = me.type;
}
@ -37,21 +35,6 @@ Ext.define('Proxmox.panel.OpenIDInputPanel', {
fieldLabel: gettext('Realm'),
allowBlank: false,
},
{
xtype: 'proxmoxcheckbox',
fieldLabel: gettext('Default realm'),
name: 'default',
value: 0,
cbind: {
deleteEmpty: '{!isCreate}',
hidden: '{!showDefaultRealm}',
disabled: '{!showDefaultRealm}',
},
autoEl: {
tag: 'div',
'data-qtip': gettext('Set realm as default for login'),
},
},
{
xtype: 'proxmoxtextfield',
fieldLabel: gettext('Client ID'),
@ -129,7 +112,7 @@ Ext.define('Proxmox.panel.OpenIDInputPanel', {
columnB: [
{
xtype: 'proxmoxtextfield',
xtype: 'textfield',
name: 'comment',
fieldLabel: gettext('Comment'),
cbind: {

View File

@ -1,45 +0,0 @@
Ext.define('Proxmox.panel.SimpleRealmInputPanel', {
extend: 'Proxmox.panel.InputPanel',
xtype: 'pmxAuthSimplePanel',
mixins: ['Proxmox.Mixin.CBind'],
showDefaultRealm: false,
column1: [
{
xtype: 'pmxDisplayEditField',
name: 'realm',
cbind: {
value: '{realm}',
},
fieldLabel: gettext('Realm'),
},
{
xtype: 'proxmoxcheckbox',
fieldLabel: gettext('Default realm'),
name: 'default',
value: 0,
deleteEmpty: true,
autoEl: {
tag: 'div',
'data-qtip': gettext('Set realm as default for login'),
},
cbind: {
hidden: '{!showDefaultRealm}',
disabled: '{!showDefaultRealm}',
},
},
],
column2: [],
columnB: [
{
xtype: 'proxmoxtextfield',
name: 'comment',
fieldLabel: gettext('Comment'),
allowBlank: true,
deleteEmpty: true,
},
],
});

View File

@ -1,36 +0,0 @@
Ext.define('Proxmox.window.ConsentModal', {
extend: 'Ext.window.Window',
alias: ['widget.pmxConsentModal'],
mixins: ['Proxmox.Mixin.CBind'],
maxWidth: 1000,
maxHeight: 1000,
minWidth: 600,
minHeight: 400,
scrollable: true,
modal: true,
closable: false,
resizable: false,
alwaysOnTop: true,
title: gettext('Consent'),
items: [
{
xtype: 'displayfield',
padding: 10,
scrollable: true,
cbind: {
value: '{consent}',
},
},
],
buttons: [
{
handler: function() {
this.up('window').close();
},
text: gettext('OK'),
},
],
});

View File

@ -29,7 +29,6 @@ Ext.define('Proxmox.window.DiskSmart', {
text: 'ID',
dataIndex: 'id',
width: 50,
align: 'right',
},
{
text: gettext('Attribute'),
@ -46,19 +45,16 @@ Ext.define('Proxmox.window.DiskSmart', {
text: gettext('Normalized'),
dataIndex: 'real-normalized',
width: 60,
align: 'right',
},
{
text: gettext('Threshold'),
dataIndex: 'threshold',
width: 60,
align: 'right',
},
{
text: gettext('Worst'),
dataIndex: 'worst',
width: 60,
align: 'right',
},
{
text: gettext('Flags'),

View File

@ -31,9 +31,6 @@ Ext.define('Proxmox.window.Edit', {
// custom submitText
submitText: undefined,
// custom options for the submit api call
submitOptions: {},
backgroundDelay: 0,
// string or function, called as (url, values) - useful if the ID of the
@ -69,15 +66,6 @@ Ext.define('Proxmox.window.Edit', {
// onlineHelp of our first item, if set.
onlineHelp: undefined,
constructor: function(conf) {
let me = this;
// make copies in order to prevent subclasses from accidentally writing
// to objects that are shared with other edit window subclasses
me.extraRequestParams = Object.assign({}, me.extraRequestParams);
me.submitOptions = Object.assign({}, me.submitOptions);
me.callParent(arguments);
},
isValid: function() {
let me = this;
@ -163,7 +151,7 @@ Ext.define('Proxmox.window.Edit', {
values = undefined;
}
let requestOptions = Ext.apply({
Proxmox.Utils.API2Request({
url: url,
waitMsgTarget: me,
method: me.method || (me.backgroundDelay ? 'POST' : 'PUT'),
@ -203,8 +191,7 @@ Ext.define('Proxmox.window.Edit', {
me.close();
}
},
}, me.submitOptions ?? {});
Proxmox.Utils.API2Request(requestOptions);
});
},
load: function(options) {
@ -318,21 +305,19 @@ Ext.define('Proxmox.window.Edit', {
},
});
let resetTool = Ext.create('Ext.panel.Tool', {
glyph: 'xf0e2@FontAwesome', // fa-undo
tooltip: gettext('Reset form data'),
callback: () => form.reset(),
style: {
paddingRight: '2px', // just slightly more room to breathe
},
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));
resetTool.setDisabled(!dirty);
resetBtn.setDisabled(!dirty);
};
form.on('dirtychange', set_button_status);
@ -349,10 +334,10 @@ Ext.define('Proxmox.window.Edit', {
me.title = Proxmox.Utils.dialog_title(me.subject, me.isCreate, me.isAdd);
}
me.buttons = [submitBtn];
if (!me.isCreate && me.showReset) {
me.tools = [resetTool];
if (me.isCreate || !me.showReset) {
me.buttons = [submitBtn];
} else {
me.buttons = [submitBtn, resetBtn];
}
if (inputPanel && inputPanel.hasAdvanced) {

View File

@ -1,58 +0,0 @@
Ext.define('Proxmox.window.EndpointEditBase', {
extend: 'Proxmox.window.Edit',
isAdd: true,
fieldDefaults: {
labelWidth: 120,
},
width: 700,
initComponent: function() {
let me = this;
me.isCreate = !me.name;
if (!me.baseUrl) {
throw "baseUrl not set";
}
if (me.type === 'group') {
me.url = `/api2/extjs${me.baseUrl}/groups`;
} else {
me.url = `/api2/extjs${me.baseUrl}/endpoints/${me.type}`;
}
if (me.isCreate) {
me.method = 'POST';
} else {
me.url += `/${me.name}`;
me.method = 'PUT';
}
let endpointConfig = Proxmox.Schema.notificationEndpointTypes[me.type];
if (!endpointConfig) {
throw 'unknown endpoint type';
}
me.subject = endpointConfig.name;
Ext.apply(me, {
items: [{
name: me.name,
xtype: endpointConfig.ipanel,
isCreate: me.isCreate,
baseUrl: me.baseUrl,
type: me.type,
defaultMailAuthor: endpointConfig.defaultMailAuthor,
}],
});
me.callParent();
if (!me.isCreate) {
me.load();
}
},
});

View File

@ -61,6 +61,10 @@ Ext.define("Proxmox.window.FileBrowser", {
'd': true, // directories
},
// enable tar download, this will add a menu to the "Download" button when the selection
// can be downloaded as `.tar` files
enableTar: false,
// prefix to prepend to downloaded file names
downloadPrefix: '',
},
@ -122,7 +126,7 @@ Ext.define("Proxmox.window.FileBrowser", {
view.lookup('selectText').setText(st);
let canDownload = view.downloadURL && view.downloadableFileTypes[data.type];
let enableMenu = data.type === 'd';
let enableMenu = view.enableTar && data.type === 'd';
let downloadBtn = view.lookup('downloadBtn');
downloadBtn.setDisabled(!canDownload || enableMenu);

View File

@ -12,12 +12,6 @@ Ext.define('Proxmox.window.LanguageEditWindow', {
xclass: 'Ext.app.ViewController',
init: function(view) {
let language = Ext.util.Cookies.get(view.cookieName) || '__default__';
if (language === 'kr') {
// fix-up wrongly used Korean code before FIXME: remove with trixie releases
language = 'ko';
let expire = Ext.Date.add(new Date(), Ext.Date.YEAR, 10);
Ext.util.Cookies.set(view.cookieName, language, expire);
}
this.getViewModel().set('language', language);
},
applyLanguage: function(button) {

File diff suppressed because it is too large Load Diff

View File

@ -7,50 +7,27 @@ Ext.define('Proxmox.window.PasswordEdit', {
url: '/api2/extjs/access/password',
width: 380,
fieldDefaults: {
labelWidth: 150,
labelWidth: 120,
},
// specifies the minimum length of *new* passwords so this can be
// adapted by each product as limits are changed there.
minLength: 5,
// allow products to opt-in as their API gains support for this.
confirmCurrentPassword: false,
items: [
{
xtype: 'textfield',
inputType: 'password',
fieldLabel: gettext('Your Current Password'),
reference: 'confirmation-password',
name: 'confirmation-password',
allowBlank: false,
vtype: 'password',
cbind: {
hidden: '{!confirmCurrentPassword}',
disabled: '{!confirmCurrentPassword}',
},
},
{
xtype: 'textfield',
inputType: 'password',
fieldLabel: gettext('New Password'),
fieldLabel: gettext('Password'),
minLength: 5,
allowBlank: false,
name: 'password',
listeners: {
change: (field) => field.next().validate(),
blur: (field) => field.next().validate(),
},
cbind: {
minLength: '{minLength}',
},
},
{
xtype: 'textfield',
inputType: 'password',
fieldLabel: gettext('Confirm New Password'),
fieldLabel: gettext('Confirm password'),
name: 'verifypassword',
allowBlank: false,
vtype: 'password',

View File

@ -1,5 +1,7 @@
// Pop-up a message window where the user has to manually enter the resource ID to enable the
// destroy confirmation button to ensure that they got the correct resource selected for.
/* Popup a message window
* where the user has to manually enter the resource ID
* to enable the destroy button
*/
Ext.define('Proxmox.window.SafeDestroy', {
extend: 'Ext.window.Window',
alias: 'widget.proxmoxSafeDestroy',

View File

@ -108,11 +108,11 @@ Ext.define('Proxmox.window.TaskViewer', {
defaultValue: 'unknown',
renderer: function(value) {
if (value !== 'stopped') {
return Ext.htmlEncode(value);
return value;
}
let es = statgrid.getObjectValue('exitstatus');
if (es) {
return Ext.htmlEncode(`${value}: ${es}`);
return value + ': ' + es;
}
return 'unknown';
},

View File

@ -45,17 +45,11 @@ Ext.define('Proxmox.window.TfaLoginWindow', {
let lastTabId = me.getLastTabUsed();
let initialTab = -1, i = 0;
let count2nd = 0;
let hasRecovery = false;
for (const k of ['webauthn', 'totp', 'recovery', 'u2f', 'yubico']) {
const available = !!challenge[k];
vm.set(`availableChallenge.${k}`, available);
if (available) {
count2nd++;
if (k === 'recovery') {
hasRecovery = true;
}
if (i === lastTabId) {
initialTab = i;
} else if (initialTab < 0) {
@ -64,32 +58,15 @@ Ext.define('Proxmox.window.TfaLoginWindow', {
}
i++;
}
if (!count2nd || (count2nd === 1 && hasRecovery && !challenge.recovery.length)) {
// no 2nd factors available (and if recovery keys are configured they're empty)
me.lookup('cannotLogin').setVisible(true);
me.lookup('recoveryKey').setVisible(false);
view.down('tabpanel').setActiveTab(2); // recovery
return;
}
view.down('tabpanel').setActiveTab(initialTab);
if (challenge.recovery) {
if (!view.challenge.recovery.length) {
me.lookup('recoveryEmpty').setVisible(true);
me.lookup('recoveryKey').setVisible(false);
} else {
let idList = view
.challenge
.recovery
.map((id) => Ext.String.format(gettext('ID {0}'), id))
.join(', ');
me.lookup('availableRecovery').update(Ext.String.htmlEncode(
Ext.String.format(gettext('Available recovery keys: {0}'), idList),
));
me.lookup('availableRecovery').setVisible(true);
if (view.challenge.recovery.length <= 3) {
me.lookup('recoveryLow').setVisible(true);
}
me.lookup('availableRecovery').update(Ext.String.htmlEncode(
gettext('Available recovery keys: ') + view.challenge.recovery.join(', '),
));
me.lookup('availableRecovery').setVisible(true);
if (view.challenge.recovery.length <= 3) {
me.lookup('recoveryLow').setVisible(true);
}
}
@ -374,7 +351,6 @@ Ext.define('Proxmox.window.TfaLoginWindow', {
allowBlank: false,
regex: /^[0-9]{2,16}$/,
regexText: gettext('TOTP codes usually consist of six decimal digits'),
inputAttrTpl: 'autocomplete=one-time-code',
},
],
},
@ -387,36 +363,6 @@ Ext.define('Proxmox.window.TfaLoginWindow', {
disabled: '{!availableChallenge.recovery}',
},
items: [
{
xtype: 'box',
reference: 'cannotLogin',
hidden: true,
html: '<i class="fa fa-exclamation-triangle warning"></i>'
+ Ext.String.format(
gettext('No second factor left! Please contact an administrator!'),
4,
),
},
{
xtype: 'box',
reference: 'recoveryEmpty',
hidden: true,
html: '<i class="fa fa-exclamation-triangle warning"></i>'
+ Ext.String.format(
gettext('No more recovery keys left! Please generate a new set!'),
4,
),
},
{
xtype: 'box',
reference: 'recoveryLow',
hidden: true,
html: '<i class="fa fa-exclamation-triangle warning"></i>'
+ Ext.String.format(
gettext('Less than {0} recovery keys available. Please generate a new set after login!'),
4,
),
},
{
xtype: 'box',
reference: 'availableRecovery',
@ -433,6 +379,16 @@ Ext.define('Proxmox.window.TfaLoginWindow', {
regex: /^[0-9a-f]{4}(-[0-9a-f]{4}){3}$/,
regexText: gettext('Does not look like a valid recovery key'),
},
{
xtype: 'box',
reference: 'recoveryLow',
hidden: true,
html: '<i class="fa fa-exclamation-triangle warning"></i>'
+ Ext.String.format(
gettext('Less than {0} recovery keys available. Please generate a new set after login!'),
4,
),
},
],
},
{