Compare commits
208 Commits
stable-bul
...
master
Author | SHA1 | Date | |
---|---|---|---|
![]() |
68a3518d13 | ||
![]() |
a37094b0fa | ||
![]() |
b4304e6691 | ||
![]() |
ee9d92e37e | ||
![]() |
2df5c28d3f | ||
![]() |
f6440d3ee1 | ||
![]() |
15a62419a1 | ||
![]() |
23289264bd | ||
![]() |
41c844b511 | ||
![]() |
82260afcdd | ||
![]() |
80b01f27bc | ||
![]() |
88d62e5386 | ||
![]() |
f26346a597 | ||
![]() |
9d5d6a7815 | ||
![]() |
7bb124c036 | ||
![]() |
867bf7e6f5 | ||
![]() |
d710ceeeb6 | ||
![]() |
d6d173f05d | ||
![]() |
a5fb3afcf9 | ||
![]() |
ac10db95ef | ||
![]() |
c806b73ff7 | ||
![]() |
1d07d61a18 | ||
![]() |
70382e111a | ||
![]() |
e79a20a8cc | ||
![]() |
461298d80a | ||
![]() |
2686922392 | ||
![]() |
3375d11ea4 | ||
![]() |
0988db8ffc | ||
![]() |
358b98bf4f | ||
![]() |
2aa5d7e7ba | ||
![]() |
ffe41ad5e3 | ||
![]() |
b1a3eb7195 | ||
![]() |
9d2fc36f61 | ||
![]() |
afb219dce0 | ||
![]() |
e84f80271d | ||
![]() |
e872ed40f7 | ||
![]() |
b0eafb68a9 | ||
![]() |
3052c4dfc8 | ||
![]() |
20002254e6 | ||
![]() |
0f8d38b5de | ||
![]() |
6fe81f0cef | ||
![]() |
4b3850bf45 | ||
![]() |
a285fec7e4 | ||
![]() |
b4b36ef29a | ||
![]() |
be8cb1efb4 | ||
![]() |
3822a031dd | ||
![]() |
5156cd1164 | ||
![]() |
8f2c09235c | ||
![]() |
96fb6cfb72 | ||
![]() |
4ca542b811 | ||
![]() |
a351d3d36d | ||
![]() |
0ad5712ac7 | ||
![]() |
ffeb8004fb | ||
![]() |
f18c0d3636 | ||
![]() |
6af66f9626 | ||
![]() |
9e69d726e0 | ||
![]() |
51e64f26ec | ||
![]() |
f420a35cb7 | ||
![]() |
fa315d842b | ||
![]() |
e91269d513 | ||
![]() |
043ce82954 | ||
![]() |
1ed4b715bc | ||
![]() |
68cea10fa7 | ||
![]() |
a5fa465796 | ||
![]() |
10f804941c | ||
![]() |
3251f4042b | ||
![]() |
2d98e03d7c | ||
![]() |
f95fcc26b6 | ||
![]() |
3a4432a6e4 | ||
![]() |
af27e81747 | ||
![]() |
9ffe9009fb | ||
![]() |
2e454014e0 | ||
![]() |
82bb667f05 | ||
![]() |
1689e53f6f | ||
![]() |
f646c22a67 | ||
![]() |
bb5cc876ef | ||
![]() |
d958d57e0d | ||
![]() |
046ec35b11 | ||
![]() |
b1df1f0efb | ||
![]() |
48830b9713 | ||
![]() |
43b66893a8 | ||
![]() |
9afdea76cf | ||
![]() |
537702e12f | ||
![]() |
0493654c85 | ||
![]() |
120b530a31 | ||
![]() |
83bae06cae | ||
![]() |
26e2aa7ce4 | ||
![]() |
2834a05d2b | ||
![]() |
502c84b1ee | ||
![]() |
2b6e8c67a6 | ||
![]() |
4a5dd69899 | ||
![]() |
da210b58a7 | ||
![]() |
4aff870f6c | ||
![]() |
f60fe4ad99 | ||
![]() |
a5d6c92f60 | ||
![]() |
be5b9a4393 | ||
![]() |
d35cb8aa7c | ||
![]() |
461bb2e56c | ||
![]() |
8d95122d85 | ||
![]() |
a5b07f65ca | ||
![]() |
03e44f5b60 | ||
![]() |
8d161ac19c | ||
![]() |
e3cf77e177 | ||
![]() |
9c69fc1b8f | ||
![]() |
d2c8fbfe0c | ||
![]() |
36d167d629 | ||
![]() |
4bee6fb074 | ||
![]() |
43b978658e | ||
![]() |
bbb7ecbe36 | ||
![]() |
fc792d6411 | ||
![]() |
905528a28d | ||
![]() |
115165bec2 | ||
![]() |
1fc3d8cd1c | ||
![]() |
64821a108c | ||
![]() |
a5630fd2dd | ||
![]() |
55f8555f18 | ||
![]() |
6ed92b735f | ||
![]() |
c972de233a | ||
![]() |
edd98f946b | ||
![]() |
49f7549b75 | ||
![]() |
1f8bfa3b30 | ||
![]() |
be5329512c | ||
![]() |
6a0b0b376c | ||
![]() |
5f7b28cb19 | ||
![]() |
3003f37779 | ||
![]() |
de0cec409a | ||
![]() |
5bd3ad4e90 | ||
![]() |
03a54ebd29 | ||
![]() |
e8f1954c55 | ||
![]() |
feacab72ca | ||
![]() |
159fec230d | ||
![]() |
78d21b71d2 | ||
![]() |
15f0d99534 | ||
![]() |
e34b0f4a12 | ||
![]() |
60e1c56233 | ||
![]() |
4ecad70c66 | ||
![]() |
7605e43eaa | ||
![]() |
64ffc0378e | ||
![]() |
d981e33b1f | ||
![]() |
40e341792e | ||
![]() |
0e3cb037d6 | ||
![]() |
8fc1d232ea | ||
![]() |
8a8b0428fd | ||
![]() |
3749f20c5e | ||
![]() |
5d7d30de0f | ||
![]() |
5a3ac9b110 | ||
![]() |
1326f771b9 | ||
![]() |
89699c6466 | ||
![]() |
9ef8030535 | ||
![]() |
e21d3a40ad | ||
![]() |
ade0e572d2 | ||
![]() |
b4b3bcad18 | ||
![]() |
247304085f | ||
![]() |
7e4b51778a | ||
![]() |
72a355b8d7 | ||
![]() |
4187bffeeb | ||
![]() |
6e70fce94f | ||
![]() |
0e1fa66f75 | ||
![]() |
9133d5b5f9 | ||
![]() |
a5736f02e5 | ||
![]() |
575b4f3790 | ||
![]() |
49bb8516ed | ||
![]() |
5d1b587fdd | ||
![]() |
90dbb2d359 | ||
![]() |
ea5aa12261 | ||
![]() |
6669a59fd1 | ||
![]() |
84f70dfaad | ||
![]() |
a60c8dc0c0 | ||
![]() |
6e5405767c | ||
![]() |
2d613cac1f | ||
![]() |
e61ffdd6ee | ||
![]() |
81562cea8d | ||
![]() |
085eed30c2 | ||
![]() |
efcb34fa21 | ||
![]() |
589317b094 | ||
![]() |
c655b2f577 | ||
![]() |
7d5201a32f | ||
![]() |
01034bdb5f | ||
![]() |
f224688d0c | ||
![]() |
a14bafeca6 | ||
![]() |
78be60a079 | ||
![]() |
7a0bbba33a | ||
![]() |
33cfd1f6b4 | ||
![]() |
50af081a20 | ||
![]() |
e6ed4498cd | ||
![]() |
b9b1a51a2e | ||
![]() |
3d6b76ee2b | ||
![]() |
f6f29f8c1c | ||
![]() |
fd468868dd | ||
![]() |
ecdde39c1c | ||
![]() |
aec7e8d23c | ||
![]() |
95fa855701 | ||
![]() |
6883083e8a | ||
![]() |
9531241400 | ||
![]() |
db5d0cc1f4 | ||
![]() |
2d04f0165d | ||
![]() |
5cbbb9c44a | ||
![]() |
b0c7069a10 | ||
![]() |
28f879c09e | ||
![]() |
e09af56554 | ||
![]() |
0676eb3738 | ||
![]() |
1683d09017 | ||
![]() |
01e64778ad | ||
![]() |
9dba61e674 | ||
![]() |
b515e164b7 | ||
![]() |
9521d1768e | ||
![]() |
b1f95caa4e | ||
![]() |
87ea4468db |
12
.gitignore
vendored
12
.gitignore
vendored
@ -1,4 +1,10 @@
|
|||||||
src/.lint-incremental
|
|
||||||
*.deb
|
|
||||||
*.changes
|
|
||||||
*.buildinfo
|
*.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
|
||||||
|
380
debian/changelog
vendored
380
debian/changelog
vendored
@ -1,3 +1,383 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
* 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-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
|
||||||
|
|
||||||
|
* apt repositories: actually ignore ignore-pre-upgrade-warning
|
||||||
|
|
||||||
|
* 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
|
||||||
|
|
||||||
|
* 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
|
||||||
|
|
||||||
|
* 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
|
||||||
|
|
||||||
|
* 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-widget-toolkit (3.7.0) bullseye; urgency=medium
|
proxmox-widget-toolkit (3.7.0) bullseye; urgency=medium
|
||||||
|
|
||||||
* dark-mode:
|
* dark-mode:
|
||||||
|
5
debian/control
vendored
5
debian/control
vendored
@ -2,11 +2,12 @@ Source: proxmox-widget-toolkit
|
|||||||
Section: web
|
Section: web
|
||||||
Priority: optional
|
Priority: optional
|
||||||
Maintainer: Proxmox Support Team <support@proxmox.com>
|
Maintainer: Proxmox Support Team <support@proxmox.com>
|
||||||
Build-Depends: debhelper-compat (= 12),
|
Build-Depends: debhelper-compat (= 13),
|
||||||
libjs-marked,
|
libjs-marked,
|
||||||
pve-eslint (>= 7.28.0),
|
pve-eslint (>= 7.28.0),
|
||||||
sassc,
|
sassc,
|
||||||
Standards-Version: 4.5.1
|
uglifyjs,
|
||||||
|
Standards-Version: 4.6.2
|
||||||
Homepage: https://www.proxmox.com
|
Homepage: https://www.proxmox.com
|
||||||
|
|
||||||
Package: proxmox-widget-toolkit
|
Package: proxmox-widget-toolkit
|
||||||
|
38
src/Makefile
38
src/Makefile
@ -1,10 +1,12 @@
|
|||||||
include defines.mk
|
include defines.mk
|
||||||
|
|
||||||
|
ESLINT ?= $(if $(shell command -v pve-eslint), pve-eslint, eslint)
|
||||||
|
|
||||||
SUBDIRS= css images proxmox-dark
|
SUBDIRS= css images proxmox-dark
|
||||||
|
|
||||||
# bundle it for now from the libjs-marked debian package to avoid touching our proxies file mapper,
|
# 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
|
# 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.min.js
|
MARKEDJS=/usr/share/javascript/marked/marked.js
|
||||||
|
|
||||||
JSSRC= \
|
JSSRC= \
|
||||||
Utils.js \
|
Utils.js \
|
||||||
@ -20,6 +22,7 @@ JSSRC= \
|
|||||||
data/ObjectStore.js \
|
data/ObjectStore.js \
|
||||||
data/RRDStore.js \
|
data/RRDStore.js \
|
||||||
data/TimezoneStore.js \
|
data/TimezoneStore.js \
|
||||||
|
data/model/NotificationConfig.js \
|
||||||
data/model/Realm.js \
|
data/model/Realm.js \
|
||||||
data/model/Certificates.js \
|
data/model/Certificates.js \
|
||||||
data/model/ACME.js \
|
data/model/ACME.js \
|
||||||
@ -28,6 +31,8 @@ JSSRC= \
|
|||||||
form/ExpireDate.js \
|
form/ExpireDate.js \
|
||||||
form/IntegerField.js \
|
form/IntegerField.js \
|
||||||
form/TextField.js \
|
form/TextField.js \
|
||||||
|
form/TextAreaField.js \
|
||||||
|
form/VlanField.js \
|
||||||
form/DateTimeField.js \
|
form/DateTimeField.js \
|
||||||
form/Checkbox.js \
|
form/Checkbox.js \
|
||||||
form/KVComboBox.js \
|
form/KVComboBox.js \
|
||||||
@ -45,6 +50,7 @@ JSSRC= \
|
|||||||
form/ACME.js \
|
form/ACME.js \
|
||||||
form/UserSelector.js \
|
form/UserSelector.js \
|
||||||
form/ThemeSelector.js \
|
form/ThemeSelector.js \
|
||||||
|
form/FingerprintField.js \
|
||||||
button/Button.js \
|
button/Button.js \
|
||||||
button/AltText.js \
|
button/AltText.js \
|
||||||
button/HelpButton.js \
|
button/HelpButton.js \
|
||||||
@ -57,18 +63,24 @@ JSSRC= \
|
|||||||
panel/InfoWidget.js \
|
panel/InfoWidget.js \
|
||||||
panel/LogView.js \
|
panel/LogView.js \
|
||||||
panel/NodeInfoRepoStatus.js \
|
panel/NodeInfoRepoStatus.js \
|
||||||
|
panel/NotificationConfigView.js \
|
||||||
panel/JournalView.js \
|
panel/JournalView.js \
|
||||||
panel/PermissionView.js \
|
panel/PermissionView.js \
|
||||||
panel/PruneKeepPanel.js \
|
panel/PruneKeepPanel.js \
|
||||||
panel/RRDChart.js \
|
panel/RRDChart.js \
|
||||||
panel/GaugeWidget.js \
|
panel/GaugeWidget.js \
|
||||||
|
panel/GotifyEditPanel.js \
|
||||||
panel/Certificates.js \
|
panel/Certificates.js \
|
||||||
panel/ACMEAccount.js \
|
panel/ACMEAccount.js \
|
||||||
panel/ACMEPlugin.js \
|
panel/ACMEPlugin.js \
|
||||||
panel/ACMEDomains.js \
|
panel/ACMEDomains.js \
|
||||||
|
panel/EmailRecipientPanel.js \
|
||||||
|
panel/SendmailEditPanel.js \
|
||||||
|
panel/SmtpEditPanel.js \
|
||||||
panel/StatusView.js \
|
panel/StatusView.js \
|
||||||
panel/TfaView.js \
|
panel/TfaView.js \
|
||||||
panel/NotesView.js \
|
panel/NotesView.js \
|
||||||
|
panel/WebhookEditPanel.js \
|
||||||
window/Edit.js \
|
window/Edit.js \
|
||||||
window/PasswordEdit.js \
|
window/PasswordEdit.js \
|
||||||
window/SafeDestroy.js \
|
window/SafeDestroy.js \
|
||||||
@ -78,13 +90,18 @@ JSSRC= \
|
|||||||
window/DiskSmart.js \
|
window/DiskSmart.js \
|
||||||
window/ZFSDetail.js \
|
window/ZFSDetail.js \
|
||||||
window/Certificates.js \
|
window/Certificates.js \
|
||||||
|
window/ConsentModal.js \
|
||||||
window/ACMEAccount.js \
|
window/ACMEAccount.js \
|
||||||
window/ACMEPluginEdit.js \
|
window/ACMEPluginEdit.js \
|
||||||
window/ACMEDomains.js \
|
window/ACMEDomains.js \
|
||||||
|
window/EndpointEditBase.js \
|
||||||
|
window/NotificationMatcherEdit.js \
|
||||||
window/FileBrowser.js \
|
window/FileBrowser.js \
|
||||||
window/AuthEditBase.js \
|
window/AuthEditBase.js \
|
||||||
window/AuthEditOpenId.js \
|
window/AuthEditOpenId.js \
|
||||||
window/AuthEditLDAP.js \
|
window/AuthEditLDAP.js \
|
||||||
|
window/AuthEditAD.js \
|
||||||
|
window/AuthEditSimple.js \
|
||||||
window/TfaWindow.js \
|
window/TfaWindow.js \
|
||||||
window/AddTfaRecovery.js \
|
window/AddTfaRecovery.js \
|
||||||
window/AddTotp.js \
|
window/AddTotp.js \
|
||||||
@ -110,29 +127,34 @@ all: $(SUBDIRS)
|
|||||||
set -e && for i in $(SUBDIRS); do $(MAKE) -C $$i; done
|
set -e && for i in $(SUBDIRS); do $(MAKE) -C $$i; done
|
||||||
|
|
||||||
.lint-incremental: $(JSSRC)
|
.lint-incremental: $(JSSRC)
|
||||||
eslint $?
|
$(ESLINT) $?
|
||||||
touch "$@"
|
touch "$@"
|
||||||
|
|
||||||
.PHONY: lint
|
.PHONY: lint
|
||||||
check: lint
|
check: lint
|
||||||
eslint --strict api-viewer/APIViewer.js
|
$(ESLINT) --strict api-viewer/APIViewer.js
|
||||||
lint: $(JSSRC)
|
lint: $(JSSRC)
|
||||||
eslint --strict $(JSSRC)
|
$(ESLINT) --strict $(JSSRC)
|
||||||
touch ".lint-incremental"
|
touch ".lint-incremental"
|
||||||
|
|
||||||
BUILD_TIME=$(or $(SOURCE_DATE_EPOCH),$(shell date '+%s.%N'))
|
BUILD_TIME=$(or $(SOURCE_DATE_EPOCH),$(shell date '+%s.%N'))
|
||||||
BUILD_VERSION=$(or $(DEB_VERSION),$(shell git rev-parse HEAD),unknown version)
|
BUILD_VERSION=$(or $(DEB_VERSION),$(shell git rev-parse HEAD),unknown version)
|
||||||
proxmoxlib.js: .lint-incremental $(JSSRC)
|
proxmoxlib.js: .lint-incremental $(JSSRC)
|
||||||
# add the version as comment in the file
|
# 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
|
cat $(JSSRC) $(MARKEDJS) >> $@.tmp
|
||||||
mv $@.tmp $@
|
mv $@.tmp $@
|
||||||
|
|
||||||
install: proxmoxlib.js
|
proxmoxlib.min.js: proxmoxlib.js
|
||||||
|
uglifyjs $< -c -m -o $@.tmp
|
||||||
|
mv $@.tmp $@
|
||||||
|
|
||||||
|
install: proxmoxlib.js proxmoxlib.min.js
|
||||||
install -d -m 755 $(WWWBASEDIR)
|
install -d -m 755 $(WWWBASEDIR)
|
||||||
install -m 0644 proxmoxlib.js $(WWWBASEDIR)
|
install -m 0644 proxmoxlib.js proxmoxlib.min.js $(WWWBASEDIR)
|
||||||
set -e && for i in $(SUBDIRS); do $(MAKE) -C $$i $@; done
|
set -e && for i in $(SUBDIRS); do $(MAKE) -C $$i $@; done
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
rm -f proxmoxlib.js
|
$(MAKE) -C proxmox-dark $@
|
||||||
|
rm -f proxmoxlib.js* proxmoxlib.min.js* .lint-incremental
|
||||||
|
@ -24,22 +24,33 @@ Ext.define('Proxmox.Markdown', {
|
|||||||
for (let i=node.attributes.length; i--;) {
|
for (let i=node.attributes.length; i--;) {
|
||||||
const name = node.attributes[i].name;
|
const name = node.attributes[i].name;
|
||||||
const value = node.attributes[i].value;
|
const value = node.attributes[i].value;
|
||||||
|
const canonicalTagName = node.tagName.toLowerCase();
|
||||||
// TODO: we may want to also disallow class and id attrs
|
// TODO: we may want to also disallow class and id attrs
|
||||||
if (
|
if (
|
||||||
!/^(class|id|name|href|src|alt|align|valign|disabled|checked|start|type)$/i.test(name)
|
!/^(class|id|name|href|src|alt|align|valign|disabled|checked|start|type|target)$/i.test(name)
|
||||||
) {
|
) {
|
||||||
node.attributes.removeNamedItem(name);
|
node.attributes.removeNamedItem(name);
|
||||||
} else if ((name === 'href' || name === 'src') && !_isHTTPLike(value)) {
|
} else if ((name === 'href' || name === 'src') && !_isHTTPLike(value)) {
|
||||||
|
let safeURL = false;
|
||||||
try {
|
try {
|
||||||
let url = new URL(value, window.location.origin);
|
let url = new URL(value, window.location.origin);
|
||||||
if (_isHTTPLike(url.protocol) || (node.tagName === 'img' && url.protocol === 'data:')) {
|
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) {
|
||||||
node.attributes[i].value = url.href;
|
node.attributes[i].value = url.href;
|
||||||
} else {
|
} else {
|
||||||
node.attributes.removeNamedItem(name);
|
node.attributes.removeNamedItem(name);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
node.attributes[i].removeNamedItem(name);
|
node.attributes.removeNamedItem(name);
|
||||||
}
|
}
|
||||||
|
} else if (name === 'target' && canonicalTagName !== 'a') {
|
||||||
|
node.attributes.removeNamedItem(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let i=node.childNodes.length; i--;) _sanitize(node.childNodes[i]);
|
for (let i=node.childNodes.length; i--;) _sanitize(node.childNodes[i]);
|
||||||
@ -55,7 +66,7 @@ Ext.define('Proxmox.Markdown', {
|
|||||||
|
|
||||||
parse: function(markdown) {
|
parse: function(markdown) {
|
||||||
/*global marked*/
|
/*global marked*/
|
||||||
let unsafeHTML = marked(markdown);
|
let unsafeHTML = marked.parse(markdown);
|
||||||
|
|
||||||
return `<div class="pmx-md">${this.sanitizeHTML(unsafeHTML)}</div>`;
|
return `<div class="pmx-md">${this.sanitizeHTML(unsafeHTML)}</div>`;
|
||||||
},
|
},
|
||||||
|
@ -4,10 +4,13 @@ Ext.define('Proxmox.Schema', { // a singleton
|
|||||||
authDomains: {
|
authDomains: {
|
||||||
pam: {
|
pam: {
|
||||||
name: 'Linux PAM',
|
name: 'Linux PAM',
|
||||||
|
ipanel: 'pmxAuthSimplePanel',
|
||||||
|
onlineHelp: 'user-realms-pam',
|
||||||
add: false,
|
add: false,
|
||||||
edit: false,
|
edit: true,
|
||||||
pwchange: true,
|
pwchange: true,
|
||||||
sync: false,
|
sync: false,
|
||||||
|
useTypeInUrl: false,
|
||||||
},
|
},
|
||||||
openid: {
|
openid: {
|
||||||
name: gettext('OpenID Connect Server'),
|
name: gettext('OpenID Connect Server'),
|
||||||
@ -18,6 +21,7 @@ Ext.define('Proxmox.Schema', { // a singleton
|
|||||||
pwchange: false,
|
pwchange: false,
|
||||||
sync: false,
|
sync: false,
|
||||||
iconCls: 'pmx-itype-icon-openid-logo',
|
iconCls: 'pmx-itype-icon-openid-logo',
|
||||||
|
useTypeInUrl: true,
|
||||||
},
|
},
|
||||||
ldap: {
|
ldap: {
|
||||||
name: gettext('LDAP Server'),
|
name: gettext('LDAP Server'),
|
||||||
@ -28,6 +32,18 @@ Ext.define('Proxmox.Schema', { // a singleton
|
|||||||
tfa: true,
|
tfa: true,
|
||||||
pwchange: false,
|
pwchange: false,
|
||||||
sync: true,
|
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
|
// to add or change existing for product specific ones
|
||||||
@ -37,6 +53,38 @@ 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: {
|
pxarFileTypes: {
|
||||||
b: { icon: 'cube', label: gettext('Block Device') },
|
b: { icon: 'cube', label: gettext('Block Device') },
|
||||||
c: { icon: 'tty', label: gettext('Character Device') },
|
c: { icon: 'tty', label: gettext('Character Device') },
|
||||||
|
476
src/Toolkit.js
476
src/Toolkit.js
@ -21,7 +21,7 @@ Ext.apply(Ext.form.field.VTypes, {
|
|||||||
IPCIDRAddressMask: /[\d./]/i,
|
IPCIDRAddressMask: /[\d./]/i,
|
||||||
|
|
||||||
IP6Address: function(v) {
|
IP6Address: function(v) {
|
||||||
return Proxmox.Utils.IP6_match.test(v);
|
return Proxmox.Utils.IP6_match.test(v);
|
||||||
},
|
},
|
||||||
IP6AddressText: gettext('Example') + ': 2001:DB8::42',
|
IP6AddressText: gettext('Example') + ': 2001:DB8::42',
|
||||||
IP6AddressMask: /[A-Fa-f0-9:]/,
|
IP6AddressMask: /[A-Fa-f0-9:]/,
|
||||||
@ -42,7 +42,7 @@ Ext.apply(Ext.form.field.VTypes, {
|
|||||||
IP6PrefixLengthMask: /[0-9]/,
|
IP6PrefixLengthMask: /[0-9]/,
|
||||||
|
|
||||||
IP64Address: function(v) {
|
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',
|
IP64AddressText: gettext('Example') + ': 192.168.1.1 2001:DB8::42',
|
||||||
IP64AddressMask: /[A-Fa-f0-9.:]/,
|
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'),
|
MacPrefixText: gettext('Example') + ': 02:8f - ' + gettext('only unicast addresses are allowed'),
|
||||||
|
|
||||||
BridgeName: function(v) {
|
BridgeName: function(v) {
|
||||||
return (/^vmbr\d{1,4}$/).test(v);
|
return (/^[a-zA-Z][a-zA-Z0-9_]{0,9}$/).test(v);
|
||||||
},
|
},
|
||||||
VlanName: function(v) {
|
VlanName: function(v) {
|
||||||
if (Proxmox.Utils.VlanInterface_match.test(v)) {
|
if (Proxmox.Utils.VlanInterface_match.test(v)) {
|
||||||
return true;
|
return true;
|
||||||
} else if (Proxmox.Utils.Vlan_match.test(v)) {
|
} else if (Proxmox.Utils.Vlan_match.test(v)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
BridgeNameText: gettext('Format') + ': vmbr<b>N</b>, where 0 <= <b>N</b> <= 9999',
|
BridgeNameText: gettext('Format') + ': alphanumeric string starting with a character',
|
||||||
|
|
||||||
BondName: function(v) {
|
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',
|
BondNameText: gettext('Format') + ': bond<b>N</b>, where 0 <= <b>N</b> <= 9999',
|
||||||
|
|
||||||
InterfaceName: function(v) {
|
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 />" +
|
InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'<br />" +
|
||||||
gettext("Minimum characters") + ": 2<br />" +
|
gettext("Minimum characters") + ": 2<br />" +
|
||||||
@ -102,7 +102,7 @@ Ext.apply(Ext.form.field.VTypes, {
|
|||||||
gettext("Must start with") + ": 'a-z'",
|
gettext("Must start with") + ": 'a-z'",
|
||||||
|
|
||||||
StorageId: function(v) {
|
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 />" +
|
StorageIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '-', '_', '.'<br />" +
|
||||||
gettext("Minimum characters") + ": 2<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 />",
|
gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'<br />",
|
||||||
|
|
||||||
ConfigId: function(v) {
|
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 />" +
|
ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'<br />" +
|
||||||
gettext("Minimum characters") + ": 2<br />" +
|
gettext("Minimum characters") + ": 2<br />" +
|
||||||
gettext("Must start with") + ": " + gettext("letter"),
|
gettext("Must start with") + ": " + gettext("letter"),
|
||||||
|
|
||||||
HttpProxy: function(v) {
|
HttpProxy: function(v) {
|
||||||
return (/^http:\/\/.*$/).test(v);
|
return (/^http:\/\/.*$/).test(v);
|
||||||
},
|
},
|
||||||
HttpProxyText: gettext('Example') + ": http://username:password@host:port/",
|
HttpProxyText: gettext('Example') + ": http://username:password@host:port/",
|
||||||
|
|
||||||
@ -129,16 +129,16 @@ Ext.apply(Ext.form.field.VTypes, {
|
|||||||
DnsName: function(v) {
|
DnsName: function(v) {
|
||||||
return Proxmox.Utils.DnsName_match.test(v);
|
return Proxmox.Utils.DnsName_match.test(v);
|
||||||
},
|
},
|
||||||
DnsNameText: gettext('This is not a valid DNS name'),
|
DnsNameText: gettext('This is not a valid hostname'),
|
||||||
|
|
||||||
DnsNameOrWildcard: function(v) {
|
DnsNameOrWildcard: function(v) {
|
||||||
return Proxmox.Utils.DnsName_or_Wildcard_match.test(v);
|
return Proxmox.Utils.DnsName_or_Wildcard_match.test(v);
|
||||||
},
|
},
|
||||||
DnsNameOrWildcardText: gettext('This is not a valid DNS name'),
|
DnsNameOrWildcardText: gettext('This is not a valid hostname'),
|
||||||
|
|
||||||
// email regex used by pve-common
|
// email regex used by pve-common
|
||||||
proxmoxMail: function(v) {
|
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",
|
proxmoxMailText: gettext('Example') + ": user@example.com",
|
||||||
|
|
||||||
@ -179,11 +179,11 @@ Ext.apply(Ext.form.field.VTypes, {
|
|||||||
HostListText: gettext('Not a valid list of hosts'),
|
HostListText: gettext('Not a valid list of hosts'),
|
||||||
|
|
||||||
password: function(val, field) {
|
password: function(val, field) {
|
||||||
if (field.initialPassField) {
|
if (field.initialPassField) {
|
||||||
let pwd = field.up('form').down(`[name=${field.initialPassField}]`);
|
let pwd = field.up('form').down(`[name=${field.initialPassField}]`);
|
||||||
return val === pwd.getValue();
|
return val === pwd.getValue();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
passwordText: gettext('Passwords do not match'),
|
passwordText: gettext('Passwords do not match'),
|
||||||
@ -216,30 +216,30 @@ Ext.define('Proxmox.UnderlayPool', {
|
|||||||
override: 'Ext.dom.UnderlayPool',
|
override: 'Ext.dom.UnderlayPool',
|
||||||
|
|
||||||
checkOut: function() {
|
checkOut: function() {
|
||||||
let cache = this.cache,
|
let cache = this.cache,
|
||||||
len = cache.length,
|
len = cache.length,
|
||||||
el;
|
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--) {
|
while (len--) {
|
||||||
if (cache[len].destroyed) {
|
if (cache[len].destroyed) {
|
||||||
cache.splice(len, 1);
|
cache.splice(len, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// end do cleanup
|
// end do cleanup
|
||||||
|
|
||||||
el = cache.shift();
|
el = cache.shift();
|
||||||
|
|
||||||
if (!el) {
|
if (!el) {
|
||||||
el = Ext.Element.create(this.elementConfig);
|
el = Ext.Element.create(this.elementConfig);
|
||||||
el.setVisibilityMode(2);
|
el.setVisibilityMode(2);
|
||||||
//<debug>
|
//<debug>
|
||||||
// tell the spec runner to ignore this element when checking if the dom is clean
|
// tell the spec runner to ignore this element when checking if the dom is clean
|
||||||
el.dom.setAttribute('data-sticky', true);
|
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 ]
|
// [ 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
|
// records can be an index, a record or an array of records
|
||||||
doDeselect: function(records, suppressEvent) {
|
doDeselect: function(records, suppressEvent) {
|
||||||
var me = this,
|
var me = this,
|
||||||
selected = me.selected,
|
selected = me.selected,
|
||||||
i = 0,
|
i = 0,
|
||||||
len, record,
|
len, record,
|
||||||
commit;
|
commit;
|
||||||
if (me.locked || !me.store) {
|
if (me.locked || !me.store) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (typeof records === "number") {
|
if (typeof records === "number") {
|
||||||
// No matching record, jump out
|
// No matching record, jump out
|
||||||
record = me.store.getAt(records);
|
record = me.store.getAt(records);
|
||||||
if (!record) {
|
if (!record) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
records = [
|
records = [
|
||||||
record,
|
record,
|
||||||
];
|
];
|
||||||
} else if (!Ext.isArray(records)) {
|
} else if (!Ext.isArray(records)) {
|
||||||
records = [
|
records = [
|
||||||
records,
|
records,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
// [P] a beforedeselection, triggered by me.onSelectChange below, can block removal by
|
// [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,
|
// 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
|
// 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 = [];
|
let removalBlocked = [];
|
||||||
len = records.length;
|
len = records.length;
|
||||||
me.suspendChanges();
|
me.suspendChanges();
|
||||||
for (; i < len; i++) {
|
for (; i < len; i++) {
|
||||||
record = records[i];
|
record = records[i];
|
||||||
if (me.isSelected(record)) {
|
if (me.isSelected(record)) {
|
||||||
committed = false;
|
committed = false;
|
||||||
me.onSelectChange(record, false, suppressEvent, commit);
|
me.onSelectChange(record, false, suppressEvent, commit);
|
||||||
if (!committed) {
|
if (!committed) {
|
||||||
removalBlocked.push(record);
|
removalBlocked.push(record);
|
||||||
}
|
}
|
||||||
if (me.destroyed) {
|
if (me.destroyed) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (removalBlocked.length > 0) {
|
if (removalBlocked.length > 0) {
|
||||||
records.remove(removalBlocked);
|
records.remove(removalBlocked);
|
||||||
}
|
}
|
||||||
selected.remove(records); // [P] FAST(er)
|
selected.remove(records); // [P] FAST(er)
|
||||||
me.lastSelected = selected.last();
|
me.lastSelected = selected.last();
|
||||||
me.resumeChanges();
|
me.resumeChanges();
|
||||||
// fire selchange if there was a change and there is no suppressEvent flag
|
// fire selchange if there was a change and there is no suppressEvent flag
|
||||||
me.maybeFireSelectionChange(records.length > 0 && !suppressEvent);
|
me.maybeFireSelectionChange(records.length > 0 && !suppressEvent);
|
||||||
return records.length;
|
return records.length;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
doMultiSelect: function(records, keepExisting, suppressEvent) {
|
doMultiSelect: function(records, keepExisting, suppressEvent) {
|
||||||
var me = this,
|
var me = this,
|
||||||
selected = me.selected,
|
selected = me.selected,
|
||||||
change = false,
|
change = false,
|
||||||
result, i, len, record, commit;
|
result, i, len, record, commit;
|
||||||
|
|
||||||
if (me.locked) {
|
if (me.locked) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
records = !Ext.isArray(records) ? [records] : records;
|
records = !Ext.isArray(records) ? [records] : records;
|
||||||
len = records.length;
|
len = records.length;
|
||||||
if (!keepExisting && selected.getCount() > 0) {
|
if (!keepExisting && selected.getCount() > 0) {
|
||||||
result = me.deselectDuringSelect(records, suppressEvent);
|
result = me.deselectDuringSelect(records, suppressEvent);
|
||||||
if (me.destroyed) {
|
if (me.destroyed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result[0]) {
|
if (result[0]) {
|
||||||
// We had a failure during selection, so jump out
|
// We had a failure during selection, so jump out
|
||||||
// Fire selection change if we did deselect anything
|
// Fire selection change if we did deselect anything
|
||||||
me.maybeFireSelectionChange(result[1] > 0 && !suppressEvent);
|
me.maybeFireSelectionChange(result[1] > 0 && !suppressEvent);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// Means something has been deselected, so we've had a change
|
// Means something has been deselected, so we've had a change
|
||||||
change = result[1] > 0;
|
change = result[1] > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let gotBlocked, blockedRecords = [];
|
let gotBlocked, blockedRecords = [];
|
||||||
commit = function() {
|
commit = function() {
|
||||||
if (!selected.getCount()) {
|
if (!selected.getCount()) {
|
||||||
me.selectionStart = record;
|
me.selectionStart = record;
|
||||||
}
|
}
|
||||||
gotBlocked = false;
|
gotBlocked = false;
|
||||||
change = true;
|
change = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (i = 0; i < len; i++) {
|
for (i = 0; i < len; i++) {
|
||||||
record = records[i];
|
record = records[i];
|
||||||
if (me.isSelected(record)) {
|
if (me.isSelected(record)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
gotBlocked = true;
|
gotBlocked = true;
|
||||||
me.onSelectChange(record, true, suppressEvent, commit);
|
me.onSelectChange(record, true, suppressEvent, commit);
|
||||||
if (me.destroyed) {
|
if (me.destroyed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (gotBlocked) {
|
if (gotBlocked) {
|
||||||
blockedRecords.push(record);
|
blockedRecords.push(record);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (blockedRecords.length > 0) {
|
if (blockedRecords.length > 0) {
|
||||||
records.remove(blockedRecords);
|
records.remove(blockedRecords);
|
||||||
}
|
}
|
||||||
selected.add(records);
|
selected.add(records);
|
||||||
me.lastSelected = record;
|
me.lastSelected = record;
|
||||||
|
|
||||||
// fire selchange if there was a change and there is no suppressEvent flag
|
// fire selchange if there was a change and there is no suppressEvent flag
|
||||||
me.maybeFireSelectionChange(change && !suppressEvent);
|
me.maybeFireSelectionChange(change && !suppressEvent);
|
||||||
},
|
},
|
||||||
deselectDuringSelect: function(toSelect, suppressEvent) {
|
deselectDuringSelect: function(toSelect, suppressEvent) {
|
||||||
var me = this,
|
var me = this,
|
||||||
selected = me.selected.getRange(),
|
selected = me.selected.getRange(),
|
||||||
changed = 0,
|
changed = 0,
|
||||||
failed = false;
|
failed = false;
|
||||||
// Prevent selection change events from firing, will happen during select
|
// Prevent selection change events from firing, will happen during select
|
||||||
me.suspendChanges();
|
me.suspendChanges();
|
||||||
me.deselectingDuringSelect = true;
|
me.deselectingDuringSelect = true;
|
||||||
let toDeselect = selected.filter(item => !Ext.Array.contains(toSelect, item));
|
let toDeselect = selected.filter(item => !Ext.Array.contains(toSelect, item));
|
||||||
if (toDeselect.length > 0) {
|
if (toDeselect.length > 0) {
|
||||||
changed = me.doDeselect(toDeselect, suppressEvent);
|
changed = me.doDeselect(toDeselect, suppressEvent);
|
||||||
if (!changed) {
|
if (!changed) {
|
||||||
failed = true;
|
failed = true;
|
||||||
}
|
}
|
||||||
if (me.destroyed) {
|
if (me.destroyed) {
|
||||||
failed = true;
|
failed = true;
|
||||||
changed = 0;
|
changed = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
me.deselectingDuringSelect = false;
|
me.deselectingDuringSelect = false;
|
||||||
me.resumeChanges();
|
me.resumeChanges();
|
||||||
return [
|
return [
|
||||||
failed,
|
failed,
|
||||||
changed,
|
changed,
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -678,11 +678,11 @@ Ext.define('Proxmox.view.DragZone', {
|
|||||||
override: 'Ext.view.DragZone',
|
override: 'Ext.view.DragZone',
|
||||||
|
|
||||||
onItemMouseDown: function(view, record, item, index, e) {
|
onItemMouseDown: function(view, record, item, index, e) {
|
||||||
// Ignore touchstart.
|
// Ignore touchstart.
|
||||||
// For touch events, we use longpress.
|
// For touch events, we use longpress.
|
||||||
if (e.pointerType !== 'touch') {
|
if (e.pointerType !== 'touch') {
|
||||||
this.onTriggerGesture(view, record, item, index, e);
|
this.onTriggerGesture(view, record, item, index, e);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -702,6 +702,39 @@ 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
|
// 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
|
// since Ext.Msg is an object and not a prototype, we need to override it
|
||||||
// after the framework has been initiated
|
// after the framework has been initiated
|
||||||
@ -723,6 +756,11 @@ 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', {
|
Ext.define('Ext.ux.IFrame', {
|
||||||
extend: 'Ext.Component',
|
extend: 'Ext.Component',
|
||||||
|
|
||||||
@ -733,154 +771,84 @@ Ext.define('Ext.ux.IFrame', {
|
|||||||
src: 'about:blank',
|
src: 'about:blank',
|
||||||
|
|
||||||
renderTpl: [
|
renderTpl: [
|
||||||
'<iframe src="{src}" id="{id}-iframeEl" data-ref="iframeEl" name="{frameName}" width="100%" height="100%" frameborder="0" allowfullscreen="true"></iframe>',
|
// 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>',
|
||||||
],
|
],
|
||||||
|
|
||||||
childEls: ['iframeEl'],
|
childEls: ['iframeEl'],
|
||||||
|
|
||||||
initComponent: function() {
|
initComponent: function() {
|
||||||
this.callParent();
|
this.callParent();
|
||||||
|
|
||||||
this.frameName = this.frameName || this.id + '-frame';
|
this.frameName = this.frameName || this.id + '-frame';
|
||||||
},
|
},
|
||||||
|
|
||||||
initEvents: function() {
|
initEvents: function() {
|
||||||
let me = this;
|
let me = this;
|
||||||
me.callParent();
|
|
||||||
me.iframeEl.on('load', me.onLoad, me);
|
me.callParent();
|
||||||
|
me.iframeEl.on('load', me.onLoad, me);
|
||||||
},
|
},
|
||||||
|
|
||||||
initRenderData: function() {
|
initRenderData: function() {
|
||||||
return Ext.apply(this.callParent(), {
|
return Ext.apply(this.callParent(), {
|
||||||
src: this.src,
|
src: this.src,
|
||||||
frameName: this.frameName,
|
frameName: this.frameName,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
getBody: function() {
|
getBody: function() {
|
||||||
let doc = this.getDoc();
|
let doc = this.getDoc();
|
||||||
return doc.body || doc.documentElement;
|
|
||||||
|
return doc.body || doc.documentElement;
|
||||||
},
|
},
|
||||||
|
|
||||||
getDoc: function() {
|
getDoc: function() {
|
||||||
try {
|
try {
|
||||||
return this.getWin().document;
|
return this.getWin().document;
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getWin: function() {
|
getWin: function() {
|
||||||
let me = this,
|
let me = this,
|
||||||
name = me.frameName,
|
name = me.frameName,
|
||||||
win = Ext.isIE
|
win = Ext.isIE ? me.iframeEl.dom.contentWindow : window.frames[name];
|
||||||
? me.iframeEl.dom.contentWindow
|
|
||||||
: window.frames[name];
|
return win;
|
||||||
return win;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getFrame: function() {
|
getFrame: function() {
|
||||||
let me = this;
|
let me = this;
|
||||||
return me.iframeEl.dom;
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeDestroy: function() {
|
return me.iframeEl.dom;
|
||||||
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() {
|
onLoad: function() {
|
||||||
let me = this,
|
let me = this,
|
||||||
doc = me.getDoc(),
|
doc = me.getDoc();
|
||||||
fn = me.onRelayedEvent;
|
|
||||||
|
|
||||||
if (doc) {
|
if (doc) {
|
||||||
try {
|
this.el.unmask();
|
||||||
// These events need to be relayed from the inner document (where they stop
|
this.fireEvent('load', this);
|
||||||
// bubbling) up to the outer document. This has to be done at the DOM level so
|
} else if (me.src) {
|
||||||
// the event reaches listeners on elements like the document body. The effected
|
this.el.unmask();
|
||||||
// mechanisms that depend on this bubbling behavior are listed to the right
|
this.fireEvent('error', this);
|
||||||
// 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) {
|
load: function(src) {
|
||||||
let me = this,
|
let me = this,
|
||||||
text = me.loadMask,
|
text = me.loadMask,
|
||||||
frame = me.getFrame();
|
frame = me.getFrame();
|
||||||
|
|
||||||
if (me.fireEvent('beforeload', me, src) !== false) {
|
if (me.fireEvent('beforeload', me, src) !== false) {
|
||||||
if (text && me.el) {
|
if (text && me.el) {
|
||||||
me.el.mask(text);
|
me.el.mask(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
frame.src = me.src = src || me.src;
|
frame.src = me.src = src || me.src;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
135
src/Utils.js
135
src/Utils.js
@ -64,6 +64,7 @@ utilities: {
|
|||||||
|
|
||||||
language_map: { //language map is sorted alphabetically by iso 639-1
|
language_map: { //language map is sorted alphabetically by iso 639-1
|
||||||
ar: `العربية - ${gettext("Arabic")}`,
|
ar: `العربية - ${gettext("Arabic")}`,
|
||||||
|
bg: `Български - ${gettext("Bulgarian")}`,
|
||||||
ca: `Català - ${gettext("Catalan")}`,
|
ca: `Català - ${gettext("Catalan")}`,
|
||||||
da: `Dansk - ${gettext("Danish")}`,
|
da: `Dansk - ${gettext("Danish")}`,
|
||||||
de: `Deutsch - ${gettext("German")}`,
|
de: `Deutsch - ${gettext("German")}`,
|
||||||
@ -72,10 +73,12 @@ utilities: {
|
|||||||
eu: `Euskera (Basque) - ${gettext("Euskera (Basque)")}`,
|
eu: `Euskera (Basque) - ${gettext("Euskera (Basque)")}`,
|
||||||
fa: `فارسی - ${gettext("Persian (Farsi)")}`,
|
fa: `فارسی - ${gettext("Persian (Farsi)")}`,
|
||||||
fr: `Français - ${gettext("French")}`,
|
fr: `Français - ${gettext("French")}`,
|
||||||
|
hr: `Hrvatski - ${gettext("Croatian")}`,
|
||||||
he: `עברית - ${gettext("Hebrew")}`,
|
he: `עברית - ${gettext("Hebrew")}`,
|
||||||
it: `Italiano - ${gettext("Italian")}`,
|
it: `Italiano - ${gettext("Italian")}`,
|
||||||
ja: `日本語 - ${gettext("Japanese")}`,
|
ja: `日本語 - ${gettext("Japanese")}`,
|
||||||
kr: `한국어 - ${gettext("Korean")}`,
|
ka: `ქართული - ${gettext("Georgian")}`,
|
||||||
|
ko: `한국어 - ${gettext("Korean")}`,
|
||||||
nb: `Bokmål - ${gettext("Norwegian (Bokmal)")}`,
|
nb: `Bokmål - ${gettext("Norwegian (Bokmal)")}`,
|
||||||
nl: `Nederlands - ${gettext("Dutch")}`,
|
nl: `Nederlands - ${gettext("Dutch")}`,
|
||||||
nn: `Nynorsk - ${gettext("Norwegian (Nynorsk)")}`,
|
nn: `Nynorsk - ${gettext("Norwegian (Nynorsk)")}`,
|
||||||
@ -85,6 +88,7 @@ utilities: {
|
|||||||
sl: `Slovenščina - ${gettext("Slovenian")}`,
|
sl: `Slovenščina - ${gettext("Slovenian")}`,
|
||||||
sv: `Svenska - ${gettext("Swedish")}`,
|
sv: `Svenska - ${gettext("Swedish")}`,
|
||||||
tr: `Türkçe - ${gettext("Turkish")}`,
|
tr: `Türkçe - ${gettext("Turkish")}`,
|
||||||
|
ukr: `Українська - ${gettext("Ukrainian")}`,
|
||||||
zh_CN: `中文(简体)- ${gettext("Chinese (Simplified)")}`,
|
zh_CN: `中文(简体)- ${gettext("Chinese (Simplified)")}`,
|
||||||
zh_TW: `中文(繁體)- ${gettext("Chinese (Traditional)")}`,
|
zh_TW: `中文(繁體)- ${gettext("Chinese (Traditional)")}`,
|
||||||
},
|
},
|
||||||
@ -93,6 +97,9 @@ utilities: {
|
|||||||
if (!value || value === '__default__') {
|
if (!value || value === '__default__') {
|
||||||
return Proxmox.Utils.defaultText + ' (English)';
|
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];
|
let text = Proxmox.Utils.language_map[value];
|
||||||
if (text) {
|
if (text) {
|
||||||
return text + ' (' + value + ')';
|
return text + ' (' + value + ')';
|
||||||
@ -149,8 +156,11 @@ utilities: {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getNoSubKeyHtml: function(url) {
|
getNoSubKeyHtml: function(url) {
|
||||||
// url http://www.proxmox.com/products/proxmox-ve/subscription-service-plans
|
let html_url = Ext.String.format('<a target="_blank" href="{0}">www.proxmox.com</a>', url || 'https://www.proxmox.com');
|
||||||
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');
|
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,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
format_boolean_with_default: function(value) {
|
format_boolean_with_default: function(value) {
|
||||||
@ -308,7 +318,7 @@ utilities: {
|
|||||||
// that way the cookie gets deleted after the browser window is closed
|
// that way the cookie gets deleted after the browser window is closed
|
||||||
if (data.ticket) {
|
if (data.ticket) {
|
||||||
Proxmox.CSRFPreventionToken = data.CSRFPreventionToken;
|
Proxmox.CSRFPreventionToken = data.CSRFPreventionToken;
|
||||||
Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, data.ticket, null, '/', null, true);
|
Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, data.ticket, null, '/', null, true, "lax");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.token) {
|
if (data.token) {
|
||||||
@ -334,7 +344,7 @@ utilities: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// ExtJS clear is basically the same, but browser may complain if any cookie isn't "secure"
|
// 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);
|
Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, "", new Date(0), null, null, true, "lax");
|
||||||
window.localStorage.removeItem("ProxmoxUser");
|
window.localStorage.removeItem("ProxmoxUser");
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -453,6 +463,12 @@ utilities: {
|
|||||||
newopts.url = '/api2/extjs' + newopts.url;
|
newopts.url = '/api2/extjs' + newopts.url;
|
||||||
}
|
}
|
||||||
delete newopts.callback;
|
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) {
|
let createWrapper = function(successFn, callbackFn, failureFn) {
|
||||||
Ext.apply(newopts, {
|
Ext.apply(newopts, {
|
||||||
@ -461,7 +477,7 @@ utilities: {
|
|||||||
if (Proxmox.Utils.toolkit === 'touch') {
|
if (Proxmox.Utils.toolkit === 'touch') {
|
||||||
options.waitMsgTarget.setMasked(false);
|
options.waitMsgTarget.setMasked(false);
|
||||||
} else {
|
} else {
|
||||||
options.waitMsgTarget.setLoading(false);
|
unmask(options.waitMsgTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let result = Ext.decode(response.responseText);
|
let result = Ext.decode(response.responseText);
|
||||||
@ -483,7 +499,7 @@ utilities: {
|
|||||||
if (Proxmox.Utils.toolkit === 'touch') {
|
if (Proxmox.Utils.toolkit === 'touch') {
|
||||||
options.waitMsgTarget.setMasked(false);
|
options.waitMsgTarget.setMasked(false);
|
||||||
} else {
|
} else {
|
||||||
options.waitMsgTarget.setLoading(false);
|
unmask(options.waitMsgTarget);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
response.result = {};
|
response.result = {};
|
||||||
@ -513,9 +529,16 @@ utilities: {
|
|||||||
if (target) {
|
if (target) {
|
||||||
if (Proxmox.Utils.toolkit === 'touch') {
|
if (Proxmox.Utils.toolkit === 'touch') {
|
||||||
target.setMasked({ xtype: 'loadmask', message: newopts.waitMsg });
|
target.setMasked({ xtype: 'loadmask', message: newopts.waitMsg });
|
||||||
} else {
|
} else if (target.rendered) {
|
||||||
// Note: ExtJS bug - this does not work when component is not rendered
|
target.waitMsgTargetCount = (target.waitMsgTargetCount ?? 0) + 1;
|
||||||
target.setLoading(newopts.waitMsg);
|
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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ext.Ajax.request(newopts);
|
Ext.Ajax.request(newopts);
|
||||||
@ -525,12 +548,7 @@ utilities: {
|
|||||||
// Proxmox.Async.api2({
|
// Proxmox.Async.api2({
|
||||||
// ...
|
// ...
|
||||||
// }).catch(Proxmox.Utils.alertResponseFailure);
|
// }).catch(Proxmox.Utils.alertResponseFailure);
|
||||||
alertResponseFailure: (response) => {
|
alertResponseFailure: res => Ext.Msg.alert(gettext('Error'), res.htmlStatus || res.result.message),
|
||||||
Ext.Msg.alert(
|
|
||||||
gettext('Error'),
|
|
||||||
response.htmlStatus || response.result.message,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
checked_command: function(orig_cmd) {
|
checked_command: function(orig_cmd) {
|
||||||
Proxmox.Utils.API2Request(
|
Proxmox.Utils.API2Request(
|
||||||
@ -565,7 +583,7 @@ utilities: {
|
|||||||
},
|
},
|
||||||
|
|
||||||
assemble_field_data: function(values, data) {
|
assemble_field_data: function(values, data) {
|
||||||
if (!Ext.isObject(data)) {
|
if (!Ext.isObject(data)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Ext.Object.each(data, function(name, val) {
|
Ext.Object.each(data, function(name, val) {
|
||||||
@ -647,6 +665,37 @@ utilities: {
|
|||||||
Proxmox.Utils.unknownText;
|
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
|
// NOTE: only add general, product agnostic, ones here! Else use override helper in product repos
|
||||||
task_desc_table: {
|
task_desc_table: {
|
||||||
aptupdate: ['', gettext('Update package database')],
|
aptupdate: ['', gettext('Update package database')],
|
||||||
@ -688,21 +737,23 @@ utilities: {
|
|||||||
},
|
},
|
||||||
|
|
||||||
format_size: function(size, useSI) {
|
format_size: function(size, useSI) {
|
||||||
let units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
|
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 order = 0;
|
let order = 0;
|
||||||
|
let commaDigits = 2;
|
||||||
const baseValue = useSI ? 1000 : 1024;
|
const baseValue = useSI ? 1000 : 1024;
|
||||||
while (size >= baseValue && order < units.length) {
|
while (size >= baseValue && order < unitsSI.length) {
|
||||||
size = size / baseValue;
|
size = size / baseValue;
|
||||||
order++;
|
order++;
|
||||||
}
|
}
|
||||||
|
|
||||||
let unit = units[order], commaDigits = 2;
|
let unit = useSI ? unitsSI[order] : unitsIEC[order];
|
||||||
if (order === 0) {
|
if (order === 0) {
|
||||||
commaDigits = 0;
|
commaDigits = 0;
|
||||||
} else if (!useSI) {
|
|
||||||
unit += 'i';
|
|
||||||
}
|
}
|
||||||
return `${size.toFixed(commaDigits)} ${unit}B`;
|
return `${size.toFixed(commaDigits)} ${unit}`;
|
||||||
},
|
},
|
||||||
|
|
||||||
SizeUnits: {
|
SizeUnits: {
|
||||||
@ -877,7 +928,7 @@ utilities: {
|
|||||||
let parsed = Proxmox.Utils.parse_task_status(status);
|
let parsed = Proxmox.Utils.parse_task_status(status);
|
||||||
switch (parsed) {
|
switch (parsed) {
|
||||||
case 'unknown': return Proxmox.Utils.unknownText;
|
case 'unknown': return Proxmox.Utils.unknownText;
|
||||||
case 'error': return Proxmox.Utils.errorText + ': ' + status;
|
case 'error': return Proxmox.Utils.errorText + ': ' + Ext.htmlEncode(status);
|
||||||
case 'warning': return status.replace('WARNINGS', Proxmox.Utils.warningsText);
|
case 'warning': return status.replace('WARNINGS', Proxmox.Utils.warningsText);
|
||||||
case 'ok': // fall-through
|
case 'ok': // fall-through
|
||||||
default: return status;
|
default: return status;
|
||||||
@ -1306,6 +1357,24 @@ 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) {
|
stringToRGB: function(string) {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
if (!string) {
|
if (!string) {
|
||||||
@ -1456,6 +1525,26 @@ utilities: {
|
|||||||
me.IP6_dotnotation_match = new RegExp("^(" + IPV6_REGEXP + ")(?:\\.(\\d+))?$");
|
me.IP6_dotnotation_match = new RegExp("^(" + IPV6_REGEXP + ")(?:\\.(\\d+))?$");
|
||||||
me.Vlan_match = /^vlan(\d+)/;
|
me.Vlan_match = /^vlan(\d+)/;
|
||||||
me.VlanInterface_match = /(\w+)\.(\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._\\-]*)$/;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ Ext.onReady(function() {
|
|||||||
'name', 'type', 'typetext', 'description', 'verbose_description',
|
'name', 'type', 'typetext', 'description', 'verbose_description',
|
||||||
'enum', 'minimum', 'maximum', 'minLength', 'maxLength',
|
'enum', 'minimum', 'maximum', 'minLength', 'maxLength',
|
||||||
'pattern', 'title', 'requires', 'format', 'default',
|
'pattern', 'title', 'requires', 'format', 'default',
|
||||||
'disallow', 'extends', 'links',
|
'disallow', 'extends', 'links', 'instance-types',
|
||||||
{
|
{
|
||||||
name: 'optional',
|
name: 'optional',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@ -214,6 +214,10 @@ Ext.onReady(function() {
|
|||||||
},
|
},
|
||||||
groupField: 'optional',
|
groupField: 'optional',
|
||||||
sorters: [
|
sorters: [
|
||||||
|
{
|
||||||
|
property: 'instance-types',
|
||||||
|
direction: 'ASC',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
property: 'name',
|
property: 'name',
|
||||||
direction: 'ASC',
|
direction: 'ASC',
|
||||||
@ -221,9 +225,27 @@ Ext.onReady(function() {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let has_type_properties = false;
|
||||||
|
|
||||||
Ext.Object.each(info.parameters.properties, function(name, pdef) {
|
Ext.Object.each(info.parameters.properties, function(name, pdef) {
|
||||||
pdef.name = name;
|
if (pdef.oneOf) {
|
||||||
pstore.add(pdef);
|
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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
pstore.sort();
|
pstore.sort();
|
||||||
@ -255,6 +277,12 @@ Ext.onReady(function() {
|
|||||||
renderer: render_type,
|
renderer: render_type,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
header: 'For Types',
|
||||||
|
dataIndex: 'instance-types',
|
||||||
|
hidden: !has_type_properties,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: 'Default',
|
header: 'Default',
|
||||||
dataIndex: 'default',
|
dataIndex: 'default',
|
||||||
|
@ -110,6 +110,7 @@ Ext.define('Proxmox.button.StdRemoveButton', {
|
|||||||
|
|
||||||
config: {
|
config: {
|
||||||
baseurl: undefined,
|
baseurl: undefined,
|
||||||
|
customConfirmationMessage: undefined,
|
||||||
},
|
},
|
||||||
|
|
||||||
getUrl: function(rec) {
|
getUrl: function(rec) {
|
||||||
@ -133,7 +134,14 @@ Ext.define('Proxmox.button.StdRemoveButton', {
|
|||||||
let me = this;
|
let me = this;
|
||||||
|
|
||||||
let name = me.getRecordName(rec);
|
let name = me.getRecordName(rec);
|
||||||
return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), `'${name}'`);
|
|
||||||
|
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}'`));
|
||||||
},
|
},
|
||||||
|
|
||||||
handler: function(btn, event, rec) {
|
handler: function(btn, event, rec) {
|
||||||
|
@ -15,7 +15,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.proxmox-tags-full .x-grid-cell-inner-treecolumn .proxmox-tag-light,
|
.proxmox-tags-full .x-grid-cell-inner-treecolumn .proxmox-tag-light,
|
||||||
.proxmox-tags-full .x-grid-cell-inner-treecolumn .proxmox-tag-dark {
|
.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 {
|
||||||
display: inherit;
|
display: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -25,8 +27,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.proxmox-tags-circle .proxmox-tag-light,
|
.proxmox-tags-circle :not(span.proxmox-tags-full) > .proxmox-tag-light,
|
||||||
.proxmox-tags-circle .proxmox-tag-dark {
|
.proxmox-tags-circle :not(span.proxmox-tags-full) > .proxmox-tag-dark,
|
||||||
|
.proxmox-tags-circle > .proxmox-tag-light,
|
||||||
|
.proxmox-tags-circle > .proxmox-tag-dark {
|
||||||
margin: 0px 1px;
|
margin: 0px 1px;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
@ -38,13 +42,17 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.proxmox-tags-none .proxmox-tag-light,
|
.proxmox-tags-none :not(span.proxmox-tags-full) > .proxmox-tag-light,
|
||||||
.proxmox-tags-none .proxmox-tag-dark {
|
.proxmox-tags-none :not(span.proxmox-tags-full) > .proxmox-tag-dark,
|
||||||
|
.proxmox-tags-none > .proxmox-tag-light,
|
||||||
|
.proxmox-tags-none > .proxmox-tag-dark {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.proxmox-tags-dense .proxmox-tag-light,
|
.proxmox-tags-dense :not(span.proxmox-tags-full) > .proxmox-tag-light,
|
||||||
.proxmox-tags-dense .proxmox-tag-dark {
|
.proxmox-tags-dense :not(span.proxmox-tags-full) > .proxmox-tag-dark,
|
||||||
|
.proxmox-tags-dense > .proxmox-tag-light,
|
||||||
|
.proxmox-tags-dense > .proxmox-tag-dark {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
margin-right: 1px;
|
margin-right: 1px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -66,6 +74,9 @@
|
|||||||
.x-mask-msg-text {
|
.x-mask-msg-text {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
.x-window-header-default-top .x-mask {
|
||||||
|
background-color: #f5f5f5B4; /* ~ 0.7 opacity */
|
||||||
|
}
|
||||||
|
|
||||||
.proxmox-disabled-row, .proxmox-disabled-row td {
|
.proxmox-disabled-row, .proxmox-disabled-row td {
|
||||||
/*color: #a0a0a0;*/
|
/*color: #a0a0a0;*/
|
||||||
@ -113,12 +124,14 @@
|
|||||||
color: #3892d4;
|
color: #3892d4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pwt-eol-icon {
|
.eol-notice a:visited {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.eol-notice > i.fa {
|
||||||
position: relative;
|
position: relative;
|
||||||
float: left;
|
float: left;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
font-size: 1.3em;
|
font-size: 1.3em;
|
||||||
color: #FF6C59;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* reduce chart legend space usage to something more sane */
|
/* reduce chart legend space usage to something more sane */
|
||||||
|
29
src/data/model/NotificationConfig.js
Normal file
29
src/data/model/NotificationConfig.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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',
|
||||||
|
});
|
@ -32,6 +32,9 @@ Ext.define('Proxmox.form.ComboGrid', {
|
|||||||
notFoundIsValid: false,
|
notFoundIsValid: false,
|
||||||
deleteEmpty: false,
|
deleteEmpty: false,
|
||||||
errorHeight: 100,
|
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.
|
// needed to trigger onKeyUp etc.
|
||||||
@ -54,7 +57,7 @@ Ext.define('Proxmox.form.ComboGrid', {
|
|||||||
setValue: function(value) {
|
setValue: function(value) {
|
||||||
let me = this;
|
let me = this;
|
||||||
let empty = Ext.isArray(value) ? !value.length : !value;
|
let empty = Ext.isArray(value) ? !value.length : !value;
|
||||||
me.triggers.clear.setVisible(!empty && me.allowBlank);
|
me.triggers.clear.setVisible(!empty && (me.allowBlank || me.showClearTrigger));
|
||||||
return me.callParent([value]);
|
return me.callParent([value]);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -400,7 +403,7 @@ Ext.define('Proxmox.form.ComboGrid', {
|
|||||||
matchFieldWidth: false,
|
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 });
|
Ext.applyIf(me.listConfig, { width: 400 });
|
||||||
|
|
||||||
|
@ -1,151 +1,169 @@
|
|||||||
Ext.define('Proxmox.DateTimeField', {
|
Ext.define('Proxmox.DateTimeField', {
|
||||||
extend: 'Ext.form.FieldContainer',
|
extend: 'Ext.form.FieldContainer',
|
||||||
xtype: 'promxoxDateTimeField',
|
// FIXME: remove once all use sites upgraded (with versioned depends on new WTK!)
|
||||||
|
alias: ['widget.promxoxDateTimeField'],
|
||||||
|
xtype: 'proxmoxDateTimeField',
|
||||||
|
|
||||||
layout: 'hbox',
|
layout: 'hbox',
|
||||||
|
|
||||||
referenceHolder: true,
|
viewModel: {
|
||||||
|
data: {
|
||||||
|
datetime: null,
|
||||||
|
minDatetime: null,
|
||||||
|
maxDatetime: null,
|
||||||
|
},
|
||||||
|
|
||||||
submitFormat: 'U',
|
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);
|
||||||
|
},
|
||||||
|
|
||||||
getValue: function() {
|
getValue: function() {
|
||||||
let me = this;
|
return this.getViewModel().get('datetime');
|
||||||
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() {
|
getSubmitValue: function() {
|
||||||
let me = this;
|
let me = this;
|
||||||
let format = me.submitFormat;
|
let value = me.getValue();
|
||||||
let value = me.getValue();
|
return value ? Ext.Date.format(value, me.submitFormat) : null;
|
||||||
|
},
|
||||||
|
|
||||||
return value ? Ext.Date.format(value, format) : null;
|
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);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
xtype: 'datefield',
|
xtype: 'datefield',
|
||||||
editable: false,
|
editable: false,
|
||||||
reference: 'dateentry',
|
|
||||||
flex: 1,
|
flex: 1,
|
||||||
format: 'Y-m-d',
|
format: 'Y-m-d',
|
||||||
|
bind: {
|
||||||
|
value: '{date}',
|
||||||
|
minValue: '{minDate}',
|
||||||
|
maxValue: '{maxDate}',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xtype: 'timefield',
|
xtype: 'timefield',
|
||||||
reference: 'timeentry',
|
|
||||||
format: 'H:i',
|
format: 'H:i',
|
||||||
width: 80,
|
width: 80,
|
||||||
value: '00:00',
|
value: '00:00',
|
||||||
increment: 60,
|
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']);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
@ -24,7 +24,7 @@ Ext.define('Proxmox.form.field.DisplayEdit', {
|
|||||||
getEditable: function() {
|
getEditable: function() {
|
||||||
let me = this;
|
let me = this;
|
||||||
let vm = me.getViewModel();
|
let vm = me.getViewModel();
|
||||||
vm.get('editable');
|
return vm.get('editable');
|
||||||
},
|
},
|
||||||
|
|
||||||
setValue: function(value) {
|
setValue: function(value) {
|
||||||
@ -37,9 +37,19 @@ Ext.define('Proxmox.form.field.DisplayEdit', {
|
|||||||
getValue: function() {
|
getValue: function() {
|
||||||
let me = this;
|
let me = this;
|
||||||
let vm = me.getViewModel();
|
let vm = me.getViewModel();
|
||||||
|
// FIXME: add return, but check all use-sites for regressions then
|
||||||
vm.get('value');
|
vm.get('value');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setEmptyText: function(emptyText) {
|
||||||
|
let me = this;
|
||||||
|
me.editField.setEmptyText(emptyText);
|
||||||
|
},
|
||||||
|
getEmptyText: function() {
|
||||||
|
let me = this;
|
||||||
|
return me.editField.getEmptyText();
|
||||||
|
},
|
||||||
|
|
||||||
layout: 'fit',
|
layout: 'fit',
|
||||||
defaults: {
|
defaults: {
|
||||||
hideLabel: true,
|
hideLabel: true,
|
||||||
@ -98,6 +108,11 @@ Ext.define('Proxmox.form.field.DisplayEdit', {
|
|||||||
|
|
||||||
me.callParent();
|
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);
|
me.getViewModel().set('editable', me.editable);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
14
src/form/FingerprintField.js
Normal file
14
src/form/FingerprintField.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
@ -39,6 +39,8 @@ Ext.define('Proxmox.form.MultiDiskSelector', {
|
|||||||
setValue: function(value) {
|
setValue: function(value) {
|
||||||
let me = this;
|
let me = this;
|
||||||
|
|
||||||
|
value ??= [];
|
||||||
|
|
||||||
if (!Ext.isArray(value)) {
|
if (!Ext.isArray(value)) {
|
||||||
value = value.split(/;, /);
|
value = value.split(/;, /);
|
||||||
}
|
}
|
||||||
|
@ -45,10 +45,6 @@ Ext.define('Proxmox.form.NetworkSelector', {
|
|||||||
networkSelectorStore.load();
|
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',
|
valueField: 'cidr',
|
||||||
displayField: 'cidr',
|
displayField: 'cidr',
|
||||||
store: {
|
store: {
|
||||||
@ -123,6 +119,7 @@ Ext.define('Proxmox.form.NetworkSelector', {
|
|||||||
header: gettext('Comment'),
|
header: gettext('Comment'),
|
||||||
flex: 2,
|
flex: 2,
|
||||||
dataIndex: 'comments',
|
dataIndex: 'comments',
|
||||||
|
renderer: Ext.String.htmlEncode,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -7,6 +7,7 @@ Ext.define('Proxmox.form.RealmComboBox', {
|
|||||||
|
|
||||||
init: function(view) {
|
init: function(view) {
|
||||||
let store = view.getStore();
|
let store = view.getStore();
|
||||||
|
store.proxy.url = `/api2/json${view.baseUrl}`;
|
||||||
if (view.storeFilter) {
|
if (view.storeFilter) {
|
||||||
store.setFilters(view.storeFilter);
|
store.setFilters(view.storeFilter);
|
||||||
}
|
}
|
||||||
@ -45,6 +46,7 @@ Ext.define('Proxmox.form.RealmComboBox', {
|
|||||||
triggerAction: 'all',
|
triggerAction: 'all',
|
||||||
valueField: 'realm',
|
valueField: 'realm',
|
||||||
displayField: 'descr',
|
displayField: 'descr',
|
||||||
|
baseUrl: '/access/domains',
|
||||||
getState: function() {
|
getState: function() {
|
||||||
return { value: this.getValue() };
|
return { value: this.getValue() };
|
||||||
},
|
},
|
||||||
|
61
src/form/TextAreaField.js
Normal file
61
src/form/TextAreaField.js
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
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();
|
||||||
|
},
|
||||||
|
});
|
@ -6,6 +6,8 @@ Ext.define('Proxmox.form.field.Textfield', {
|
|||||||
skipEmptyText: true,
|
skipEmptyText: true,
|
||||||
|
|
||||||
deleteEmpty: false,
|
deleteEmpty: false,
|
||||||
|
|
||||||
|
trimValue: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
getSubmitData: function() {
|
getSubmitData: function() {
|
||||||
@ -29,6 +31,9 @@ Ext.define('Proxmox.form.field.Textfield', {
|
|||||||
let me = this;
|
let me = this;
|
||||||
|
|
||||||
let value = this.processRawValue(this.getRawValue());
|
let value = this.processRawValue(this.getRawValue());
|
||||||
|
if (me.getTrimValue() && typeof value === 'string') {
|
||||||
|
value = value.trim();
|
||||||
|
}
|
||||||
if (value !== '') {
|
if (value !== '') {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
40
src/form/VlanField.js
Normal file
40
src/form/VlanField.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
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();
|
||||||
|
},
|
||||||
|
});
|
@ -67,7 +67,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
|||||||
editor: {
|
editor: {
|
||||||
xtype: 'proxmoxWindowEdit',
|
xtype: 'proxmoxWindowEdit',
|
||||||
subject: text,
|
subject: text,
|
||||||
onlineHelp: opts.onlineHelp,
|
|
||||||
fieldDefaults: {
|
fieldDefaults: {
|
||||||
labelWidth: opts.labelWidth || 100,
|
labelWidth: opts.labelWidth || 100,
|
||||||
},
|
},
|
||||||
@ -84,6 +83,9 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
if (opts.onlineHelp) {
|
||||||
|
me.rows[name].editor.onlineHelp = opts.onlineHelp;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
add_text_row: function(name, text, opts) {
|
add_text_row: function(name, text, opts) {
|
||||||
@ -100,7 +102,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
|||||||
editor: {
|
editor: {
|
||||||
xtype: 'proxmoxWindowEdit',
|
xtype: 'proxmoxWindowEdit',
|
||||||
subject: text,
|
subject: text,
|
||||||
onlineHelp: opts.onlineHelp,
|
|
||||||
fieldDefaults: {
|
fieldDefaults: {
|
||||||
labelWidth: opts.labelWidth || 100,
|
labelWidth: opts.labelWidth || 100,
|
||||||
},
|
},
|
||||||
@ -115,6 +116,9 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
if (opts.onlineHelp) {
|
||||||
|
me.rows[name].editor.onlineHelp = opts.onlineHelp;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
add_boolean_row: function(name, text, opts) {
|
add_boolean_row: function(name, text, opts) {
|
||||||
@ -131,7 +135,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
|||||||
editor: {
|
editor: {
|
||||||
xtype: 'proxmoxWindowEdit',
|
xtype: 'proxmoxWindowEdit',
|
||||||
subject: text,
|
subject: text,
|
||||||
onlineHelp: opts.onlineHelp,
|
|
||||||
fieldDefaults: {
|
fieldDefaults: {
|
||||||
labelWidth: opts.labelWidth || 100,
|
labelWidth: opts.labelWidth || 100,
|
||||||
},
|
},
|
||||||
@ -147,6 +150,9 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
if (opts.onlineHelp) {
|
||||||
|
me.rows[name].editor.onlineHelp = opts.onlineHelp;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
add_integer_row: function(name, text, opts) {
|
add_integer_row: function(name, text, opts) {
|
||||||
@ -163,7 +169,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
|
|||||||
editor: {
|
editor: {
|
||||||
xtype: 'proxmoxWindowEdit',
|
xtype: 'proxmoxWindowEdit',
|
||||||
subject: text,
|
subject: text,
|
||||||
onlineHelp: opts.onlineHelp,
|
|
||||||
fieldDefaults: {
|
fieldDefaults: {
|
||||||
labelWidth: opts.labelWidth || 100,
|
labelWidth: opts.labelWidth || 100,
|
||||||
},
|
},
|
||||||
@ -180,6 +185,40 @@ 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
|
editorConfig: {}, // default config passed to editor
|
||||||
|
@ -2,7 +2,7 @@ Ext.define('apt-pkglist', {
|
|||||||
extend: 'Ext.data.Model',
|
extend: 'Ext.data.Model',
|
||||||
fields: [
|
fields: [
|
||||||
'Package', 'Title', 'Description', 'Section', 'Arch', 'Priority', 'Version', 'OldVersion',
|
'Package', 'Title', 'Description', 'Section', 'Arch', 'Priority', 'Version', 'OldVersion',
|
||||||
'ChangeLogUrl', 'Origin',
|
'Origin',
|
||||||
],
|
],
|
||||||
idProperty: 'Package',
|
idProperty: 'Package',
|
||||||
});
|
});
|
||||||
@ -108,8 +108,8 @@ Ext.define('Proxmox.node.APT', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let show_changelog = function(rec) {
|
let show_changelog = function(rec) {
|
||||||
if (!rec?.data?.ChangeLogUrl || !rec?.data?.Package) {
|
if (!rec?.data?.Package) {
|
||||||
console.debug('cannot show changelog, missing Package and/or ChangeLogUrl', rec);
|
console.debug('cannot show changelog, missing Package', rec);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +154,7 @@ Ext.define('Proxmox.node.APT', {
|
|||||||
text: gettext('Changelog'),
|
text: gettext('Changelog'),
|
||||||
selModel: sm,
|
selModel: sm,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
enableFn: rec => !!rec?.data?.ChangeLogUrl && !!rec?.data?.Package,
|
enableFn: rec => !!rec?.data?.Package,
|
||||||
handler: (b, e, rec) => show_changelog(rec),
|
handler: (b, e, rec) => show_changelog(rec),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -270,7 +270,7 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
|
|||||||
let txt = [gettext('Warning')];
|
let txt = [gettext('Warning')];
|
||||||
record.data.warnings.forEach((warning) => {
|
record.data.warnings.forEach((warning) => {
|
||||||
if (warning.property === 'Suites') {
|
if (warning.property === 'Suites') {
|
||||||
txt.push(warning.message);
|
txt.push(Ext.htmlEncode(warning.message));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
metaData.tdAttr = `data-qtip="${Ext.htmlEncode(txt.join('<br>'))}"`;
|
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 no-subscription repository is NOT production-ready')
|
||||||
: gettext('The test repository may contain unstable updates')
|
: gettext('The test repository may contain unstable updates')
|
||||||
;
|
;
|
||||||
metaData.tdAttr = `data-qtip="${Ext.htmlEncode(qtip)}"`;
|
metaData.tdAttr = `data-qtip="${Ext.htmlEncode(Ext.htmlEncode(qtip))}"`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return components.join(' ') + err;
|
return components.join(' ') + err;
|
||||||
@ -343,14 +343,15 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
|
|||||||
header: gettext('Origin'),
|
header: gettext('Origin'),
|
||||||
dataIndex: 'Origin',
|
dataIndex: 'Origin',
|
||||||
width: 120,
|
width: 120,
|
||||||
renderer: (value, meta, rec) => {
|
renderer: function(value, meta, rec) {
|
||||||
if (typeof value !== 'string' || value.length === 0) {
|
if (typeof value !== 'string' || value.length === 0) {
|
||||||
value = gettext('Other');
|
value = gettext('Other');
|
||||||
}
|
}
|
||||||
let cls = 'fa fa-fw fa-question-circle-o';
|
let cls = 'fa fa-fw fa-question-circle-o';
|
||||||
if (value.match(/^\s*Proxmox\s*$/i)) {
|
let originType = this.up('proxmoxNodeAPTRepositories').classifyOrigin(value);
|
||||||
|
if (originType === 'Proxmox') {
|
||||||
cls = 'pmx-itype-icon pmx-itype-icon-proxmox-x';
|
cls = 'pmx-itype-icon pmx-itype-icon-proxmox-x';
|
||||||
} else if (value.match(/^\s*Debian\s*(:?Backports)?$/i)) {
|
} else if (originType === 'Debian') {
|
||||||
cls = 'pmx-itype-icon pmx-itype-icon-debian-swirl';
|
cls = 'pmx-itype-icon pmx-itype-icon-debian-swirl';
|
||||||
}
|
}
|
||||||
return `<i class='${cls}'></i> ${value}`;
|
return `<i class='${cls}'></i> ${value}`;
|
||||||
@ -360,6 +361,7 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
|
|||||||
header: gettext('Comment'),
|
header: gettext('Comment'),
|
||||||
dataIndex: 'Comment',
|
dataIndex: 'Comment',
|
||||||
flex: 2,
|
flex: 2,
|
||||||
|
renderer: Ext.String.htmlEncode,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
@ -404,6 +406,16 @@ Ext.define('Proxmox.node.APTRepositories', {
|
|||||||
|
|
||||||
product: 'Proxmox VE', // default
|
product: 'Proxmox VE', // default
|
||||||
|
|
||||||
|
classifyOrigin: function(origin) {
|
||||||
|
origin ||= '';
|
||||||
|
if (origin.match(/^\s*Proxmox\s*$/i)) {
|
||||||
|
return 'Proxmox';
|
||||||
|
} else if (origin.match(/^\s*Debian\s*(:?Backports)?$/i)) {
|
||||||
|
return 'Debian';
|
||||||
|
}
|
||||||
|
return 'Other';
|
||||||
|
},
|
||||||
|
|
||||||
controller: {
|
controller: {
|
||||||
xclass: 'Ext.app.ViewController',
|
xclass: 'Ext.app.ViewController',
|
||||||
|
|
||||||
@ -449,7 +461,13 @@ Ext.define('Proxmox.node.APTRepositories', {
|
|||||||
let enterprise = vm.get('enterpriseRepo');
|
let enterprise = vm.get('enterpriseRepo');
|
||||||
let nosubscription = vm.get('noSubscriptionRepo');
|
let nosubscription = vm.get('noSubscriptionRepo');
|
||||||
let test = vm.get('testRepo');
|
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 wrongSuites = vm.get('suitesWarning');
|
||||||
|
let mixedSuites = vm.get('mixedSuites');
|
||||||
|
|
||||||
if (!enterprise && !nosubscription && !test) {
|
if (!enterprise && !nosubscription && !test) {
|
||||||
addCritical(
|
addCritical(
|
||||||
@ -467,17 +485,37 @@ Ext.define('Proxmox.node.APTRepositories', {
|
|||||||
addWarn(gettext('Some suites are misconfigured'));
|
addWarn(gettext('Some suites are misconfigured'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!activeSubscription && enterprise) {
|
if (mixedSuites) {
|
||||||
addWarn(gettext('The enterprise repository is enabled, but there is no active subscription!'));
|
addWarn(gettext('Detected mixed suites before upgrade'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nosubscription) {
|
let productionReadyCheck = (repos, type, noSubAlternateName) => {
|
||||||
addWarn(gettext('The no-subscription repository is not recommended for production use!'));
|
if (!activeSubscription && repos.enterprise) {
|
||||||
}
|
addWarn(Ext.String.format(
|
||||||
|
gettext('The {0}enterprise repository is enabled, but there is no active subscription!'),
|
||||||
|
type,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if (test) {
|
if (repos.nosubscription) {
|
||||||
addWarn(gettext('The test repository may pull in unstable updates and is not recommended for production use!'));
|
addWarn(Ext.String.format(
|
||||||
}
|
gettext('The {0}no-subscription{1} repository is not recommended for production use!'),
|
||||||
|
type,
|
||||||
|
noSubAlternateName,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (errors.length > 0) {
|
if (errors.length > 0) {
|
||||||
text = gettext('Fatal parsing error for at least one repository');
|
text = gettext('Fatal parsing error for at least one repository');
|
||||||
@ -497,10 +535,14 @@ Ext.define('Proxmox.node.APTRepositories', {
|
|||||||
product: 'Proxmox VE', // default
|
product: 'Proxmox VE', // default
|
||||||
errors: [],
|
errors: [],
|
||||||
suitesWarning: false,
|
suitesWarning: false,
|
||||||
|
mixedSuites: false, // used before major upgrade
|
||||||
subscriptionActive: '',
|
subscriptionActive: '',
|
||||||
noSubscriptionRepo: '',
|
noSubscriptionRepo: '',
|
||||||
enterpriseRepo: '',
|
enterpriseRepo: '',
|
||||||
testRepo: '',
|
testRepo: '',
|
||||||
|
cephEnterpriseRepo: '',
|
||||||
|
cephNoSubscriptionRepo: '',
|
||||||
|
cephTestRepo: '',
|
||||||
selectionenabled: false,
|
selectionenabled: false,
|
||||||
state: {},
|
state: {},
|
||||||
},
|
},
|
||||||
@ -610,6 +652,12 @@ Ext.define('Proxmox.node.APTRepositories', {
|
|||||||
vm.set('noSubscriptionRepo', status);
|
vm.set('noSubscriptionRepo', status);
|
||||||
} else if (handle === 'test') {
|
} else if (handle === 'test') {
|
||||||
vm.set('testRepo', status);
|
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();
|
me.getController().updateState();
|
||||||
|
|
||||||
@ -631,6 +679,11 @@ Ext.define('Proxmox.node.APTRepositories', {
|
|||||||
let digest;
|
let digest;
|
||||||
let suitesWarning = false;
|
let suitesWarning = false;
|
||||||
|
|
||||||
|
// Usually different suites will give errors anyways, but before a major upgrade the
|
||||||
|
// current and the next suite are allowed, so it makes sense to check for mixed suites.
|
||||||
|
let checkMixedSuites = false;
|
||||||
|
let mixedSuites = false;
|
||||||
|
|
||||||
if (success && records.length > 0) {
|
if (success && records.length > 0) {
|
||||||
let data = records[0].data;
|
let data = records[0].data;
|
||||||
let files = data.files;
|
let files = data.files;
|
||||||
@ -649,17 +702,23 @@ Ext.define('Proxmox.node.APTRepositories', {
|
|||||||
infos[path][idx] = {
|
infos[path][idx] = {
|
||||||
origin: '',
|
origin: '',
|
||||||
warnings: [],
|
warnings: [],
|
||||||
|
// Used as a heuristic to detect mixed repositories pre-upgrade. The
|
||||||
|
// warning is set on all repositories that do configure the next suite.
|
||||||
|
gotIgnorePreUpgradeWarning: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.kind === 'origin') {
|
if (info.kind === 'origin') {
|
||||||
infos[path][idx].origin = info.message;
|
infos[path][idx].origin = info.message;
|
||||||
} else if (info.kind === 'warning' ||
|
} else if (info.kind === 'warning') {
|
||||||
(info.kind === 'ignore-pre-upgrade-warning' && !repoGrid.majorUpgradeAllowed)
|
|
||||||
) {
|
|
||||||
infos[path][idx].warnings.push(info);
|
infos[path][idx].warnings.push(info);
|
||||||
} else {
|
} else if (info.kind === 'ignore-pre-upgrade-warning') {
|
||||||
throw 'unknown info';
|
infos[path][idx].gotIgnorePreUpgradeWarning = true;
|
||||||
|
if (!repoGrid.majorUpgradeAllowed) {
|
||||||
|
infos[path][idx].warnings.push(info);
|
||||||
|
} else {
|
||||||
|
checkMixedSuites = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -670,11 +729,24 @@ Ext.define('Proxmox.node.APTRepositories', {
|
|||||||
repo.Path = file.path;
|
repo.Path = file.path;
|
||||||
repo.Index = n;
|
repo.Index = n;
|
||||||
if (infos[file.path] && infos[file.path][n]) {
|
if (infos[file.path] && infos[file.path][n]) {
|
||||||
repo.Origin = infos[file.path][n].origin || Proxmox.Utils.UnknownText;
|
repo.Origin = infos[file.path][n].origin || Proxmox.Utils.unknownText;
|
||||||
repo.warnings = infos[file.path][n].warnings || [];
|
repo.warnings = infos[file.path][n].warnings || [];
|
||||||
|
|
||||||
if (repo.Enabled && repo.warnings.some(w => w.property === 'Suites')) {
|
if (repo.Enabled) {
|
||||||
suitesWarning = true;
|
if (repo.warnings.some(w => w.property === 'Suites')) {
|
||||||
|
suitesWarning = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let originType = me.classifyOrigin(repo.Origin);
|
||||||
|
// Only Proxmox and Debian repositories checked here, because the
|
||||||
|
// warning can be missing for others for a different reason (e.g.
|
||||||
|
// using 'stable' or non-Debian code names).
|
||||||
|
if (checkMixedSuites && repo.Types.includes('deb') &&
|
||||||
|
(originType === 'Proxmox' || originType === 'Debian') &&
|
||||||
|
!infos[file.path][n].gotIgnorePreUpgradeWarning
|
||||||
|
) {
|
||||||
|
mixedSuites = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gridData.push(repo);
|
gridData.push(repo);
|
||||||
@ -690,6 +762,7 @@ Ext.define('Proxmox.node.APTRepositories', {
|
|||||||
|
|
||||||
vm.set('errors', errors);
|
vm.set('errors', errors);
|
||||||
vm.set('suitesWarning', suitesWarning);
|
vm.set('suitesWarning', suitesWarning);
|
||||||
|
vm.set('mixedSuites', mixedSuites);
|
||||||
me.getController().updateState();
|
me.getController().updateState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,6 +2,10 @@ Ext.define('Proxmox.node.DNSEdit', {
|
|||||||
extend: 'Proxmox.window.Edit',
|
extend: 'Proxmox.window.Edit',
|
||||||
alias: ['widget.proxmoxNodeDNSEdit'],
|
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() {
|
initComponent: function() {
|
||||||
let me = this;
|
let me = this;
|
||||||
|
|
||||||
@ -21,6 +25,7 @@ Ext.define('Proxmox.node.DNSEdit', {
|
|||||||
fieldLabel: gettext('DNS server') + " 1",
|
fieldLabel: gettext('DNS server') + " 1",
|
||||||
vtype: 'IP64Address',
|
vtype: 'IP64Address',
|
||||||
skipEmptyText: true,
|
skipEmptyText: true,
|
||||||
|
deleteEmpty: me.deleteEmpty,
|
||||||
name: 'dns1',
|
name: 'dns1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -28,6 +33,7 @@ Ext.define('Proxmox.node.DNSEdit', {
|
|||||||
fieldLabel: gettext('DNS server') + " 2",
|
fieldLabel: gettext('DNS server') + " 2",
|
||||||
vtype: 'IP64Address',
|
vtype: 'IP64Address',
|
||||||
skipEmptyText: true,
|
skipEmptyText: true,
|
||||||
|
deleteEmpty: me.deleteEmpty,
|
||||||
name: 'dns2',
|
name: 'dns2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -35,6 +41,7 @@ Ext.define('Proxmox.node.DNSEdit', {
|
|||||||
fieldLabel: gettext('DNS server') + " 3",
|
fieldLabel: gettext('DNS server') + " 3",
|
||||||
vtype: 'IP64Address',
|
vtype: 'IP64Address',
|
||||||
skipEmptyText: true,
|
skipEmptyText: true,
|
||||||
|
deleteEmpty: me.deleteEmpty,
|
||||||
name: 'dns3',
|
name: 'dns3',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -2,6 +2,10 @@ Ext.define('Proxmox.node.DNSView', {
|
|||||||
extend: 'Proxmox.grid.ObjectGrid',
|
extend: 'Proxmox.grid.ObjectGrid',
|
||||||
alias: ['widget.proxmoxNodeDNSView'],
|
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() {
|
initComponent: function() {
|
||||||
let me = this;
|
let me = this;
|
||||||
|
|
||||||
@ -12,6 +16,7 @@ Ext.define('Proxmox.node.DNSView', {
|
|||||||
let run_editor = () => Ext.create('Proxmox.node.DNSEdit', {
|
let run_editor = () => Ext.create('Proxmox.node.DNSEdit', {
|
||||||
autoShow: true,
|
autoShow: true,
|
||||||
nodename: me.nodename,
|
nodename: me.nodename,
|
||||||
|
deleteEmpty: me.deleteEmpty,
|
||||||
});
|
});
|
||||||
|
|
||||||
Ext.apply(me, {
|
Ext.apply(me, {
|
||||||
@ -21,7 +26,7 @@ Ext.define('Proxmox.node.DNSView', {
|
|||||||
run_editor: run_editor,
|
run_editor: run_editor,
|
||||||
rows: {
|
rows: {
|
||||||
search: {
|
search: {
|
||||||
header: 'Search domain',
|
header: gettext('Search domain'),
|
||||||
required: true,
|
required: true,
|
||||||
renderer: Ext.htmlEncode,
|
renderer: Ext.htmlEncode,
|
||||||
},
|
},
|
||||||
|
@ -2,6 +2,9 @@ Ext.define('Proxmox.node.NetworkEdit', {
|
|||||||
extend: 'Proxmox.window.Edit',
|
extend: 'Proxmox.window.Edit',
|
||||||
alias: ['widget.proxmoxNodeNetworkEdit'],
|
alias: ['widget.proxmoxNodeNetworkEdit'],
|
||||||
|
|
||||||
|
// Enable to show the VLAN ID field
|
||||||
|
enableBridgeVlanIds: false,
|
||||||
|
|
||||||
initComponent: function() {
|
initComponent: function() {
|
||||||
let me = this;
|
let me = this;
|
||||||
|
|
||||||
@ -57,11 +60,53 @@ Ext.define('Proxmox.node.NetworkEdit', {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (me.iftype === 'bridge') {
|
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({
|
column2.push({
|
||||||
xtype: 'proxmoxcheckbox',
|
xtype: 'proxmoxcheckbox',
|
||||||
fieldLabel: gettext('VLAN aware'),
|
fieldLabel: gettext('VLAN aware'),
|
||||||
name: 'bridge_vlan_aware',
|
name: 'bridge_vlan_aware',
|
||||||
deleteEmpty: !me.isCreate,
|
deleteEmpty: !me.isCreate,
|
||||||
|
listeners: {
|
||||||
|
change: function(f, newVal) {
|
||||||
|
if (vlanIdsField) {
|
||||||
|
vlanIdsField.setDisabled(!newVal);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
column2.push({
|
column2.push({
|
||||||
xtype: 'textfield',
|
xtype: 'textfield',
|
||||||
@ -72,6 +117,9 @@ Ext.define('Proxmox.node.NetworkEdit', {
|
|||||||
'data-qtip': gettext('Space-separated list of interfaces, for example: enp0s0 enp1s0'),
|
'data-qtip': gettext('Space-separated list of interfaces, for example: enp0s0 enp1s0'),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
if (vlanIdsField) {
|
||||||
|
advancedColumn2.push(vlanIdsField);
|
||||||
|
}
|
||||||
} else if (me.iftype === 'OVSBridge') {
|
} else if (me.iftype === 'OVSBridge') {
|
||||||
column2.push({
|
column2.push({
|
||||||
xtype: 'textfield',
|
xtype: 'textfield',
|
||||||
@ -97,7 +145,7 @@ Ext.define('Proxmox.node.NetworkEdit', {
|
|||||||
name: 'ovs_bridge',
|
name: 'ovs_bridge',
|
||||||
});
|
});
|
||||||
column2.push({
|
column2.push({
|
||||||
xtype: 'pveVlanField',
|
xtype: 'proxmoxvlanfield',
|
||||||
deleteEmpty: !me.isCreate,
|
deleteEmpty: !me.isCreate,
|
||||||
name: 'ovs_tag',
|
name: 'ovs_tag',
|
||||||
value: '',
|
value: '',
|
||||||
@ -140,7 +188,7 @@ Ext.define('Proxmox.node.NetworkEdit', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
column2.push({
|
column2.push({
|
||||||
xtype: 'pveVlanField',
|
xtype: 'proxmoxvlanfield',
|
||||||
name: 'vlan-id',
|
name: 'vlan-id',
|
||||||
value: me.vlanidvalue,
|
value: me.vlanidvalue,
|
||||||
disabled: me.disablevlanid,
|
disabled: me.disablevlanid,
|
||||||
@ -211,7 +259,7 @@ Ext.define('Proxmox.node.NetworkEdit', {
|
|||||||
name: 'ovs_bridge',
|
name: 'ovs_bridge',
|
||||||
});
|
});
|
||||||
column2.push({
|
column2.push({
|
||||||
xtype: 'pveVlanField',
|
xtype: 'proxmoxvlanfield',
|
||||||
deleteEmpty: !me.isCreate,
|
deleteEmpty: !me.isCreate,
|
||||||
name: 'ovs_tag',
|
name: 'ovs_tag',
|
||||||
value: '',
|
value: '',
|
||||||
@ -254,7 +302,7 @@ Ext.define('Proxmox.node.NetworkEdit', {
|
|||||||
value: me.iface,
|
value: me.iface,
|
||||||
vtype: iface_vtype,
|
vtype: iface_vtype,
|
||||||
allowBlank: false,
|
allowBlank: false,
|
||||||
maxLength: 15,
|
maxLength: iface_vtype === 'BridgeName' ? 10 : 15,
|
||||||
autoEl: {
|
autoEl: {
|
||||||
tag: 'div',
|
tag: 'div',
|
||||||
'data-qtip': gettext('For example, vmbr0.100, vmbr0, vlan0.100, vlan0'),
|
'data-qtip': gettext('For example, vmbr0.100, vmbr0, vlan0.100, vlan0'),
|
||||||
|
@ -33,6 +33,9 @@ Ext.define('Proxmox.node.NetworkView', {
|
|||||||
|
|
||||||
showApplyBtn: false,
|
showApplyBtn: false,
|
||||||
|
|
||||||
|
// for options passed down to the network edit window
|
||||||
|
editOptions: {},
|
||||||
|
|
||||||
initComponent: function() {
|
initComponent: function() {
|
||||||
let me = this;
|
let me = this;
|
||||||
|
|
||||||
@ -100,6 +103,7 @@ Ext.define('Proxmox.node.NetworkView', {
|
|||||||
nodename: me.nodename,
|
nodename: me.nodename,
|
||||||
iface: rec.data.iface,
|
iface: rec.data.iface,
|
||||||
iftype: rec.data.type,
|
iftype: rec.data.type,
|
||||||
|
...me.editOptions,
|
||||||
listeners: {
|
listeners: {
|
||||||
destroy: () => reload(),
|
destroy: () => reload(),
|
||||||
},
|
},
|
||||||
@ -170,6 +174,7 @@ Ext.define('Proxmox.node.NetworkView', {
|
|||||||
nodename: me.nodename,
|
nodename: me.nodename,
|
||||||
iftype: iType,
|
iftype: iType,
|
||||||
iface_default: findNextFreeInterfaceId(iDefault ?? iType),
|
iface_default: findNextFreeInterfaceId(iDefault ?? iType),
|
||||||
|
...me.editOptions,
|
||||||
onlineHelp: 'sysadmin_network_configuration',
|
onlineHelp: 'sysadmin_network_configuration',
|
||||||
listeners: {
|
listeners: {
|
||||||
destroy: () => reload(),
|
destroy: () => reload(),
|
||||||
|
@ -29,6 +29,8 @@ Ext.define('Proxmox.node.ServiceView', {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let filterInstalledOnly = record => record.get('unit-state') !== 'not-found';
|
||||||
|
|
||||||
let store = Ext.create('Proxmox.data.DiffStore', {
|
let store = Ext.create('Proxmox.data.DiffStore', {
|
||||||
rstore: rstore,
|
rstore: rstore,
|
||||||
sortAfterUpdate: true,
|
sortAfterUpdate: true,
|
||||||
@ -38,6 +40,24 @@ Ext.define('Proxmox.node.ServiceView', {
|
|||||||
direction: 'ASC',
|
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() {
|
let view_service_log = function() {
|
||||||
@ -166,6 +186,8 @@ Ext.define('Proxmox.node.ServiceView', {
|
|||||||
restart_btn,
|
restart_btn,
|
||||||
'-',
|
'-',
|
||||||
syslog_btn,
|
syslog_btn,
|
||||||
|
'->',
|
||||||
|
unHideCB,
|
||||||
],
|
],
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
Ext.define('Proxmox.panel.AuthView', {
|
Ext.define('Proxmox.panel.AuthView', {
|
||||||
extend: 'Ext.grid.GridPanel',
|
extend: 'Ext.grid.GridPanel',
|
||||||
|
|
||||||
alias: 'widget.pmxAuthView',
|
alias: 'widget.pmxAuthView',
|
||||||
|
mixins: ['Proxmox.Mixin.CBind'],
|
||||||
|
|
||||||
|
showDefaultRealm: false,
|
||||||
|
|
||||||
stateful: true,
|
stateful: true,
|
||||||
stateId: 'grid-authrealms',
|
stateId: 'grid-authrealms',
|
||||||
@ -11,7 +13,7 @@ Ext.define('Proxmox.panel.AuthView', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
baseUrl: '/access/domains',
|
baseUrl: '/access/domains',
|
||||||
useTypeInUrl: false,
|
storeBaseUrl: '/access/domains',
|
||||||
|
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
@ -26,6 +28,17 @@ Ext.define('Proxmox.panel.AuthView', {
|
|||||||
sortable: true,
|
sortable: true,
|
||||||
dataIndex: 'type',
|
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'),
|
header: gettext('Comment'),
|
||||||
sortable: false,
|
sortable: false,
|
||||||
@ -35,21 +48,17 @@ Ext.define('Proxmox.panel.AuthView', {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
store: {
|
|
||||||
model: 'pmx-domains',
|
|
||||||
sorters: {
|
|
||||||
property: 'realm',
|
|
||||||
direction: 'ASC',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
openEditWindow: function(authType, realm) {
|
openEditWindow: function(authType, realm) {
|
||||||
let me = this;
|
let me = this;
|
||||||
|
const { useTypeInUrl, onlineHelp } = Proxmox.Schema.authDomains[authType];
|
||||||
|
|
||||||
Ext.create('Proxmox.window.AuthEditBase', {
|
Ext.create('Proxmox.window.AuthEditBase', {
|
||||||
baseUrl: me.baseUrl,
|
baseUrl: me.baseUrl,
|
||||||
useTypeInUrl: me.useTypeInUrl,
|
useTypeInUrl,
|
||||||
|
onlineHelp,
|
||||||
authType,
|
authType,
|
||||||
realm,
|
realm,
|
||||||
|
showDefaultRealm: me.showDefaultRealm,
|
||||||
listeners: {
|
listeners: {
|
||||||
destroy: () => me.reload(),
|
destroy: () => me.reload(),
|
||||||
},
|
},
|
||||||
@ -95,6 +104,18 @@ Ext.define('Proxmox.panel.AuthView', {
|
|||||||
initComponent: function() {
|
initComponent: function() {
|
||||||
var me = this;
|
var me = this;
|
||||||
|
|
||||||
|
me.store = {
|
||||||
|
model: 'pmx-domains',
|
||||||
|
sorters: {
|
||||||
|
property: 'realm',
|
||||||
|
direction: 'ASC',
|
||||||
|
},
|
||||||
|
proxy: {
|
||||||
|
type: 'proxmox',
|
||||||
|
url: `/api2/json${me.storeBaseUrl}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let menuitems = [];
|
let menuitems = [];
|
||||||
for (const [authType, config] of Object.entries(Proxmox.Schema.authDomains).sort()) {
|
for (const [authType, config] of Object.entries(Proxmox.Schema.authDomains).sort()) {
|
||||||
if (!config.add) { continue; }
|
if (!config.add) { continue; }
|
||||||
@ -123,7 +144,7 @@ Ext.define('Proxmox.panel.AuthView', {
|
|||||||
xtype: 'proxmoxStdRemoveButton',
|
xtype: 'proxmoxStdRemoveButton',
|
||||||
getUrl: (rec) => {
|
getUrl: (rec) => {
|
||||||
let url = me.baseUrl;
|
let url = me.baseUrl;
|
||||||
if (me.useTypeInUrl) {
|
if (Proxmox.Schema.authDomains[rec.data.type].useTypeInUrl) {
|
||||||
url += `/${rec.get('type')}`;
|
url += `/${rec.get('type')}`;
|
||||||
}
|
}
|
||||||
url += `/${rec.getId()}`;
|
url += `/${rec.getId()}`;
|
||||||
|
@ -85,7 +85,7 @@ Ext.define('Proxmox.panel.Certificates', {
|
|||||||
url: `/api2/extjs/${url}?restart=1`,
|
url: `/api2/extjs/${url}?restart=1`,
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
success: function(response, opt) {
|
success: function(response, opt) {
|
||||||
if (cert.reloadUid) {
|
if (cert.reloadUi) {
|
||||||
Ext.getBody().mask(
|
Ext.getBody().mask(
|
||||||
gettext('API server will be restarted to use new certificates, please reload web-interface!'),
|
gettext('API server will be restarted to use new certificates, please reload web-interface!'),
|
||||||
['pve-static-mask'],
|
['pve-static-mask'],
|
||||||
@ -237,10 +237,16 @@ Ext.define('Proxmox.panel.Certificates', {
|
|||||||
{
|
{
|
||||||
xtype: 'proxmoxButton',
|
xtype: 'proxmoxButton',
|
||||||
text: gettext('Delete Custom Certificate'),
|
text: gettext('Delete Custom Certificate'),
|
||||||
confirmMsg: rec => Ext.String.format(
|
confirmMsg: rec => {
|
||||||
gettext('Are you sure you want to remove the certificate used for {0}'),
|
let cert = me.certById[rec.id];
|
||||||
me.certById[rec.id].name,
|
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');
|
||||||
|
},
|
||||||
callback: () => me.reload(),
|
callback: () => me.reload(),
|
||||||
selModel: me.selModel,
|
selModel: me.selModel,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
|
@ -220,7 +220,11 @@ Ext.define('Proxmox.DiskList', {
|
|||||||
let extendedInfo = '';
|
let extendedInfo = '';
|
||||||
if (rec) {
|
if (rec) {
|
||||||
let types = [];
|
let types = [];
|
||||||
if (rec.data.osdid !== undefined && rec.data.osdid >= 0) {
|
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) {
|
||||||
types.push(`OSD.${rec.data.osdid.toString()}`);
|
types.push(`OSD.${rec.data.osdid.toString()}`);
|
||||||
}
|
}
|
||||||
if (rec.data.journals > 0) {
|
if (rec.data.journals > 0) {
|
||||||
@ -321,14 +325,14 @@ Ext.define('Proxmox.DiskList', {
|
|||||||
dataIndex: 'status',
|
dataIndex: 'status',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Mounted',
|
header: gettext('Mounted'),
|
||||||
width: 60,
|
width: 60,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
renderer: Proxmox.Utils.format_boolean,
|
renderer: Proxmox.Utils.format_boolean,
|
||||||
dataIndex: 'mounted',
|
dataIndex: 'mounted',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Wearout',
|
header: gettext('Wearout'),
|
||||||
width: 90,
|
width: 90,
|
||||||
sortable: true,
|
sortable: true,
|
||||||
align: 'right',
|
align: 'right',
|
||||||
@ -337,7 +341,7 @@ Ext.define('Proxmox.DiskList', {
|
|||||||
if (Ext.isNumeric(value)) {
|
if (Ext.isNumeric(value)) {
|
||||||
return (100 - value).toString() + '%';
|
return (100 - value).toString() + '%';
|
||||||
}
|
}
|
||||||
return 'N/A';
|
return gettext('N/A');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -3,6 +3,7 @@ Ext.define('Proxmox.EOLNotice', {
|
|||||||
extend: 'Ext.Component',
|
extend: 'Ext.Component',
|
||||||
alias: 'widget.proxmoxEOLNotice',
|
alias: 'widget.proxmoxEOLNotice',
|
||||||
|
|
||||||
|
userCls: 'eol-notice',
|
||||||
padding: '0 5',
|
padding: '0 5',
|
||||||
|
|
||||||
config: {
|
config: {
|
||||||
@ -17,14 +18,25 @@ Ext.define('Proxmox.EOLNotice', {
|
|||||||
'data-qtip': gettext("You won't get any security fixes after the End-Of-Life date. Please consider upgrading."),
|
'data-qtip': gettext("You won't get any security fixes after the End-Of-Life date. Please consider upgrading."),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getIconCls: function() {
|
||||||
|
let me = this;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const eolDate = new Date(me.eolDate);
|
||||||
|
const warningCutoff = new Date(eolDate.getTime() - (21 * 24 * 60 * 60 * 1000)); // 3 weeks
|
||||||
|
|
||||||
|
return now > warningCutoff ? 'critical fa-exclamation-triangle' : 'info-blue fa-info-circle';
|
||||||
|
},
|
||||||
|
|
||||||
initComponent: function() {
|
initComponent: function() {
|
||||||
let me = this;
|
let me = this;
|
||||||
|
|
||||||
|
let iconCls = me.getIconCls();
|
||||||
let href = me.href.startsWith('http') ? me.href : `https://${me.href}`;
|
let href = me.href.startsWith('http') ? me.href : `https://${me.href}`;
|
||||||
let message = Ext.String.format(
|
let message = Ext.String.format(
|
||||||
gettext('Support for {0} {1} ends on {2}'), me.product, me.version, me.eolDate);
|
gettext('Support for {0} {1} ends on {2}'), me.product, me.version, me.eolDate);
|
||||||
|
|
||||||
me.html = `<i class="fa pwt-eol-icon fa-exclamation-triangle"></i>
|
me.html = `<i class="fa ${iconCls}"></i>
|
||||||
<a href="${href}" target="_blank">${message} <i class="fa fa-external-link"></i></a>
|
<a href="${href}" target="_blank">${message} <i class="fa fa-external-link"></i></a>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
91
src/panel/EmailRecipientPanel.js
Normal file
91
src/panel/EmailRecipientPanel.js
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
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();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
75
src/panel/GotifyEditPanel.js
Normal file
75
src/panel/GotifyEditPanel.js
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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;
|
||||||
|
},
|
||||||
|
});
|
@ -22,19 +22,28 @@ Ext.define('Proxmox.panel.LogView', {
|
|||||||
updateParams: function() {
|
updateParams: function() {
|
||||||
let me = this;
|
let me = this;
|
||||||
let viewModel = me.getViewModel();
|
let viewModel = me.getViewModel();
|
||||||
let since = viewModel.get('since');
|
|
||||||
let until = viewModel.get('until');
|
if (viewModel.get('hide_timespan') || viewModel.get('livemode')) {
|
||||||
if (viewModel.get('hide_timespan')) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let since = viewModel.get('since');
|
||||||
|
let until = viewModel.get('until');
|
||||||
|
|
||||||
if (since > until) {
|
if (since > until) {
|
||||||
Ext.Msg.alert('Error', 'Since date must be less equal than Until date.');
|
Ext.Msg.alert('Error', 'Since date must be less equal than Until date.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.set('params.since', Ext.Date.format(since, 'Y-m-d'));
|
let submitFormat = viewModel.get('submitFormat');
|
||||||
viewModel.set('params.until', Ext.Date.format(until, 'Y-m-d') + ' 23:59:59');
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
me.getView().loadTask.delay(200);
|
me.getView().loadTask.delay(200);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -175,6 +184,27 @@ 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) {
|
init: function(view) {
|
||||||
let me = this;
|
let me = this;
|
||||||
|
|
||||||
@ -189,6 +219,7 @@ Ext.define('Proxmox.panel.LogView', {
|
|||||||
viewModel.set('since', since);
|
viewModel.set('since', since);
|
||||||
viewModel.set('params.limit', view.pageSize);
|
viewModel.set('params.limit', view.pageSize);
|
||||||
viewModel.set('hide_timespan', !view.log_select_timespan);
|
viewModel.set('hide_timespan', !view.log_select_timespan);
|
||||||
|
viewModel.set('submitFormat', view.submitFormat);
|
||||||
me.lookup('content').setStyle('line-height', `${view.lineHeight}px`);
|
me.lookup('content').setStyle('line-height', `${view.lineHeight}px`);
|
||||||
|
|
||||||
view.loadTask = new Ext.util.DelayedTask(me.doLoad, me);
|
view.loadTask = new Ext.util.DelayedTask(me.doLoad, me);
|
||||||
@ -224,6 +255,8 @@ Ext.define('Proxmox.panel.LogView', {
|
|||||||
data: {
|
data: {
|
||||||
until: null,
|
until: null,
|
||||||
since: null,
|
since: null,
|
||||||
|
submitFormat: 'Y-m-d',
|
||||||
|
livemode: true,
|
||||||
hide_timespan: false,
|
hide_timespan: false,
|
||||||
data: {
|
data: {
|
||||||
start: 0,
|
start: 0,
|
||||||
@ -263,32 +296,70 @@ Ext.define('Proxmox.panel.LogView', {
|
|||||||
},
|
},
|
||||||
items: [
|
items: [
|
||||||
'->',
|
'->',
|
||||||
'Since: ',
|
|
||||||
{
|
{
|
||||||
xtype: 'datefield',
|
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',
|
||||||
name: 'since_date',
|
name: 'since_date',
|
||||||
reference: 'since',
|
reference: 'since',
|
||||||
format: 'Y-m-d',
|
format: 'Y-m-d',
|
||||||
bind: {
|
bind: {
|
||||||
|
disabled: '{livemode}',
|
||||||
value: '{since}',
|
value: '{since}',
|
||||||
maxValue: '{until}',
|
maxValue: '{until}',
|
||||||
|
submitFormat: '{submitFormat}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'Until: ',
|
|
||||||
{
|
{
|
||||||
xtype: 'datefield',
|
xtype: 'box',
|
||||||
|
autoEl: { cn: gettext('Until') + ':' },
|
||||||
|
bind: {
|
||||||
|
disabled: '{livemode}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
xtype: 'proxmoxDateTimeField',
|
||||||
name: 'until_date',
|
name: 'until_date',
|
||||||
reference: 'until',
|
reference: 'until',
|
||||||
format: 'Y-m-d',
|
format: 'Y-m-d',
|
||||||
bind: {
|
bind: {
|
||||||
|
disabled: '{livemode}',
|
||||||
value: '{until}',
|
value: '{until}',
|
||||||
minValue: '{since}',
|
minValue: '{since}',
|
||||||
|
submitFormat: '{submitFormat}',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xtype: 'button',
|
xtype: 'button',
|
||||||
text: 'Update',
|
text: 'Update',
|
||||||
handler: 'updateParams',
|
handler: 'updateParams',
|
||||||
|
bind: {
|
||||||
|
disabled: '{livemode}',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -7,6 +7,8 @@ Ext.define('Proxmox.panel.NotesView', {
|
|||||||
bodyPadding: 10,
|
bodyPadding: 10,
|
||||||
scrollable: true,
|
scrollable: true,
|
||||||
animCollapse: false,
|
animCollapse: false,
|
||||||
|
collapseFirst: false,
|
||||||
|
|
||||||
maxLength: 64 * 1024,
|
maxLength: 64 * 1024,
|
||||||
enableTBar: false,
|
enableTBar: false,
|
||||||
onlineHelp: 'markdown_basics',
|
onlineHelp: 'markdown_basics',
|
||||||
@ -17,6 +19,7 @@ Ext.define('Proxmox.panel.NotesView', {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
text: gettext('Edit'),
|
text: gettext('Edit'),
|
||||||
|
iconCls: 'fa fa-pencil-square-o',
|
||||||
handler: function() {
|
handler: function() {
|
||||||
let view = this.up('panel');
|
let view = this.up('panel');
|
||||||
view.run_editor();
|
view.run_editor();
|
||||||
@ -109,7 +112,23 @@ Ext.define('Proxmox.panel.NotesView', {
|
|||||||
listeners: {
|
listeners: {
|
||||||
render: function(c) {
|
render: function(c) {
|
||||||
let me = this;
|
let me = this;
|
||||||
me.getEl().on('dblclick', me.run_editor, me);
|
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);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
afterlayout: function() {
|
afterlayout: function() {
|
||||||
let me = this;
|
let me = this;
|
||||||
@ -122,10 +141,11 @@ Ext.define('Proxmox.panel.NotesView', {
|
|||||||
|
|
||||||
tools: [
|
tools: [
|
||||||
{
|
{
|
||||||
type: 'gear',
|
glyph: 'xf044@FontAwesome', // fa-pencil-square-o
|
||||||
handler: function() {
|
tooltip: gettext('Edit notes'),
|
||||||
let view = this.up('panel');
|
callback: view => view.run_editor(),
|
||||||
view.run_editor();
|
style: {
|
||||||
|
paddingRight: '5px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
410
src/panel/NotificationConfigView.js
Normal file
410
src/panel/NotificationConfigView.js
Normal file
@ -0,0 +1,410 @@
|
|||||||
|
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`);
|
||||||
|
},
|
||||||
|
});
|
103
src/panel/SendmailEditPanel.js
Normal file
103
src/panel/SendmailEditPanel.js
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
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;
|
||||||
|
},
|
||||||
|
});
|
205
src/panel/SmtpEditPanel.js
Normal file
205
src/panel/SmtpEditPanel.js
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
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;
|
||||||
|
},
|
||||||
|
});
|
@ -67,8 +67,11 @@ Ext.define('Proxmox.panel.TfaView', {
|
|||||||
onLoad: function(store, data, success) {
|
onLoad: function(store, data, success) {
|
||||||
if (!success) return;
|
if (!success) return;
|
||||||
|
|
||||||
|
let now = new Date().getTime() / 1000;
|
||||||
let records = [];
|
let records = [];
|
||||||
Ext.Array.each(data, user => {
|
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 => {
|
Ext.Array.each(user.data.entries, entry => {
|
||||||
records.push({
|
records.push({
|
||||||
fullid: `${user.id}/${entry.id}`,
|
fullid: `${user.id}/${entry.id}`,
|
||||||
@ -77,6 +80,7 @@ Ext.define('Proxmox.panel.TfaView', {
|
|||||||
description: entry.description,
|
description: entry.description,
|
||||||
created: entry.created,
|
created: entry.created,
|
||||||
enable: entry.enable,
|
enable: entry.enable,
|
||||||
|
locked: tfa_locked || (entry.type === 'totp' && totp_locked),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -154,8 +158,10 @@ Ext.define('Proxmox.panel.TfaView', {
|
|||||||
|
|
||||||
renderUser: fullid => fullid.split('/')[0],
|
renderUser: fullid => fullid.split('/')[0],
|
||||||
|
|
||||||
renderEnabled: enabled => {
|
renderEnabled: function(enabled, metaData, record) {
|
||||||
if (enabled === undefined) {
|
if (record.data.locked) {
|
||||||
|
return gettext("Locked");
|
||||||
|
} else if (enabled === undefined) {
|
||||||
return Proxmox.Utils.yesText;
|
return Proxmox.Utils.yesText;
|
||||||
} else {
|
} else {
|
||||||
return Proxmox.Utils.format_boolean(enabled);
|
return Proxmox.Utils.format_boolean(enabled);
|
||||||
|
423
src/panel/WebhookEditPanel.js
Normal file
423
src/panel/WebhookEditPanel.js
Normal file
@ -0,0 +1,423 @@
|
|||||||
|
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();
|
||||||
|
},
|
||||||
|
});
|
@ -12,6 +12,12 @@
|
|||||||
// make the color transparent so the border doesn't appear visibly,
|
// make the color transparent so the border doesn't appear visibly,
|
||||||
// like in crisp, but keep to keep the layout intact
|
// like in crisp, but keep to keep the layout intact
|
||||||
border-color: transparent;
|
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 {
|
.x-window-header-title-default {
|
||||||
|
@ -33,6 +33,13 @@
|
|||||||
color: $icon-color;
|
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
|
// but some are implement as background SVGs or PNGs. invert them via
|
||||||
// filters:
|
// filters:
|
||||||
.fa-ceph::before,
|
.fa-ceph::before,
|
||||||
@ -104,6 +111,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// pbs show task log in longest task list column
|
// 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 {
|
.x-action-col-icon.fa-chevron-right::before {
|
||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
@ -222,6 +232,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// The usage icons dynamically displaying how full a storage is
|
||||||
.usage-wrapper {
|
.usage-wrapper {
|
||||||
border: 1px solid $icon-color;
|
border: 1px solid $icon-color;
|
||||||
|
@ -4,7 +4,8 @@ img[id^="proxmoxlogo-"][id$="-img"] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// removes the gray line in the header of the mail gateway
|
// removes the gray line in the header of the mail gateway
|
||||||
div[id^="versioninfo-"] + div[id^="panel-"] > div[id^="panel-"][id$="-bodyWrap"] > div {
|
div[id^="versioninfo-"] + div[id^="panel-"] > div[id^="panel-"][id$="-bodyWrap"] > div,
|
||||||
|
div.eol-notice + div[id^="panel-"] > div[id^="panel-"][id$="-bodyWrap"] > div {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
@ -95,14 +95,14 @@ Ext.define('Proxmox.window.ACMEPluginEdit', {
|
|||||||
let field = Ext.create({
|
let field = Ext.create({
|
||||||
xtype,
|
xtype,
|
||||||
name: `custom_${name}`,
|
name: `custom_${name}`,
|
||||||
fieldLabel: label,
|
fieldLabel: Ext.htmlEncode(label),
|
||||||
width: '100%',
|
width: '100%',
|
||||||
labelWidth: 150,
|
labelWidth: 150,
|
||||||
labelSeparator: '=',
|
labelSeparator: '=',
|
||||||
emptyText: definition.default || '',
|
emptyText: definition.default || '',
|
||||||
autoEl: definition.description ? {
|
autoEl: definition.description ? {
|
||||||
tag: 'div',
|
tag: 'div',
|
||||||
'data-qtip': definition.description,
|
'data-qtip': Ext.htmlEncode(Ext.htmlEncode(definition.description)),
|
||||||
} : undefined,
|
} : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -127,6 +127,7 @@ Ext.define('Proxmox.window.ACMEPluginEdit', {
|
|||||||
if (me.createdFields[key]) {
|
if (me.createdFields[key]) {
|
||||||
me.createdFields[key].setValue(value);
|
me.createdFields[key].setValue(value);
|
||||||
me.createdFields[key].originalValue = me.originalValues[key];
|
me.createdFields[key].originalValue = me.originalValues[key];
|
||||||
|
me.createdFields[key].checkDirty();
|
||||||
} else {
|
} else {
|
||||||
extradata.push(`${key}=${value}`);
|
extradata.push(`${key}=${value}`);
|
||||||
}
|
}
|
||||||
|
@ -220,5 +220,6 @@ ${keyString}
|
|||||||
|
|
||||||
printFrame.src = "data:text/html;base64," + btoa(html);
|
printFrame.src = "data:text/html;base64," + btoa(html);
|
||||||
document.body.appendChild(printFrame);
|
document.body.appendChild(printFrame);
|
||||||
|
me.on('destroy', () => document.body.removeChild(printFrame));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -224,10 +224,10 @@ Ext.define('Proxmox.window.AddTotp', {
|
|||||||
visible: '{!secretEmpty}',
|
visible: '{!secretEmpty}',
|
||||||
},
|
},
|
||||||
style: {
|
style: {
|
||||||
margin: '5px auto',
|
margin: '16px auto',
|
||||||
padding: '5px',
|
padding: '16px',
|
||||||
width: '266px',
|
width: '288px',
|
||||||
height: '266px',
|
height: '288px',
|
||||||
'background-color': 'white',
|
'background-color': 'white',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
14
src/window/AuthEditAD.js
Normal file
14
src/window/AuthEditAD.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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',
|
||||||
|
});
|
@ -1,5 +1,8 @@
|
|||||||
Ext.define('Proxmox.window.AuthEditBase', {
|
Ext.define('Proxmox.window.AuthEditBase', {
|
||||||
extend: 'Proxmox.window.Edit',
|
extend: 'Proxmox.window.Edit',
|
||||||
|
mixins: ['Proxmox.Mixin.CBind'],
|
||||||
|
|
||||||
|
showDefaultRealm: false,
|
||||||
|
|
||||||
isAdd: true,
|
isAdd: true,
|
||||||
|
|
||||||
@ -29,9 +32,9 @@ Ext.define('Proxmox.window.AuthEditBase', {
|
|||||||
|
|
||||||
let authConfig = Proxmox.Schema.authDomains[me.authType];
|
let authConfig = Proxmox.Schema.authDomains[me.authType];
|
||||||
if (!authConfig) {
|
if (!authConfig) {
|
||||||
throw 'unknown auth type';
|
throw `unknown auth type ${me.authType}`;
|
||||||
} else if (!authConfig.add && me.isCreate) {
|
} else if (!authConfig.add && me.isCreate) {
|
||||||
throw 'trying to add non addable realm';
|
throw `trying to add non addable realm of type ${me.authType}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
me.subject = authConfig.name;
|
me.subject = authConfig.name;
|
||||||
@ -51,7 +54,9 @@ Ext.define('Proxmox.window.AuthEditBase', {
|
|||||||
realm: me.realm,
|
realm: me.realm,
|
||||||
xtype: authConfig.ipanel,
|
xtype: authConfig.ipanel,
|
||||||
isCreate: me.isCreate,
|
isCreate: me.isCreate,
|
||||||
|
useTypeInUrl: me.useTypeInUrl,
|
||||||
type: me.authType,
|
type: me.authType,
|
||||||
|
showDefaultRealm: me.showDefaultRealm,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: gettext('Sync Options'),
|
title: gettext('Sync Options'),
|
||||||
@ -67,7 +72,9 @@ Ext.define('Proxmox.window.AuthEditBase', {
|
|||||||
realm: me.realm,
|
realm: me.realm,
|
||||||
xtype: authConfig.ipanel,
|
xtype: authConfig.ipanel,
|
||||||
isCreate: me.isCreate,
|
isCreate: me.isCreate,
|
||||||
|
useTypeInUrl: me.useTypeInUrl,
|
||||||
type: me.authType,
|
type: me.authType,
|
||||||
|
showDefaultRealm: me.showDefaultRealm,
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +93,7 @@ Ext.define('Proxmox.window.AuthEditBase', {
|
|||||||
// only check this when the type is not in the api path
|
// only check this when the type is not in the api path
|
||||||
if (!me.useTypeInUrl && data.type !== me.authType) {
|
if (!me.useTypeInUrl && data.type !== me.authType) {
|
||||||
me.close();
|
me.close();
|
||||||
throw "got wrong auth type";
|
throw `got wrong auth type '${me.authType}' for realm '${data.type}'`;
|
||||||
}
|
}
|
||||||
me.setValues(data);
|
me.setValues(data);
|
||||||
},
|
},
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
|
|
||||||
Ext.define('Proxmox.panel.LDAPInputPanelViewModel', {
|
Ext.define('Proxmox.panel.LDAPInputPanelViewModel', {
|
||||||
extend: 'Ext.app.ViewModel',
|
extend: 'Ext.app.ViewModel',
|
||||||
|
|
||||||
alias: 'viewmodel.pmxAuthLDAPPanel',
|
alias: 'viewmodel.pmxAuthLDAPPanel',
|
||||||
|
|
||||||
data: {
|
data: {
|
||||||
@ -23,6 +21,8 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
|
|||||||
xtype: 'pmxAuthLDAPPanel',
|
xtype: 'pmxAuthLDAPPanel',
|
||||||
mixins: ['Proxmox.Mixin.CBind'],
|
mixins: ['Proxmox.Mixin.CBind'],
|
||||||
|
|
||||||
|
showDefaultRealm: false,
|
||||||
|
|
||||||
viewModel: {
|
viewModel: {
|
||||||
type: 'pmxAuthLDAPPanel',
|
type: 'pmxAuthLDAPPanel',
|
||||||
},
|
},
|
||||||
@ -32,11 +32,11 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
|
|||||||
onlineHelp: 'user-realms-ldap',
|
onlineHelp: 'user-realms-ldap',
|
||||||
|
|
||||||
onGetValues: function(values) {
|
onGetValues: function(values) {
|
||||||
if (this.isCreate) {
|
if (this.isCreate && !this.useTypeInUrl) {
|
||||||
values.type = this.type;
|
values.type = this.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.anonymous_search) {
|
if (values.anonymous_search && !this.isCreate) {
|
||||||
if (!values.delete) {
|
if (!values.delete) {
|
||||||
values.delete = [];
|
values.delete = [];
|
||||||
}
|
}
|
||||||
@ -64,6 +64,12 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
|
|||||||
return values;
|
return values;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
cbindData: function(config) {
|
||||||
|
return {
|
||||||
|
isLdap: this.type === 'ldap',
|
||||||
|
isAd: this.type === 'ad',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
column1: [
|
column1: [
|
||||||
{
|
{
|
||||||
@ -76,19 +82,40 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
|
|||||||
fieldLabel: gettext('Realm'),
|
fieldLabel: gettext('Realm'),
|
||||||
allowBlank: false,
|
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',
|
xtype: 'proxmoxtextfield',
|
||||||
fieldLabel: gettext('Base Domain Name'),
|
fieldLabel: gettext('Base Domain Name'),
|
||||||
name: 'base-dn',
|
name: 'base-dn',
|
||||||
allowBlank: false,
|
|
||||||
emptyText: 'cn=Users,dc=company,dc=net',
|
emptyText: 'cn=Users,dc=company,dc=net',
|
||||||
|
cbind: {
|
||||||
|
hidden: '{!isLdap}',
|
||||||
|
allowBlank: '{!isLdap}',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xtype: 'proxmoxtextfield',
|
xtype: 'proxmoxtextfield',
|
||||||
fieldLabel: gettext('User Attribute Name'),
|
fieldLabel: gettext('User Attribute Name'),
|
||||||
name: 'user-attr',
|
name: 'user-attr',
|
||||||
allowBlank: false,
|
|
||||||
emptyText: 'uid / sAMAccountName',
|
emptyText: 'uid / sAMAccountName',
|
||||||
|
cbind: {
|
||||||
|
hidden: '{!isLdap}',
|
||||||
|
allowBlank: '{!isLdap}',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xtype: 'proxmoxcheckbox',
|
xtype: 'proxmoxcheckbox',
|
||||||
@ -103,7 +130,14 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
|
|||||||
fieldLabel: gettext('Bind Domain Name'),
|
fieldLabel: gettext('Bind Domain Name'),
|
||||||
name: 'bind-dn',
|
name: 'bind-dn',
|
||||||
allowBlank: false,
|
allowBlank: false,
|
||||||
emptyText: 'cn=user,dc=company,dc=net',
|
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'),
|
||||||
|
} : {},
|
||||||
|
},
|
||||||
bind: {
|
bind: {
|
||||||
disabled: "{anonymous_search}",
|
disabled: "{anonymous_search}",
|
||||||
},
|
},
|
||||||
@ -113,9 +147,9 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
|
|||||||
inputType: 'password',
|
inputType: 'password',
|
||||||
fieldLabel: gettext('Bind Password'),
|
fieldLabel: gettext('Bind Password'),
|
||||||
name: 'password',
|
name: 'password',
|
||||||
allowBlank: true,
|
|
||||||
cbind: {
|
cbind: {
|
||||||
emptyText: get => !get('isCreate') ? gettext('Unchanged') : '',
|
emptyText: get => !get('isCreate') ? gettext('Unchanged') : '',
|
||||||
|
allowBlank: '{!isCreate}',
|
||||||
},
|
},
|
||||||
bind: {
|
bind: {
|
||||||
disabled: "{anonymous_search}",
|
disabled: "{anonymous_search}",
|
||||||
@ -147,7 +181,9 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
|
|||||||
maxValue: 65535,
|
maxValue: 65535,
|
||||||
emptyText: gettext('Default'),
|
emptyText: gettext('Default'),
|
||||||
submitEmptyText: false,
|
submitEmptyText: false,
|
||||||
deleteEmpty: true,
|
cbind: {
|
||||||
|
deleteEmpty: '{!isCreate}',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xtype: 'proxmoxKVComboBox',
|
xtype: 'proxmoxKVComboBox',
|
||||||
@ -187,7 +223,7 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
|
|||||||
|
|
||||||
columnB: [
|
columnB: [
|
||||||
{
|
{
|
||||||
xtype: 'textfield',
|
xtype: 'proxmoxtextfield',
|
||||||
name: 'comment',
|
name: 'comment',
|
||||||
fieldLabel: gettext('Comment'),
|
fieldLabel: gettext('Comment'),
|
||||||
cbind: {
|
cbind: {
|
||||||
@ -195,7 +231,6 @@ Ext.define('Proxmox.panel.LDAPInputPanel', {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -295,16 +330,30 @@ Ext.define('Proxmox.panel.LDAPSyncInputPanel', {
|
|||||||
xtype: 'proxmoxtextfield',
|
xtype: 'proxmoxtextfield',
|
||||||
name: 'firstname',
|
name: 'firstname',
|
||||||
fieldLabel: gettext('First Name attribute'),
|
fieldLabel: gettext('First Name attribute'),
|
||||||
|
autoEl: {
|
||||||
|
tag: 'div',
|
||||||
|
'data-qtip': Ext.String.format(gettext('Often called {0}'), '`givenName`'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xtype: 'proxmoxtextfield',
|
xtype: 'proxmoxtextfield',
|
||||||
name: 'lastname',
|
name: 'lastname',
|
||||||
fieldLabel: gettext('Last Name attribute'),
|
fieldLabel: gettext('Last Name attribute'),
|
||||||
|
autoEl: {
|
||||||
|
tag: 'div',
|
||||||
|
'data-qtip': Ext.String.format(gettext('Often called {0}'), '`sn`'),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xtype: 'proxmoxtextfield',
|
xtype: 'proxmoxtextfield',
|
||||||
name: 'email',
|
name: 'email',
|
||||||
fieldLabel: gettext('E-Mail attribute'),
|
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',
|
xtype: 'displayfield',
|
||||||
@ -336,7 +385,9 @@ Ext.define('Proxmox.panel.LDAPSyncInputPanel', {
|
|||||||
xtype: 'proxmoxtextfield',
|
xtype: 'proxmoxtextfield',
|
||||||
name: 'user-classes',
|
name: 'user-classes',
|
||||||
fieldLabel: gettext('User classes'),
|
fieldLabel: gettext('User classes'),
|
||||||
deleteEmpty: true,
|
cbind: {
|
||||||
|
deleteEmpty: '{!isCreate}',
|
||||||
|
},
|
||||||
emptyText: 'inetorgperson, posixaccount, person, user',
|
emptyText: 'inetorgperson, posixaccount, person, user',
|
||||||
autoEl: {
|
autoEl: {
|
||||||
tag: 'div',
|
tag: 'div',
|
||||||
@ -347,7 +398,9 @@ Ext.define('Proxmox.panel.LDAPSyncInputPanel', {
|
|||||||
xtype: 'proxmoxtextfield',
|
xtype: 'proxmoxtextfield',
|
||||||
name: 'filter',
|
name: 'filter',
|
||||||
fieldLabel: gettext('User Filter'),
|
fieldLabel: gettext('User Filter'),
|
||||||
deleteEmpty: true,
|
cbind: {
|
||||||
|
deleteEmpty: '{!isCreate}',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -3,12 +3,14 @@ Ext.define('Proxmox.panel.OpenIDInputPanel', {
|
|||||||
xtype: 'pmxAuthOpenIDPanel',
|
xtype: 'pmxAuthOpenIDPanel',
|
||||||
mixins: ['Proxmox.Mixin.CBind'],
|
mixins: ['Proxmox.Mixin.CBind'],
|
||||||
|
|
||||||
|
showDefaultRealm: false,
|
||||||
|
|
||||||
type: 'openid',
|
type: 'openid',
|
||||||
|
|
||||||
onGetValues: function(values) {
|
onGetValues: function(values) {
|
||||||
let me = this;
|
let me = this;
|
||||||
|
|
||||||
if (me.isCreate) {
|
if (me.isCreate && !me.useTypeInUrl) {
|
||||||
values.type = me.type;
|
values.type = me.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,6 +37,21 @@ Ext.define('Proxmox.panel.OpenIDInputPanel', {
|
|||||||
fieldLabel: gettext('Realm'),
|
fieldLabel: gettext('Realm'),
|
||||||
allowBlank: false,
|
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',
|
xtype: 'proxmoxtextfield',
|
||||||
fieldLabel: gettext('Client ID'),
|
fieldLabel: gettext('Client ID'),
|
||||||
@ -112,7 +129,7 @@ Ext.define('Proxmox.panel.OpenIDInputPanel', {
|
|||||||
|
|
||||||
columnB: [
|
columnB: [
|
||||||
{
|
{
|
||||||
xtype: 'textfield',
|
xtype: 'proxmoxtextfield',
|
||||||
name: 'comment',
|
name: 'comment',
|
||||||
fieldLabel: gettext('Comment'),
|
fieldLabel: gettext('Comment'),
|
||||||
cbind: {
|
cbind: {
|
||||||
|
45
src/window/AuthEditSimple.js
Normal file
45
src/window/AuthEditSimple.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
36
src/window/ConsentModal.js
Normal file
36
src/window/ConsentModal.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
@ -29,6 +29,7 @@ Ext.define('Proxmox.window.DiskSmart', {
|
|||||||
text: 'ID',
|
text: 'ID',
|
||||||
dataIndex: 'id',
|
dataIndex: 'id',
|
||||||
width: 50,
|
width: 50,
|
||||||
|
align: 'right',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: gettext('Attribute'),
|
text: gettext('Attribute'),
|
||||||
@ -45,16 +46,19 @@ Ext.define('Proxmox.window.DiskSmart', {
|
|||||||
text: gettext('Normalized'),
|
text: gettext('Normalized'),
|
||||||
dataIndex: 'real-normalized',
|
dataIndex: 'real-normalized',
|
||||||
width: 60,
|
width: 60,
|
||||||
|
align: 'right',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: gettext('Threshold'),
|
text: gettext('Threshold'),
|
||||||
dataIndex: 'threshold',
|
dataIndex: 'threshold',
|
||||||
width: 60,
|
width: 60,
|
||||||
|
align: 'right',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: gettext('Worst'),
|
text: gettext('Worst'),
|
||||||
dataIndex: 'worst',
|
dataIndex: 'worst',
|
||||||
width: 60,
|
width: 60,
|
||||||
|
align: 'right',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: gettext('Flags'),
|
text: gettext('Flags'),
|
||||||
|
@ -31,6 +31,9 @@ Ext.define('Proxmox.window.Edit', {
|
|||||||
// custom submitText
|
// custom submitText
|
||||||
submitText: undefined,
|
submitText: undefined,
|
||||||
|
|
||||||
|
// custom options for the submit api call
|
||||||
|
submitOptions: {},
|
||||||
|
|
||||||
backgroundDelay: 0,
|
backgroundDelay: 0,
|
||||||
|
|
||||||
// string or function, called as (url, values) - useful if the ID of the
|
// string or function, called as (url, values) - useful if the ID of the
|
||||||
@ -66,6 +69,15 @@ Ext.define('Proxmox.window.Edit', {
|
|||||||
// onlineHelp of our first item, if set.
|
// onlineHelp of our first item, if set.
|
||||||
onlineHelp: undefined,
|
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() {
|
isValid: function() {
|
||||||
let me = this;
|
let me = this;
|
||||||
|
|
||||||
@ -151,7 +163,7 @@ Ext.define('Proxmox.window.Edit', {
|
|||||||
values = undefined;
|
values = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
Proxmox.Utils.API2Request({
|
let requestOptions = Ext.apply({
|
||||||
url: url,
|
url: url,
|
||||||
waitMsgTarget: me,
|
waitMsgTarget: me,
|
||||||
method: me.method || (me.backgroundDelay ? 'POST' : 'PUT'),
|
method: me.method || (me.backgroundDelay ? 'POST' : 'PUT'),
|
||||||
@ -191,7 +203,8 @@ Ext.define('Proxmox.window.Edit', {
|
|||||||
me.close();
|
me.close();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
}, me.submitOptions ?? {});
|
||||||
|
Proxmox.Utils.API2Request(requestOptions);
|
||||||
},
|
},
|
||||||
|
|
||||||
load: function(options) {
|
load: function(options) {
|
||||||
@ -305,19 +318,21 @@ Ext.define('Proxmox.window.Edit', {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let resetBtn = Ext.create('Ext.Button', {
|
let resetTool = Ext.create('Ext.panel.Tool', {
|
||||||
text: 'Reset',
|
glyph: 'xf0e2@FontAwesome', // fa-undo
|
||||||
disabled: true,
|
tooltip: gettext('Reset form data'),
|
||||||
handler: function() {
|
callback: () => form.reset(),
|
||||||
form.reset();
|
style: {
|
||||||
|
paddingRight: '2px', // just slightly more room to breathe
|
||||||
},
|
},
|
||||||
|
disabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
let set_button_status = function() {
|
let set_button_status = function() {
|
||||||
let valid = form.isValid();
|
let valid = form.isValid();
|
||||||
let dirty = form.isDirty();
|
let dirty = form.isDirty();
|
||||||
submitBtn.setDisabled(!valid || !(dirty || me.isCreate));
|
submitBtn.setDisabled(!valid || !(dirty || me.isCreate));
|
||||||
resetBtn.setDisabled(!dirty);
|
resetTool.setDisabled(!dirty);
|
||||||
};
|
};
|
||||||
|
|
||||||
form.on('dirtychange', set_button_status);
|
form.on('dirtychange', set_button_status);
|
||||||
@ -334,10 +349,10 @@ Ext.define('Proxmox.window.Edit', {
|
|||||||
me.title = Proxmox.Utils.dialog_title(me.subject, me.isCreate, me.isAdd);
|
me.title = Proxmox.Utils.dialog_title(me.subject, me.isCreate, me.isAdd);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (me.isCreate || !me.showReset) {
|
me.buttons = [submitBtn];
|
||||||
me.buttons = [submitBtn];
|
|
||||||
} else {
|
if (!me.isCreate && me.showReset) {
|
||||||
me.buttons = [submitBtn, resetBtn];
|
me.tools = [resetTool];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inputPanel && inputPanel.hasAdvanced) {
|
if (inputPanel && inputPanel.hasAdvanced) {
|
||||||
|
58
src/window/EndpointEditBase.js
Normal file
58
src/window/EndpointEditBase.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
@ -61,10 +61,6 @@ Ext.define("Proxmox.window.FileBrowser", {
|
|||||||
'd': true, // directories
|
'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
|
// prefix to prepend to downloaded file names
|
||||||
downloadPrefix: '',
|
downloadPrefix: '',
|
||||||
},
|
},
|
||||||
@ -126,7 +122,7 @@ Ext.define("Proxmox.window.FileBrowser", {
|
|||||||
view.lookup('selectText').setText(st);
|
view.lookup('selectText').setText(st);
|
||||||
|
|
||||||
let canDownload = view.downloadURL && view.downloadableFileTypes[data.type];
|
let canDownload = view.downloadURL && view.downloadableFileTypes[data.type];
|
||||||
let enableMenu = view.enableTar && data.type === 'd';
|
let enableMenu = data.type === 'd';
|
||||||
|
|
||||||
let downloadBtn = view.lookup('downloadBtn');
|
let downloadBtn = view.lookup('downloadBtn');
|
||||||
downloadBtn.setDisabled(!canDownload || enableMenu);
|
downloadBtn.setDisabled(!canDownload || enableMenu);
|
||||||
|
@ -12,6 +12,12 @@ Ext.define('Proxmox.window.LanguageEditWindow', {
|
|||||||
xclass: 'Ext.app.ViewController',
|
xclass: 'Ext.app.ViewController',
|
||||||
init: function(view) {
|
init: function(view) {
|
||||||
let language = Ext.util.Cookies.get(view.cookieName) || '__default__';
|
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);
|
this.getViewModel().set('language', language);
|
||||||
},
|
},
|
||||||
applyLanguage: function(button) {
|
applyLanguage: function(button) {
|
||||||
|
1374
src/window/NotificationMatcherEdit.js
Normal file
1374
src/window/NotificationMatcherEdit.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -7,27 +7,50 @@ Ext.define('Proxmox.window.PasswordEdit', {
|
|||||||
|
|
||||||
url: '/api2/extjs/access/password',
|
url: '/api2/extjs/access/password',
|
||||||
|
|
||||||
|
width: 380,
|
||||||
fieldDefaults: {
|
fieldDefaults: {
|
||||||
labelWidth: 120,
|
labelWidth: 150,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// 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: [
|
items: [
|
||||||
{
|
{
|
||||||
xtype: 'textfield',
|
xtype: 'textfield',
|
||||||
inputType: 'password',
|
inputType: 'password',
|
||||||
fieldLabel: gettext('Password'),
|
fieldLabel: gettext('Your Current Password'),
|
||||||
minLength: 5,
|
reference: 'confirmation-password',
|
||||||
|
name: 'confirmation-password',
|
||||||
|
allowBlank: false,
|
||||||
|
vtype: 'password',
|
||||||
|
cbind: {
|
||||||
|
hidden: '{!confirmCurrentPassword}',
|
||||||
|
disabled: '{!confirmCurrentPassword}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
xtype: 'textfield',
|
||||||
|
inputType: 'password',
|
||||||
|
fieldLabel: gettext('New Password'),
|
||||||
allowBlank: false,
|
allowBlank: false,
|
||||||
name: 'password',
|
name: 'password',
|
||||||
listeners: {
|
listeners: {
|
||||||
change: (field) => field.next().validate(),
|
change: (field) => field.next().validate(),
|
||||||
blur: (field) => field.next().validate(),
|
blur: (field) => field.next().validate(),
|
||||||
},
|
},
|
||||||
|
cbind: {
|
||||||
|
minLength: '{minLength}',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
xtype: 'textfield',
|
xtype: 'textfield',
|
||||||
inputType: 'password',
|
inputType: 'password',
|
||||||
fieldLabel: gettext('Confirm password'),
|
fieldLabel: gettext('Confirm New Password'),
|
||||||
name: 'verifypassword',
|
name: 'verifypassword',
|
||||||
allowBlank: false,
|
allowBlank: false,
|
||||||
vtype: 'password',
|
vtype: 'password',
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
/* Popup a message window
|
// Pop-up a message window where the user has to manually enter the resource ID to enable the
|
||||||
* where the user has to manually enter the resource ID
|
// destroy confirmation button to ensure that they got the correct resource selected for.
|
||||||
* to enable the destroy button
|
|
||||||
*/
|
|
||||||
Ext.define('Proxmox.window.SafeDestroy', {
|
Ext.define('Proxmox.window.SafeDestroy', {
|
||||||
extend: 'Ext.window.Window',
|
extend: 'Ext.window.Window',
|
||||||
alias: 'widget.proxmoxSafeDestroy',
|
alias: 'widget.proxmoxSafeDestroy',
|
||||||
|
@ -108,11 +108,11 @@ Ext.define('Proxmox.window.TaskViewer', {
|
|||||||
defaultValue: 'unknown',
|
defaultValue: 'unknown',
|
||||||
renderer: function(value) {
|
renderer: function(value) {
|
||||||
if (value !== 'stopped') {
|
if (value !== 'stopped') {
|
||||||
return value;
|
return Ext.htmlEncode(value);
|
||||||
}
|
}
|
||||||
let es = statgrid.getObjectValue('exitstatus');
|
let es = statgrid.getObjectValue('exitstatus');
|
||||||
if (es) {
|
if (es) {
|
||||||
return value + ': ' + es;
|
return Ext.htmlEncode(`${value}: ${es}`);
|
||||||
}
|
}
|
||||||
return 'unknown';
|
return 'unknown';
|
||||||
},
|
},
|
||||||
|
@ -45,11 +45,17 @@ Ext.define('Proxmox.window.TfaLoginWindow', {
|
|||||||
|
|
||||||
let lastTabId = me.getLastTabUsed();
|
let lastTabId = me.getLastTabUsed();
|
||||||
let initialTab = -1, i = 0;
|
let initialTab = -1, i = 0;
|
||||||
|
let count2nd = 0;
|
||||||
|
let hasRecovery = false;
|
||||||
for (const k of ['webauthn', 'totp', 'recovery', 'u2f', 'yubico']) {
|
for (const k of ['webauthn', 'totp', 'recovery', 'u2f', 'yubico']) {
|
||||||
const available = !!challenge[k];
|
const available = !!challenge[k];
|
||||||
vm.set(`availableChallenge.${k}`, available);
|
vm.set(`availableChallenge.${k}`, available);
|
||||||
|
|
||||||
if (available) {
|
if (available) {
|
||||||
|
count2nd++;
|
||||||
|
if (k === 'recovery') {
|
||||||
|
hasRecovery = true;
|
||||||
|
}
|
||||||
if (i === lastTabId) {
|
if (i === lastTabId) {
|
||||||
initialTab = i;
|
initialTab = i;
|
||||||
} else if (initialTab < 0) {
|
} else if (initialTab < 0) {
|
||||||
@ -58,15 +64,32 @@ Ext.define('Proxmox.window.TfaLoginWindow', {
|
|||||||
}
|
}
|
||||||
i++;
|
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);
|
view.down('tabpanel').setActiveTab(initialTab);
|
||||||
|
|
||||||
if (challenge.recovery) {
|
if (challenge.recovery) {
|
||||||
me.lookup('availableRecovery').update(Ext.String.htmlEncode(
|
if (!view.challenge.recovery.length) {
|
||||||
gettext('Available recovery keys: ') + view.challenge.recovery.join(', '),
|
me.lookup('recoveryEmpty').setVisible(true);
|
||||||
));
|
me.lookup('recoveryKey').setVisible(false);
|
||||||
me.lookup('availableRecovery').setVisible(true);
|
} else {
|
||||||
if (view.challenge.recovery.length <= 3) {
|
let idList = view
|
||||||
me.lookup('recoveryLow').setVisible(true);
|
.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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -351,6 +374,7 @@ Ext.define('Proxmox.window.TfaLoginWindow', {
|
|||||||
allowBlank: false,
|
allowBlank: false,
|
||||||
regex: /^[0-9]{2,16}$/,
|
regex: /^[0-9]{2,16}$/,
|
||||||
regexText: gettext('TOTP codes usually consist of six decimal digits'),
|
regexText: gettext('TOTP codes usually consist of six decimal digits'),
|
||||||
|
inputAttrTpl: 'autocomplete=one-time-code',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -363,6 +387,36 @@ Ext.define('Proxmox.window.TfaLoginWindow', {
|
|||||||
disabled: '{!availableChallenge.recovery}',
|
disabled: '{!availableChallenge.recovery}',
|
||||||
},
|
},
|
||||||
items: [
|
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',
|
xtype: 'box',
|
||||||
reference: 'availableRecovery',
|
reference: 'availableRecovery',
|
||||||
@ -379,16 +433,6 @@ Ext.define('Proxmox.window.TfaLoginWindow', {
|
|||||||
regex: /^[0-9a-f]{4}(-[0-9a-f]{4}){3}$/,
|
regex: /^[0-9a-f]{4}(-[0-9a-f]{4}){3}$/,
|
||||||
regexText: gettext('Does not look like a valid recovery key'),
|
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,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user