Compare commits

...

208 Commits

Author SHA1 Message Date
Thomas Lamprecht
68a3518d13 bump version to 4.3.7
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2025-02-26 19:16:22 +01:00
Markus Frank
a37094b0fa form: RealmComboBox: add option to change the API path
Signed-off-by: Markus Frank <m.frank@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2025-02-26 18:50:34 +01:00
Markus Frank
b4304e6691 panel: AuthView: change API path in pmx-domains model
Currently is always using the default path because it was hardcoded into
the model 'pmx-domains'. Introduce new variable storeBaseUrl because PBS
uses two different paths to access the realm/domains API.

Co-authored-by: Dominik Csapak <d.csapak@proxmox.com>
Signed-off-by: Markus Frank <m.frank@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2025-02-26 18:50:29 +01:00
Markus Frank
ee9d92e37e fix: window: AuthEditBase: rename variable 'realm' to 'type'
PVE/PMG API returns a variable called 'type' instead of 'realm'

Fixes: 3822a03 ("window: AuthEditBase: include more information in thrown errors")
Signed-off-by: Markus Frank <m.frank@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2025-02-26 18:50:29 +01:00
Thomas Lamprecht
2df5c28d3f bump version to 4.3.6
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2025-02-25 18:10:10 +01:00
Dominik Csapak
f6440d3ee1 object grid: fix onlineHelp setting from editorConfig for row editors
In our row editors helpers, we unconditionally set onlineHelp from
'opts.onlineHelp', even if it's undefined.

Later we use 'Ext.apply' to set first the editorConfig defaults, then
the 'rowdef.editor' settings. In javascript, the objects

{} and
{ foo: undefined }

are not the same, so Ext.apply overwrites the default from editorConfig
with that from the row definition, also for undefined.

This means if we have a default onlineHelp in editorConfig and none in
the add_*_row options, we would not show it.

To fix it, check if 'opts.onlineHelp' is truthy before setting it in
the row definition. This should not happen for other options used
from the row helper options, since those are nested
(Ext.apply does not work recursively)

This fixes a regression in pmg-gui, where we set a default onlineHelp
for e.g. the Mail Proxy Options which would not show up anymore.

Note: PMG is the only product where we used this pattern, so this
was not visible anywhere in PVE or PBS.

Fixes: 7d16f8b (object grid: allow to pass online help to row editors)
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
Reviewed-By: Stoiko Ivanov <s.ivanov@proxmox.com>
Tested-By: Stoiko Ivanov <s.ivanov@proxmox.com>
2025-02-25 18:07:02 +01:00
Thomas Lamprecht
15a62419a1 bump version to 4.3.5
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2025-02-25 17:08:17 +01:00
Hannes Duerr
23289264bd form: add field component for certificate fingerprints
Create a new component for (SHA256) certificate fingerprints as we can
reuse this in several places.

Signed-off-by: Hannes Duerr <h.duerr@proxmox.com>
 [TL: drop filenames from commit message and fix widget alias name]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2025-02-25 17:04:22 +01:00
Maximiliano Sandoval
41c844b511 fix #6088: notification: matcher: use more descriptive strings
Add separate text for inverted and non-inverted cases, this should
reduce some confusion about how inversion works and allow for better
translations.

Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
 [TL: expand commit message.]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2025-01-28 15:21:02 +01:00
Fabian Grünbichler
82260afcdd changelog: s/UNRELEASED/bookworm
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2025-01-20 15:09:06 +01:00
Fabian Grünbichler
80b01f27bc bump version to 4.3.4
Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2025-01-20 11:40:02 +01:00
Friedrich Weber
88d62e5386 acme: plugin: add htmlEncodes
to avoid interpreting html tags in the values.

When setting a tooltip via the `data-qtip` attribute, the contents
needs to be double-encoded. Ext.js internally uses `innerHTML` to
apply the generated HTML, which undoes one layer of encoding in case
of `data-qtip`.

Signed-off-by: Friedrich Weber <f.weber@proxmox.com>
2025-01-20 11:37:06 +01:00
Friedrich Weber
f26346a597 apt: add missing htmlEncode to qtip
to avoid interpreting html tags in the values.

When setting a tooltip via the `data-qtip` attribute, the contents
needs to be double-encoded. Ext.js internally uses `innerHTML` to
apply the generated HTML, which undoes one layer of encoding in case
of `data-qtip`.

Signed-off-by: Friedrich Weber <f.weber@proxmox.com>
2025-01-20 11:37:06 +01:00
Dominik Csapak
9d5d6a7815 task viewer: htmlEncode the status
so we don't accidentally interpret html tags from the task status

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2025-01-20 11:37:01 +01:00
Dominik Csapak
7bb124c036 button: htmlEncode the name/id for the confirm message
so we don't accidentally interpret html tags

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2025-01-20 11:37:01 +01:00
Dominik Csapak
867bf7e6f5 utils: htmlEncode status text
so we don't accidentally interpret html tags in the output

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2025-01-20 11:37:01 +01:00
Maximiliano Sandoval
d710ceeeb6 webhook edit: do not split translatable strings
Splitting translatable strings deprives translators for context which
might be needed for getting the correct tenses or genders. See [1] for
example.

[1] https://wiki.gnome.org/TranslationProject(2f)DevGuidelines(2f)Never(20)split(20)sentences.html

Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
2025-01-15 17:19:59 +01:00
Gabriel Goller
d6d173f05d textarea field: add emptyText message to show markdown is supported
Just like in our `Notes` fields show a emptyText message that explains
that markdown can be used.

Reported-by: Lukas Wagner <l.wagner@proxmox.com>
Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
2024-11-27 15:31:57 +01:00
Thomas Lamprecht
a5fb3afcf9 bump version to 4.3.3
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-11-27 12:25:47 +01:00
Fiona Ebner
ac10db95ef display-edit field: add emptyText getter and setter to support data-bind
To access the underlying fields a reference to them is now explicitly
saved after component initialisation.

First user is intended to be the path field for datastores where the
emptyText should dynamically be for a relative or absolute path.

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
 [ TL: switch over from saving the xtype to query in the getter or
   setter to directly savign a reference to the underlying fields. ]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-11-27 12:17:10 +01:00
Thomas Lamprecht
c806b73ff7 bump version to 4.3.2
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-11-25 18:32:26 +01:00
Gabriel Goller
1d07d61a18 form: add support for multiline textarea
This adds support for a editable multiline textarea in the ObjectGrid.
Now we can add a textarea row, which will open a textarea popup, and
encode the multi-line text into an base64 string (with utf8 support).

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
 [ TL: various style, naming fixes and add fieldOps to allow passing,
   e.g., an empty text]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-11-25 18:16:12 +01:00
Gabriel Goller
70382e111a window: add consent modal
Add consentModal that gets displayed before the login. Simply shows the
text in a scrollable box and contains a single button "OK".

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
2024-11-25 16:32:25 +01:00
Thomas Lamprecht
e79a20a8cc node: service state: restore original behavior
This reverts commit 461298d80a and
commit 4ca542b811 to get back to the
original state, which is already good enough here, as systemd unit
states are simply not translatable well, and are never translated by
systemd, so doing that will make most users life actually harder.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-11-19 15:39:41 +01:00
Christian Ebner
461298d80a utils: never translate systemd unit states
Translating unit states might be counter productive in case of
debugging, opt for not translating them and drop usage of the
translation helper.

This was spotted by several devs on doing translations in languages
they know, so it really seems not right to add.

Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
 [ TL: add last sentence to add some weight to not doing that ]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-11-19 15:29:38 +01:00
Thomas Lamprecht
2686922392 bump version to 4.3.1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-11-19 12:40:09 +01:00
Thomas Lamprecht
3375d11ea4 auth view/edit: make default-view checkbox opt-in
To avoid suggesting that a project has a feature that is not
implemented in the backend.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-11-19 12:37:26 +01:00
Thomas Lamprecht
0988db8ffc network view: pass generic editOptions config to edit window
Avoid the need to loop through every product specific feature
enablement, rather allow one to pass a generic object including them.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-11-16 16:37:15 +01:00
Thomas Lamprecht
358b98bf4f network edit: rename bridge_set_vids to enableBridgeVlanIds
we use camelCase in the JavaScript code and besides casing the old
name wasn't really telling and suggested that it was there to set a
specific VID not to enable setting them.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-11-16 16:35:51 +01:00
Thomas Lamprecht
2aa5d7e7ba network edit: shorten bridge VLAN ID label and tooltip
This is certainly a bit subjective, but IMO not much information is
lost, and the "space-separated" in the tooltip was never true (at
least for the backend), so might be even confusing; the example should
be enough to direct users in the right direction, real docs in the
documentation would be way better anyway than all those subtle UI
hints.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-11-16 16:33:15 +01:00
Thomas Lamprecht
ffe41ad5e3 network edit: shorten and improve bridge vlan ID validator
This does a few things, but all affecting the same validator and there
would not be much value with separate commits, so just use one.

Namely:
- reduce code by a lot, mostly by having a explicit ordered if-else
  chain that avoids the need for some extra checks as further branches
  can assume that former did not evaluate to true, thus we cans safe
  the closure that checked invalidity for a range-atom.
  While this allows entering "useless" ranges like "2-2" it's not
  clear why that should be a disallowed range, it's perfectly clear
  about what it represents.
- use Number.isNaN to avoid oddities from global isNaN that MDN warns
  against [0]
- give explicit error messages for different failure cases like
  out-of-range or not-a-number
- place error messages under gettext, our translators frequently ask
  to avoid untranslatable literals.
- support the same lists as the backend does, i.e. allow multiple
  whitespace and comma and semicolon as separators.

[0]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isNaN#description

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-11-16 16:30:30 +01:00
Aaron Lauterer
b1a3eb7195 fix #3892: network: add bridge vids field for bridge_vids
The new optional bridge_vids field allows to set that property via the
GUI. Since the backend needs to support it, the field needs to be
explicitly enabled.

For now, Proxmox VE (PVE) is the use case.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
2024-11-15 18:57:28 +01:00
Shannon Sterz
9d2fc36f61 dark theme: make icons in the permissions tree in pve dark
these icons are multiply style because they seem to be handled
differently across products. so this in essence "double inverts" them
in the context where that is needed

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
2024-11-15 16:49:17 +01:00
Thomas Lamprecht
afb219dce0 add Bulgarian as available language
Got recently contributed to proxmox-i18n in commit 8ee24fd ("add
Bulgarian translations")

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-11-14 22:23:50 +01:00
Dominik Csapak
e84f80271d webhook edit: display validity for added key/value fields
by calling 'isValid()' once the widget is attached to the grid

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-11-12 17:42:36 +01:00
Dominik Csapak
e872ed40f7 webhook edit: add emptytext to key-value fields
namely 'Key' and 'Value'

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-11-12 17:42:36 +01:00
Dominik Csapak
b0eafb68a9 webhook edit: use type in add button text
so one can more easily see what gets added.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-11-12 17:42:36 +01:00
Dominik Csapak
3052c4dfc8 webhook edit: make items config not static
modifying static elements from the class, like done here with e.g.

 me.items[0][key] = value;

is dangerous, since it directly modifies the class definition of those
arrays/objects.

Instead move the definition in initComponent, which uses a fresh
declaration each time the component is initialized.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-11-12 17:42:36 +01:00
Dominik Csapak
20002254e6 webhook edit: improve layout and component hierarchy
* instead of manually setting margin/paddings and the fieldLabel, just
  use a FieldContainer instead of Container. That implements the
  Ext.form.Labelable mixin, which correctly positions the label. This
  also has the effect that the labels are now styled correctly.

* modify the margins to get a consistent spacing between fields

* reverse the order of grid/button, to be consistent with our other
  grids with this input pattern

* make the label of the textarea a proper fieldLabel with a
  FieldContainer, which gets rid of the ':' in the gettext and
  styles the label correctly.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-11-12 17:42:36 +01:00
Christoph Heiss
0f8d38b5de fix #5379: window: AuthEdit{LDAP, OpenId}: add 'Default realm' checkbox
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
2024-11-11 23:58:34 +01:00
Christoph Heiss
6fe81f0cef fix #5379: panel: AuthView: add column displaying whether the realm is default
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
2024-11-11 23:58:34 +01:00
Christoph Heiss
4b3850bf45 schema: make PAM realm editable using new AuthSimple panel
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
2024-11-11 23:58:34 +01:00
Christoph Heiss
a285fec7e4 window: add panel for editing simple, built-in realms
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
2024-11-11 23:58:34 +01:00
Christoph Heiss
b4b36ef29a panel: AuthView: use help link from schema if set
This can be used to set the `onlineHelp` identifier in the schema as
opposed to in the panel directly. Needed e.g. to share a panel between
PAM and PBS realm.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
2024-11-11 23:57:46 +01:00
Christoph Heiss
be8cb1efb4 panel: AuthView: make useTypeInUrl property per-realm
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
2024-11-11 23:57:46 +01:00
Christoph Heiss
3822a031dd window: AuthEditBase: include more information in thrown errors
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
2024-11-11 23:57:46 +01:00
Thomas Lamprecht
5156cd1164 bump version to 4.3.0
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-11-11 21:58:01 +01:00
Timothy Nicholson
8f2c09235c fix #5831: ui: right-align s.m.a.r.t numerical table data
Signed-off-by: Timothy Nicholson <t.nicholson@proxmox.com>
2024-11-11 21:39:49 +01:00
Shannon Sterz
96fb6cfb72 password edit: add a minimum length parameter
so products can independently specify the minimum length of new
passwords

Signed-off-by: Shannon Sterz <s.sterz@proxmox.com>
2024-11-11 21:38:34 +01:00
Timothy Nicholson
4ca542b811 fix #5836: ui: translate systemd states in ServiceView
Signed-off-by: Timothy Nicholson <t.nicholson@proxmox.com>
2024-11-11 21:34:54 +01:00
Daniel Herzig
a351d3d36d fix #5611: node service view: hide non-installed services
This patch adds a filter to identify services, which are reported as
'not-found' by the api. By default they will not be shown in the UI
anymore, but visibility can still be toggled using a new checkbox.

Signed-off-by: Daniel Herzig <d.herzig@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-11-11 21:28:34 +01:00
Lukas Wagner
0ad5712ac7 notification: add UI for adding/updating webhook targets
The widgets for editing the headers/secrets were adapted from
the 'Tag Edit' dialog from PVE's datacenter options.

Apart from that, the new dialog is rather standard. I've decided
to put the http method and url in a single row, mostly to
save space and also to make it analogous to how an actual http request
is structured (VERB URL, followed by headers, followed by the body).

The secrets are a mechanism to store tokens/passwords in the
protected notification config. Secrets are accessible via
templating in the URL, headers and body via {{ secrets.NAME }}.
Secrets can only be set/updated, but not retrieved/displayed.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Tested-By: Stefan Hanreich <s.hanreich@proxmox.com>
2024-11-10 18:03:15 +01:00
Gabriel Goller
ffeb8004fb utils: add base64 conversion helper
Add helper functions to convert from a utf8 string to a base64 string
and vice-versa. Using the TextEncoder/TextDecoder we can support unicode
such as emojis as well [0].

[0]: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem

Signed-off-by: Gabriel Goller <g.goller@proxmox.com>
Reviewed-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-11-10 18:01:44 +01:00
Dominik Csapak
f18c0d3636 css: add some conditions to the tag classes for the tag view
in the tag view, we have a custom 'full' style in a place where we
can have another tagstyle class above. to compensate for that, we have
to add another condition to those styles, namely that there is not the
'proxmox-tags-full' in between.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-11-10 11:30:00 +01:00
Thomas Lamprecht
6af66f9626 bump version to 4.2.4
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-10-16 18:55:24 +02:00
Dominik Csapak
9e69d726e0 fix external linking to products by setting cookie SameSite attribute to lax
We introduced the 'strict' setting when browsers warned about our
cookies not having any SameSite setting [0]. While this works in
general, it had an unforeseen side effect:

When opening a link to the web UI of Proxmox projects, any existing
cookie does not get sent on the initial page load due to coming from
another origin. This then leads to the username and CSRF prevention
token not being set in the index response.
The UI code interprets this as the user being logged out (e.g. because
the ticket is not valid) and clears the cookie, displaying the login
window, even if the cookie's ticket value was still valid.

The MDN reference[1] says that setting it to 'lax' is similar to
'strict', but sends the cookie when navigating *to* our origin even
from other sites, which is what we want when linking from elsewhere.
(This would have also been the default if we wouldn't have set any
attribute).

[0]: https://lore.proxmox.com/pve-devel/20230315162630.289768-1-m.carrara@proxmox.com/
[1]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#SameSite_attribute

Fixes: aec7e8d ("toolkit/utils: set SameSite attr of auth cookie to 'strict'")
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-10-15 15:25:14 +02:00
Dominik Csapak
51e64f26ec css: dark theme: fix panel borders for pmg eol theme
since the header markup is different than what we use in pve, the
'spacer' elements in the pmg header get a (different colored) border by
default.

We already overwrite it for the first spacer, but when we show an EOL
notice, we have two of them, so add the second one to the rule too.
This also uses the 'css next sibling selector', just for the sibling of
the div with the 'eol-notice' class instead of the 'versioninfo' one.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-09-26 11:14:28 +02:00
Lukas Wagner
f420a35cb7 notification: matcher: move match-severity fields to panel
Also introduce a local viewModel that is linked to a parent viewModel,
allowing us to move the formulas to the panel.
This should make the code more cohesive and easier to follow.

No functional changes.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Tested-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
2024-09-23 17:09:32 +02:00
Lukas Wagner
fa315d842b notification: matcher: move match-calendar fields to panel
Also introduce a local viewModel that is linked to a parent viewModel,
allowing us to move the formulas to the panel.
This should make the code more cohesive and easier to follow.

No functional changes.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Tested-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
2024-09-23 17:09:32 +02:00
Lukas Wagner
e91269d513 notification: matcher: move match-field formulas to local viewModel
This should make the code more cohesive and easier to follow.

No functional changes.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Tested-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
2024-09-23 17:09:32 +02:00
Lukas Wagner
043ce82954 notification: matcher: match-field: show known fields/values
These changes introduce combogrid pickers for the 'field' and 'value'
form elements for 'match-field' match rules. The 'field' picker shows
a list of all known metadata fields, while the 'value' picker shows a
list of all known values, filtered depending on the current value of
'field'.

The list of known fields/values is retrieved from new API endpoints.
Some values are marked 'internal' by the backend. This means that the
'value' field was not user-created (counter example: backup job
IDs) and can therefore be used as a base for translations.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Tested-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
Reviewed-by: Max Carrara <m.carrara@proxmox.com>
2024-09-23 17:09:32 +02:00
Thomas Lamprecht
1ed4b715bc bump version to 4.2.3
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-25 11:45:15 +02:00
Dominik Csapak
68cea10fa7 realm edit: don't send 'delete' parameter when creating new entry
since tha api does not expect a 'delete' parameter here

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-04-25 11:42:51 +02:00
Dominik Csapak
a5fa465796 realm edit: don't send type as extra parameter when 'useTypeInUrl' is set
in that case, the type is already part of the url and we must not send
it additionally as an parameter

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-04-25 11:42:51 +02:00
Thomas Lamprecht
10f804941c bump version to 4.2.2
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-24 21:44:17 +02:00
Stefan Lendl
3251f4042b form: include vlan field widget from PVE
Copied from PVE to use in PBS network configuration.

Signed-off-by: Stefan Lendl <s.lendl@proxmox.com>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@proxmox.com>
Tested-by: Folke Gleumes <f.gleumes@proxmox.com>
2024-04-24 21:43:06 +02:00
Thomas Lamprecht
2d98e03d7c bump version to 4.2.1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-23 19:33:24 +02:00
Lukas Wagner
f95fcc26b6 sendmail: smtp: allow one to override the default mail author
In PBS, we obviously don't have "Proxmox VE" as a default sender, so
we need a mechanism to change the default author.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Tested-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-23 19:33:10 +02:00
Maximiliano Sandoval
3a4432a6e4 fix #5251: tfa: set autocomplete on tfa input form
Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
2024-04-23 17:10:43 +02:00
Thomas Lamprecht
af27e81747 bump version to 4.2.0
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-21 12:31:57 +02:00
Stefan Hanreich
9ffe9009fb network edit: allow bridges to have any valid interface name
Allow the web UI to accept bridge interfaces with any valid interface
name, rather than being limited to the arbitrary "vmbr" prefix.

Limiting to at most 10 characters, since SDN possibly adds a .XXXX
prefix for Vlans. Since the hard limit for network interface names is
15 characters, limiting it to 10 characters here enables SDN to append
the VLAN prefix in any case.

Originally-by: Jillian Morgan <jillian.morgan@primordial.ca>
Reviewed-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
Signed-off-by: Stefan Hanreich <s.hanreich@proxmox.com>
 [ TL: avoid far away intermediate variable ]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-21 11:54:05 +02:00
Thomas Lamprecht
2e454014e0 gitignore: add more build artefacts to ignore list
ignore the assembled widget toolkit, the CSS generated from SCSS and
some more packaging related files

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-21 09:56:55 +02:00
Thomas Lamprecht
82bb667f05 edit window: fixup syntax mis-hap
argh, it's was a bit to late for the previous clean-up and I forgot to
drop a closing parenthesis after reworking the if expression (and did
test again...).

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-20 20:43:21 +02:00
Thomas Lamprecht
1689e53f6f window: edit: simplify setting submit button
Since the reset-form functionality got moved into the header tools in
commit 046ec35 ("fix #5277: move reset button into window header
toolbar") we can unconditionally set the initial buttons, as those
will always just contain the submit one now.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-20 20:41:11 +02:00
Thomas Lamprecht
f646c22a67 notes view: make opening the editor on double-click opt-in
One can get some smart-selection behavior when double clicking text in
browsers, e.g., whole-word selection, and the notes view is generally
for having some text that is often copied, like hostnames or IP
addresses.

Opening the notes editor on double click is interfering with that
select+copy workflow, so instead of hard-coding that make it opt-in,
controlled by a setting from the browser-local storage.

Add some handling to cope with live-changes to that setting, as having
to re-open a panel to make it take effect is annoying and might make
people believe that this is buggy.

This new setting has (currently) to be handled by the per-product UI.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-20 17:26:48 +02:00
Thomas Lamprecht
bb5cc876ef notes view: use pencil-square-o icon for opening the editor
The gears one from the native tool with the same name is not really
telling and is normally rather used for options, not editing a
specific (notes) field.

So go for the edit pencil that indicates editing some field and use
that for both the edit button in the top bar and the tool one in the
title header bar.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-20 17:19:33 +02:00
Thomas Lamprecht
d958d57e0d notes view: place collapse tool on the right
so it's always at the end for both the collapsed and expanded state

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-20 17:19:24 +02:00
Thomas Lamprecht
046ec35b11 fix #5277: move reset button into window header toolbar
The 'Reset' button, which can be used to reset the form to its
original values when editing an existing resource or property, was
located at the right to the submit button since its initial addition
(before the git epoch started at Proxmox).

As it had the exact same form and color as the 'OK' submit button, it
is easy to press by accident, which then resets the pending changes
one wanted to submit – while not catastrophic it's just needlessly bad
UX.

As this UX-mishap is something one gets used too relatively fast,
especially as developer due to frequently opening such dialogues to
test changes, its something that mostly newer users will run into.
Luckily one took the effort to actually open an enhancement request,
providing ample resources to underline their point.

While there where quite a few proposals to improve this, most of them
had some (smaller) disadvantage (e.g., potentially jumping location,
confusion with other buttons like the help one).

Moving the reset functionality as as icon-only + tooltip button into
the window header title bar was the proposal that had no real
disadvantage and solved the underlying UX issue by cleanly separating
submit from reset. Having reset near the close-window tool has no
negative implications, as both have a similar effect, the discard the
current pending changes that the user did not yet submit, so if one
mistakenly hits close instead of reset, or vice-versa, nothing is
lost.
A nice side-benefit of that option is that the change is really small
code wise.

Closes: #5277
Reported-by:  Tristan Harward <trisweb@gmail.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-20 17:19:24 +02:00
Thomas Lamprecht
b1df1f0efb css: correctly mask disabled elements inside headers
The mask ExtJS uses to add a disabled look is using the general
default background color of panels as base color, i.e. white for light
mode and almost black for the dark mode.

But as the top header of windows uses a darker variant, having a mask
applied on some icons that is rendered directly in the header, without
any (button) element that provides its own background color, will make
that element show off.

This mostly happens for Tools, which we do not have many besides the
"Close" on, which is almost never disabled.

This was noticed when trying out to move the reset button inside the
window header tool bar, when that was disabled (e.g., form was not
dirty) it stuck out quite a bit in an odd way.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-20 17:19:24 +02:00
Thomas Lamprecht
48830b9713 window: whitespace indentation fix
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-20 17:19:24 +02:00
Thomas Lamprecht
43b66893a8 buildsys: also clean proxmox-dark folder in src clean target
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-20 17:19:24 +02:00
Thomas Lamprecht
9afdea76cf safe destroy window: rework top-level comment
avoid odd wrap, this isn't a poem, and add some more context.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-20 17:19:24 +02:00
Thomas Lamprecht
537702e12f eol notice: escalate to warning only shortly before EOL
The EOL message is positioned quite noticeable already in all our
products, being always visible. So use a notice-style for the icon
and its color until three weeks before EOL, when we switch to a
critical warning.

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

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

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

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-16 13:04:12 +02:00
Thomas Lamprecht
120b530a31 notify: shorten text to "reset" for built-in targets
While the difference between Reset and Remove is a bit more subtle
this also leads to less jumping around of UI elements on the right to
it (we normally avoid such size-changes that cause layout changes
completely).

Also, the confirmation message is quite telling, so this is not too
bad.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-10 12:18:58 +02:00
Lukas Wagner
83bae06cae notify: change 'Remove' button to 'Reset to default' for built-ins
A HTTP DELETE for a built-in target/matcher acts as a reset to its
defaults. This patch changes the 'Remove' button text based on the
selected target/matcher. If it is a built-in, the button text is
changed to 'Reset to default'. Also, if the built-in is not actually
modified, the button is disabled.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2024-04-10 12:12:30 +02:00
Lukas Wagner
26e2aa7ce4 remove button: allow to set custom confirmation message
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2024-04-10 12:12:30 +02:00
Maximiliano Sandoval
2834a05d2b i18n: mark strings as translatable
Note that N/A is already translatable in other places.

Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
2024-04-10 12:09:04 +02:00
Friedrich Weber
502c84b1ee window: edit: avoid sharing custom config objects between subclasses
Currently, `Proxmox.window.Edit` initializes `extraRequestParams` and
`submitOptions` to two objects that, if not overwritten, are shared
between all instances of subclasses. This bears the danger of
modifying the shared object in a subclass instead of overwriting it,
which affects all edit windows of the current session and can cause
hard-to-catch GUI bugs.

One such bug is the following: Currently, the `PVE.pool.AddStorage`
component inadvertently adds `poolid` to an `extraRequestParams`
object that is shared between all instances of `Proxmox.window.Edit`.
As a result, after adding a storage to a pool, opening any edit window
will send a GET request with a superfluous `poolid` parameter and
cause an error in the GUI:

> Parameter verification failed. (400)
> poolid: property is not defined in schema and the schema does not
> allow additional properties

This breaks all edit windows of the current session. A workaround is
to reload the current browser session.

To avoid this class of bugs in the future, implement a constructor
that makes copies of `extraRequestParams` and `submitOptions`. This
ensures that any subclass instance modifies only its own copies, and
modifications do not leak to other subclass instances.

Suggested-by: Stefan Sterz <s.sterz@proxmox.com>
Suggested-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
Signed-off-by: Friedrich Weber <f.weber@proxmox.com>
Tested-by: Stefan Sterz <s.sterz@proxmox>
2024-04-10 10:35:18 +02:00
Stefan Sterz
2b6e8c67a6 dark-mode: set intentionally black icons to $icon-color
some icons intentionally use black as their color in the light theme.
this includes the little pencil and check mark icon in the acme
overview. change their color to the regular dark-mode icon-color. for
this to work the filter inversion needed for some other icons needs to
be removed too.

Signed-off-by: Stefan Sterz <s.sterz@proxmox.com>
2024-04-10 10:28:02 +02:00
Thomas Lamprecht
4a5dd69899 update IFrame class to ExtJS 7.0 implantation
To get rid of some outdated event handlers like `beforeunload` which
Chromium based browsers are deprecating this year [0].

For those wondering about why we do not use ExtJS implementation
directly here it might be worth adding that the `Ext.ux` name space is
build to a separate file that has a (minified!) size of almost 160
KiB, and we only use a handful of those, so copying is a lot cheaper.

[0]: https://developer.chrome.com/docs/web-platform/deprecating-unload

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-04-01 16:27:18 +02:00
Christoph Heiss
da210b58a7 window: ldap: add tooltips for firstname, lastname and email attributes
Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
2024-03-25 17:15:24 +01:00
Christoph Heiss
4aff870f6c window: add Active Directory auth panel
As AD realms are mostly just LDAP, reuse the LDAP panel and just
show/hide some elements based on the type.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
2024-03-25 17:15:24 +01:00
Thomas Lamprecht
f60fe4ad99 bump version to 4.1.5
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-03-21 17:40:57 +01:00
Thomas Lamprecht
a5d6c92f60 window: password edit: clarify labels
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-03-21 17:33:24 +01:00
Wolfgang Bumiller
be5b9a4393 window: password edit: add opt-in confirmation-password field
For when the product UI using this component wants to show an extra
confirmation field where the user that executes the password change,
have to confirm their own password.

Reported-by: Wouter Arts <security@wth-security.nl>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
 [ TL: use already included CBind mixin instead of constructor ]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-03-21 17:30:27 +01:00
Dominik Csapak
d35cb8aa7c utils: api request: defer masking after layout
Since recently (not sure when exactly), the 'load()' method of the
edit window did not correctly mask the window anymore

The reason seems to be that the API2Request tries to mask the
component before it's rendered, and that did never work correctly.

Instead of simply calling `setLoading`, test if the component is
rendered, and if not, mask it after it has finished it's layout.

Since we cannot guarantee that there is only one API2Request with the
waitMsgTarget set to it, nor that the 'afterlayout' and api call
responses come in a specific order, we count the loads, and only
ever unmask the component when the counter reaches zero again.

Since we're strictly in non-async code here and JavaScript is
single-threaded, this should not result in a data race.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-03-19 18:05:13 +01:00
Maximiliano Sandoval
461bb2e56c certificates: removal prompt: don't display name if there is no name
The default certificate does not have a name, which caused this to
display an undefined text in the prompt.

Reported-by: Dietmar Maurer <dietmar@proxmox.com>
Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
 [ TL: drop useless instance of calling format, keep arrow-fn ]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-03-07 15:30:50 +01:00
Stefan Lendl
8d95122d85 gitignore: sort gitignore
Signed-off-by: Stefan Lendl <s.lendl@proxmox.com>
2024-03-07 15:23:31 +01:00
Stefan Lendl
a5b07f65ca gitignore: build outputs
Signed-off-by: Stefan Lendl <s.lendl@proxmox.com>
2024-03-07 15:23:31 +01:00
Dominik Csapak
03e44f5b60 edit window: add optional custom submit options
sometimes it's necessary or handy to add custom options to the submit
api call (e.g. timeout). So just expose a `submitOptions` where users
of the edit window can put their custom options.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-03-07 15:08:34 +01:00
Thomas Lamprecht
8d161ac19c dns: update comment to avoid coupling to downstream dependency
Not much of use, better comment why this exist, other products could
change or new ones get added with new semantic used there too, so the
previous comment would be guaranteed to become outdated rather sooner
than later.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-03-01 12:41:41 +01:00
Dominik Csapak
e3cf77e177 dns: optionally send delete for optional values
pbs only deletes the optional values here when they are sent with the
'delete' parameter, in contrast to pve/pmg that don't have a delete
parameter currently and always use the parameters as source of truth.

So to handle that, optionally set deleteEmpty if set from outside

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-03-01 12:34:55 +01:00
Thomas Lamprecht
9c69fc1b8f bump version to 4.1.4
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-02-28 11:46:43 +01:00
Lukas Wagner
d2c8fbfe0c utils: add extendable, translatable notification event descriptions
Add a similar mechanism like we have for adding and overriding task
description per product UI.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-02-28 11:40:48 +01:00
Thomas Lamprecht
36d167d629 form: combo grid: clarify that showClearTrigger cannot actively hide them
As getting a good setting name is a bit hard here, the current one
might me interpreted such that setting it to false will always hide
the trigger, but that's not the case, this is mostly a "force show
trigger even if allowBlank is set to false", and that's a bit of a
long name ;-)
So just add a comment and reevaluate if this really causes confusion.

While at it simplify the boolean expression to make it shorter and
easier to read.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-02-28 11:40:48 +01:00
Lukas Wagner
4bee6fb074 combogrid: add 'showClearTrigger' config
This allows one configure the clear trigger to be shown, even if
'allowBlank' is set false. This can be useful if one has a
non-editable combogrid where the value is set to something not
present in the store. Example: Match rule editing, one selects
a backup job to be match. If the backup job is removed and the match
rule edit window is opened again, then the old, deleted value cannot
be removed from the combogrid if there is no clear trigger.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-02-28 11:40:48 +01:00
Dominik Csapak
43b978658e datetimefield: fix changing date to end of month from short months
When selecting a new date, we get a date object from the currently
selected date before the change. If that month has less days than what
was selected for the new month, `setDate` will wrap that to the
following month since the old month is still selected there.

For example:

select any date in april (has 30 days)
then select the 31th of january

this will actually select the 1st of january since we first get

setDate: 20xx-04-XX -> 20xx-04-31 (wrap) -> 20xx-05-01
setMonth: 20xx-05-01 -> 20xx-01-01

To fix this, use the additional parameters of setFullYear[0] to set
all of them simultaneously

0: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setFullYear

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2024-02-27 14:33:41 +01:00
Thomas Lamprecht
bbb7ecbe36 d/changelog: drop duplicate 'for'
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-18 12:59:56 +01:00
Thomas Lamprecht
fc792d6411 i18n: use correct ISO 639-1 code for Korean with backward compat
recently the proxmox-i18n repo got a fix where we moved the files for
Korean to the correct language code, i.e., from previously wrong used
kr (Kanuri) to the correct ko (Korean).

This loads the correct ExtJS locale and is less confusing for our
Korean speakers, but we still want a clean transition for those that
have still the 'kr' value set in their language cookie.
Note that this transition only happens when the user opens the
language selector, as otherwise we do not have the product-specific
cookie name available, so a better transition would need to happen in
the per-product UIs.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-01-15 18:40:59 +01:00
Fiona Ebner
905528a28d fix #5074: notify: sendmail: smtp: fix mailto/mailto-user parameter deletion
by doing a cbind of isCreate to the top-level widget so that cbind in
the nested widgets for deleteEmpty works.

In the GUI, when a sendmail/smtp target is edited and either
'Additional Recipients' or 'Recipients' is completely removed (only
possible if the other field contains a value), parameter deletion did
not work properly. After applying the changes, the old value would
still be in place.

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
Tested-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Lukas Wagner <l.wagner@proxmox.com>
2024-01-05 10:33:26 +01:00
Thomas Lamprecht
115165bec2 bump version to 4.1.3
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-23 11:16:28 +01:00
Lukas Wagner
1fc3d8cd1c notification ui: display yellow warning triangle instead of red icon
Instead of coloring the entire icon red, show a yellow warning
triangle containing an exclamation mark in case of validation errors.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-11-23 10:10:10 +01:00
Lukas Wagner
64821a108c notification ui: change icon for for match-field tree nodes
The old icon was slightly ambiguous since we also use it for LXC
containers.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-11-23 10:10:10 +01:00
Thomas Lamprecht
a5630fd2dd schema: do not translate SMTP
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-23 09:09:59 +01:00
Thomas Lamprecht
55f8555f18 bump version to 4.1.2
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-21 21:36:07 +01:00
Lukas Wagner
6ed92b735f notification ui: add 'unknown' to match-severity dropdown
This is the severity used for forwarded mails, since we cannot
reasonably infer a priority here.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-11-21 17:38:52 +01:00
Lukas Wagner
c972de233a notification ui: add appropriate onlineHelp anchors
This links the dialog windows to the correct help section (different
target types, matchers).

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-11-21 17:38:40 +01:00
Fiona Ebner
edd98f946b notification matcher: fix inverted match modes
The 'not' prefix is already stripped in the set() method of the view
model's 'rootMode' and not present anymore when updating the store.
The information about whether the mode is inverted or not is present
in the 'invert' data member.

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
2023-11-20 16:17:42 +01:00
Thomas Lamprecht
49f7549b75 bump version to 4.1.1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-17 16:56:10 +01:00
Dominik Csapak
1f8bfa3b30 notification matcher: improve wording for mode
by removing the 'invert' checkbox and instead show the 4 modes possible,
we still assemble/parse the invert for the backend

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2023-11-17 15:11:25 +01:00
Dominik Csapak
be5329512c notification matcher: improve handling empty and invalid values
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2023-11-17 15:11:25 +01:00
Lukas Wagner
6a0b0b376c notification ui: add column for 'origin'
This column shows whether a matcher/target was provided as a built-in
default config or if it was created by the user. For built-ins, it
also shows whether the built-in settings have been changed.

To reset a built-in entry to its defaults, one can simply delete it.
For best UX, the 'delete' button should change its text to 'reset
defaults' when a built-in target/matcher is selected. This will be
added in another patch.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-11-17 15:04:39 +01:00
Lukas Wagner
5f7b28cb19 notification ui: add enable checkbox for targets/matchers
Add a 'enable' checkbox for targets and matchers in their edit
windows. Also show a new 'enable' column in the overview panel.
The parameter in the config is actually called 'disable', so
the UI needs to invert the setting in the appropriate
on{Get,Set}Values hooks.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-11-17 15:04:39 +01:00
Lukas Wagner
3003f37779 panel: notification: add gui for SMTP endpoints
This new endpoint configuration panel is embedded in the existing
EndpointEditBase dialog window. This commit also factors out some of
the non-trivial common form elements that are shared between the new
panel and the already existing SendmailEditPanel into a separate panel
EmailRecipientPanel.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-11-17 15:04:39 +01:00
Lukas Wagner
de0cec409a noficiation: matcher edit: make 'field' an editable combobox
For now with fixed options that are shared between most notification
events - later, once we have a notification registry, this should be
filled dynamically.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-11-17 15:04:39 +01:00
Lukas Wagner
5bd3ad4e90 notification ui: unprotected mailto-root target
A default notification config will now be created in pve-manager's
postinst hook - which is not magic in any way and can be modified
and deleted as desired.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-11-17 15:04:39 +01:00
Lukas Wagner
03a54ebd29 notification: matcher: add UI for matcher editing
This modifies the old filter edit window in the following ways:
  - Split content into multiple panels
    - Name and comment in the first tab
    - Match rules in a tree-structure in the second tab
    - Targets to notify in the third tab

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-11-17 15:04:39 +01:00
Lukas Wagner
e8f1954c55 notification ui: rename filter to matcher
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-11-17 15:04:39 +01:00
Lukas Wagner
feacab72ca notification ui: remove notification groups
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-11-17 15:04:39 +01:00
Lukas Wagner
159fec230d notification ui: remove filter setting for targets
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-11-17 15:04:39 +01:00
Lukas Wagner
78d21b71d2 notification ui: add target selector for matcher
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-11-17 15:04:39 +01:00
Thomas Lamprecht
15f0d99534 form: displaye-edit: add one of the two missing returns
the other one _should_ not be problematic, as field-container itself
isn't picked up as a "real" field itself, but we might bind to that
somewhere, where enabling could break this.

The editable one seems to not be used yet, according to Dominik, so
fix that now already.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-17 15:00:32 +01:00
Dominik Csapak
e34b0f4a12 api-viewer: implement basic oneOf support
for parameters only for now, also only implement the basic use case we
want to have currently: use in section config apis where we have more
than one type.

we could improve upon that, e.g. by properly grouping the type relevant
options, and also implementing that for return types.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2023-11-17 09:58:17 +01:00
Thomas Lamprecht
60e1c56233 bump version to 4.1.0
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-11-14 09:19:18 +01:00
Dominik Csapak
4ecad70c66 combogrid: initialze value with [] by default
we have to initialize the value of a combogrid to something (else extjs
does not initialize everything in the object *sometimes* for yet unknown
reasons), but the empty string is wrong.

we already have at least two places where we set the default value to []
(namely NodeSelector and ha GroupSelector) with the comment:

 // set default value to empty array, else it inits it with
 // null and after the store load it is an empty array,
 // triggering dirtychange

so it makes sense to always set it to that by default. This only ever is
relevant when the combogrid has `allowBlank: true`, since if it does not
it's either invalid (and thus "dirty") or it has a selected value anyway

this should make the manual setting of

 value: [],

unnecessary in the child classes. We can even remove it direcly in the
NetworkSelector.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2023-11-14 09:03:59 +01:00
Fabian Grünbichler
7605e43eaa apt: drop ChangeLogUrl
it's not returned anymore by the corresponding backends, since fetching
changelogs is now fully delegated to `apt`.

Signed-off-by: Fabian Grünbichler <f.gruenbichler@proxmox.com>
2023-11-13 18:16:30 +01:00
Dominik Csapak
64ffc0378e window/FileBrowser: enable tar button by default
all endpoints now can handle the 'tar' parameter, so add it for all

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2023-11-13 16:45:55 +01:00
Aaron Lauterer
d981e33b1f DiskList: render osdid-list if present
Render all OSD IDs in 'osdid-list' if the parameter is present.

It is possible to have multiple OSD daemons on a disk. We want to list
them all in the UI.
Fall back to the 'osdid' parameter if 'osdid-list' is not available.

We check rec.data['osdid-list'] against its general truthiness as it
might not be present at all or null.

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
2023-11-13 15:33:55 +01:00
Christian Ebner
40e341792e fix #4442: Extend LogView for firewall datetime filtering
Extends the current panels date filtering capability to date-time
based filtering, and adds a config option to switch between livemode
and filter mode, analogous to the JournalView panel.

The `submitFormat` config is introduced to adapt the formatting of
params values for their corresponding api calls.

Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
2023-11-13 15:23:12 +01:00
Christian Ebner
0e3cb037d6 fix #4442: adapt DateTimeField to be more declarative
Reworks the current implementation of the DateTimeField to be more
declarative by using a ViewModel and data bindings as well as formulas,
in order to reduce code and unwanted complexity.

Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
2023-11-13 15:23:12 +01:00
Lukas Wagner
8fc1d232ea panel: sendmail edit: don't translate 'Proxmox VE' in author field
The default value is determined by the backend and is never
translated (which does not make sense any way for a product name).
This was likely just a copy/paste mistake from other from fields.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-11-06 18:20:10 +01:00
Lukas Wagner
8a8b0428fd schema: endpoint types: don't translate endpoint type names
... that are not really translatable.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-11-06 18:19:29 +01:00
Thomas Lamprecht
3749f20c5e utils: fix trailing comma
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-10-31 21:47:12 +01:00
Thomas Lamprecht
5d7d30de0f text field: add trimValue config
Inspired by a recent bug detected in the subscription key field, where
a trailing white space caused verification issues.

We might even enable the trimming by default, after checking call
sites that is – most often one wants to trim the text to be submitted

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-10-24 13:59:19 +02:00
Thomas Lamprecht
5a3ac9b110 utils: code style cleanup/reduction
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-10-24 13:57:43 +02:00
Thomas Lamprecht
1326f771b9 bump version to 4.0.9
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-10-03 10:40:11 +02:00
Thomas Lamprecht
89699c6466 parser: split checking IMG and A tags, make the latter more strict
Split the logic so that each tag is handled explicitly on it's own
if-else branch, which is now safer to do as we default to
allow-only-http-like.

Also address a recently introduced regression from the implementation
of the #4756 where any user that could edit notes could use
javascript: script-urls for XSS purpose to prepare a link that could
leak private user information when another user clicked on it, at
least if they omitted basic sanity checks by looking at the URL
displayed by the browser before.

We have to override a false-positive triggered by a eslint heuristic,
a simple string compression should be always safe.

Fixes: 5cbbb9c ("fix #4756: markdown notes: allow any valid URL for a tags")
Reported-by: Hieu Dang Cong <HieuDC5@fpt.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-10-03 10:31:18 +02:00
Thomas Lamprecht
9ef8030535 parser: make it clearer that we mark HTTP-like URLs always as safe
we should make this controllable by the user for images, while modern
browser are quite safe w.r.t. not transmitting to much info on cross
origin requests, it still might be nicer if they have some control
over this.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-10-03 09:15:11 +02:00
Thomas Lamprecht
e21d3a40ad parser: use safer mechanism for allowing URLs
Having a default-remove boolean flag is making it easier to get this
right and decouple the if-branches that check if something is OK
(which may get more complex in the future) from the actual handling of
the result by always removing the href attribute if not explicitly
told otherwise.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-10-03 08:37:36 +02:00
Thomas Lamprecht
ade0e572d2 parser: factor out getting lower-case canonical tag name
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-10-03 08:27:22 +02:00
Lukas Wagner
b4b3bcad18 notification config view: add missing parameter for Ext.String.format
Fixes: 7e4b51 ("notification config view: fix using gettext with parameter")
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-09-18 12:06:33 +02:00
Thomas Lamprecht
247304085f sendmail edit panel: fix using gettext with parameter
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-09-14 08:14:25 +02:00
Thomas Lamprecht
7e4b51778a notification config view: fix using gettext with parameter
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-09-13 18:17:07 +02:00
Thomas Lamprecht
72a355b8d7 notification config view: fix using gettext with parameter
One must use a parameter {0} replacement string as otherwise this
cannot be translated at all.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-09-13 18:15:12 +02:00
Thomas Lamprecht
4187bffeeb bump version to 4.0.8
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-09-13 17:16:06 +02:00
Thomas Lamprecht
6e70fce94f utils: language map: add entry for Croatian translation
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-09-13 17:05:03 +02:00
Lukas Wagner
0e1fa66f75 auth: ldap: openid: use proxmoxtextfield for comment
The regular `textfield` does not support the `deleteEmpty`
setting. Thus, if no comment was entered the configuration
would still end up with an empty `comment` key:

ldap: foo
    server ....
    bind-dn ...
    comment

Fixed by switching over to `proxmoxtextfield`, which properly
deletes empty keys.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
2023-09-07 17:52:38 +02:00
Dominik Csapak
9133d5b5f9 fix #4951: accept undefined as value for the MultiDiskSelector
otherwise it tries to string split it and throws an exception

This can happen when there was no initial value and the form is reset.

Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
2023-09-07 17:49:38 +02:00
Filip Schauer
a5736f02e5 fix #4531: acme plugins: correct change detection of dirty form fields
Fix the ACME plugin edit form only detecting dirtychanges once the
value of a textfield was dirtied and then changed back to the
original.

This behaviour is caused as we cannot reuse the field's
resetOriginalValue method, due to that cause breakage here, e.g., if
the value was edited, then another plugin (without a schema) gets
selected, and then one would switch back again to the previous plugin,
it would cause the (actually still dirty) value to get registered as
new original one by mistake.

So the fix here is to keep the manual originalValue tracking, but add
the missing call to checkDirty after setting the originalValue to
refresh the dirty flag.

Fixes: 45708891 ("ui: add ACMEPluginEdit window") from pve-manager
Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
 [TL: record reason for originalValue handling & sligthly reword ]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-08-30 16:02:41 +02:00
Christian Ebner
575b4f3790 utils: Remove outdated url in comment
Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
2023-08-23 10:14:15 +02:00
Wolfgang Bumiller
49bb8516ed bump version to 4.0.7
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2023-08-16 10:43:13 +02:00
Lukas Wagner
5d1b587fdd notification: add ui for managing notification filters
This commit adds a new dialog window, containing all fields necessary
to configure notification filters.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Dominik Csapak <d.csapak@proxmox.com>
2023-08-16 10:37:44 +02:00
Lukas Wagner
90dbb2d359 notification: allow to select filter for notification targets
This commit adds a new selector field for existing endpoint
configuration where one is able to select a notification filter.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Dominik Csapak <d.csapak@proxmox.com>
2023-08-16 10:37:40 +02:00
Lukas Wagner
ea5aa12261 notification: add gui for notification groups
The GUI is based on the 'plugin-based' dialog window EndpointEditBase
that was introduced in an earlier commit.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Dominik Csapak <d.csapak@proxmox.com>
2023-08-16 10:37:35 +02:00
Lukas Wagner
6669a59fd1 notification: add gui for gotify notification endpoints
The GUI is based on the 'plugin-based' dialog window EndpointEditBase
that was introduced in an earlier commit.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Dominik Csapak <d.csapak@proxmox.com>
2023-08-16 10:37:31 +02:00
Lukas Wagner
84f70dfaad notification: add gui for sendmail notification endpoints
This commit adds a new panel 'NotificationConfigView' that is supposed
to be embedded in the datacenter configuration side-bar.
This new view lists all notification endpoints, allowing to
add/modify/delete/test them.

Furthermore, this commits adds the dialog for adding/modifying
sendmail endpoints. The dialog is 'plugin-in' based, meaning that it
consists of a base window (EndpointEditBase) and a panel that holds
the actual fields for the endpoint type configuration. This will show
be beneficial once the GUI for other endpoint types is added.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
Reviewed-by: Dominik Csapak <d.csapak@proxmox.com>
2023-08-16 10:37:24 +02:00
Filip Schauer
a60c8dc0c0 certificates: delete: Fix a typo
Fix the UI not refreshing when a successful certificate deletion
requires it.

Signed-off-by: Filip Schauer <f.schauer@proxmox.com>
2023-08-14 12:39:33 +02:00
Thomas Lamprecht
6e5405767c utils: add entry for Georgian translation
which got recently submitted as new translation:
https://git.proxmox.com/?p=extjs.git;a=commitdiff;h=6ee3ee85552152132700448eab148a3bf9a30fcc
https://git.proxmox.com/?p=proxmox-i18n.git;a=commitdiff;h=323c79d6b4fb8c7eb4b5eab7fb0f66d9cd970632

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-08-03 16:31:37 +02:00
Amin Vakil
2d613cac1f fix #4874: improve error message for invalid hostname
Current error message is not correct because having underscores in
domain names are perfectly valid, although it's not acceptable at host
names, so it should be changed to "This is not a valid hostname".

https://www.ietf.org/rfc/rfc1123.txt section 2.1 "Host Names and Numbers"
https://www.rfc-editor.org/rfc/rfc2181#section-11

Signed-off-by: Amin Vakil <info@aminvakil.com>
 [TL: s/Host /host/ once more, reflow msg with 70cc & reword subject]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-08-03 10:49:39 +02:00
Friedrich Weber
e61ffdd6ee ui: add some missing htmlEncodes
Signed-off-by: Friedrich Weber <f.weber@proxmox.com>
2023-07-25 16:58:12 +02:00
Thomas Lamprecht
81562cea8d bump version to 4.0.6
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-26 20:25:01 +02:00
Stefan Sterz
085eed30c2 window: ldap auth edit forbid specifying a bind_dn without a password
this commit enforces passwords when using an non-anonymous bind.
hence, it removes the possibility of configuring unauthenticated binds
and brings the gui in-line with the backend.

Signed-off-by: Stefan Sterz <s.sterz@proxmox.com>
2023-06-26 14:54:53 +02:00
Dominik Csapak
efcb34fa21 try using 'pve-eslint' if it exists
but fallback to 'eslint' otherwise

Suggested-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
 [T: move into www/manager Makefile directly]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-24 17:47:18 +02:00
Thomas Lamprecht
589317b094 bump version to 4.0.5
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-16 15:58:33 +02:00
Maximiliano Sandoval
c655b2f577 window: addtotp: Increase the size of the quiet zone
It is recommended that the quiet zone has the width of 4 blocks, since
each block is around 4 pixels each, we need a margin of 16 pixels and a
size of 256 + 2 * 16 pixels.

Signed-off-by: Maximiliano Sandoval <m.sandoval@proxmox.com>
2023-06-16 15:52:37 +02:00
Fiona Ebner
7d5201a32f apt repositories: add production ready warnings for Ceph repositories
Could've been done for the test repository already, but now that there
is a split between no-subscription and enterprise it becomes even more
relevant.

Reported-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
2023-06-16 15:50:03 +02:00
Thomas Lamprecht
01034bdb5f utils: add Українська - Ukrainian to language map
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-14 14:18:55 +02:00
Thomas Lamprecht
f224688d0c bump version to 4.0.4
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-09 17:29:54 +02:00
Fiona Ebner
a14bafeca6 apt repositories: avoid potential type error in classifyOrigin helper
with undefined (or otherwise falsy) argument.

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

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
2023-06-09 17:29:22 +02:00
Thomas Lamprecht
7a0bbba33a bump version to 4.0.3
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-09 08:07:45 +02:00
Wolfgang Bumiller
33cfd1f6b4 tfa: show 'Locked' in 'Enabled' column if tfa is locked
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2023-06-07 18:05:24 +02:00
Wolfgang Bumiller
50af081a20 tfa: improve UX for recovery keys and when none are left
If we get an empty challenge, tell the user to contact an
administrator as it means no 2nd factors and no recovery
keys are available.

Currently if only 1 key was available and it had a high ID,
we'd show something like: "Recovery keys available: 9,
Warning, less than 4 keys available."
Let's start off with the warning, and then be explicit about
the IDs.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2023-06-07 18:05:20 +02:00
Fiona Ebner
e6ed4498cd apt repositories: detect mixed suites before major upgrade
Usually, differing suites already produce warnings/errors, but before
a major upgrade the current and the next suite are both valid. Mixing
them is an issue though.

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
2023-06-07 17:19:38 +02:00
Fiona Ebner
b9b1a51a2e apt repositories: just ignore unknown info rather than throwing an error
This will avoid breaking older UI when extending the backend.

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
2023-06-07 17:19:32 +02:00
Fiona Ebner
3d6b76ee2b apt repositories: add classifyOrigin helper
to be used again to detect mixed repositories before upgrade.

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

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-07 17:19:14 +02:00
Thomas Lamprecht
f6f29f8c1c date time field: fixup syntax error
Fixes: 6883083 ("form: date time field: add alias for backward compat")
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-07 17:19:14 +02:00
Fiona Ebner
fd468868dd apt repositories: actually ignore ignore-pre-upgrade-warning
when upgrading is possible rather than throwing an error by reaching
the else branch.

Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-07 17:19:14 +02:00
Max Carrara
ecdde39c1c toolkit/utils: fix whitespace
Signed-off-by: Max Carrara <m.carrara@proxmox.com>
2023-06-06 17:11:42 +02:00
Max Carrara
aec7e8d23c toolkit/utils: set SameSite attr of auth cookie to 'strict'
Overrides 'Ext.util.Cookies', optionally allowing the SameSite
attribute of cookies to be defined. Using this override, the SameSite
attribute of the auth cookie is now set to 'strict', prohibiting the
cookie from being sent along in cross-site sub-requests or when the
user navigates to a different site.

Signed-off-by: Max Carrara <m.carrara@proxmox.com>
2023-06-06 17:11:42 +02:00
Christian Ebner
95fa855701 DateTimeField: Extend and refactor to make field value bindable
Extends the date time field so that bindings are updated on value changes.
Also adds a config to disable child components and avoid modification of
current values by cloning the referenced object for min/max value calculation.

Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
2023-06-06 16:45:47 +02:00
Thomas Lamprecht
6883083e8a form: date time field: add alias for backward compat
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-06 16:43:56 +02:00
Christian Ebner
9531241400 DateTimeField: fix typo in xtype
Signed-off-by: Christian Ebner <c.ebner@proxmox.com>
2023-06-06 16:41:43 +02:00
Thomas Lamprecht
db5d0cc1f4 bump version to 4.0.2
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-03 13:45:34 +02:00
Thomas Lamprecht
2d04f0165d markdown parser: allow setting target tag for links
If one really want's to force a link to open in a new tab (or window,
depending on the browser settings).

Note that we don't set target to _blank by default for links, as
opening in a new tab can already simply be done via a middle-click on
the link without that, but once the target is set opening in the same
tab cannot easily be done, i.e., without a target set the reader has
more freedom and flexibility.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-03 13:41:23 +02:00
Thomas Lamprecht
5cbbb9c44a fix #4756: markdown notes: allow any valid URL for a tags
As anchor elements cannot load things into the current browsing
context and are not necessarily more dangerous to users compared to
HTTP(S) links, which we allowed since adding markdown rendering in
the first place.

Allows adding short-cuts for virtual guest resources, like RDP or SSH
links.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-03 13:41:23 +02:00
Thomas Lamprecht
b0c7069a10 markdown parser: normalize tag names and protocol for check
As the tag names are often uppercase, and so this was overly strict
and didn't always match correctly

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-03 13:41:23 +02:00
Thomas Lamprecht
28f879c09e markdown parser: correctly remove src & href attribute if not valid URL
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-03 13:41:23 +02:00
Thomas Lamprecht
e09af56554 ship a minified version of the widget-toolkit JS library
so use sites can switch between literal and minified version like we
do already for ExtJS via the debug flag.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-03 13:41:23 +02:00
Thomas Lamprecht
0676eb3738 buildsys: avoid whitespace in build info heading
this is added to the URL as is by most index templates, and literal
(non-URL-encoded) white space might break things.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-03 13:41:23 +02:00
Thomas Lamprecht
1683d09017 bump version to 4.0.1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-01 16:35:36 +02:00
Noel Ullreich
01e64778ad fix #4551: ui: translate byte unit in format_size
Some languages translate byte units like 'GiB' or write them in their
own script.

By `gettext`ing the units in the `format_size` function, we can
translate the units for (almost) all of the web interface.

Signed-off-by: Noel Ullreich <n.ullreich@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-01 16:10:34 +02:00
Thomas Lamprecht
9dba61e674 parser: adapt to new marked Markdown renderer calling convention
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-06-01 16:03:57 +02:00
Aaron Lauterer
b515e164b7 tfa: paperkey: cleanup iframes for printing after window close
similar as recently done for the PBS storage add/edit window in
pve-manager

Signed-off-by: Aaron Lauterer <a.lauterer@proxmox.com>
[ TL: refer to same change as done in pve-manager ]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-05-28 19:40:02 +02:00
Thomas Lamprecht
9521d1768e buildsys: expand clean target
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-05-25 10:27:10 +02:00
Thomas Lamprecht
b1f95caa4e bump version to 4.0.0
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-05-25 10:15:45 +02:00
Thomas Lamprecht
87ea4468db d/control: raise compat level to 13
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2023-05-25 10:15:32 +02:00
65 changed files with 4697 additions and 576 deletions

12
.gitignore vendored
View File

@ -1,4 +1,10 @@
src/.lint-incremental
*.deb
*.changes
*.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
View File

@ -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
* dark-mode:

5
debian/control vendored
View File

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

View File

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

View File

@ -24,22 +24,33 @@ Ext.define('Proxmox.Markdown', {
for (let i=node.attributes.length; i--;) {
const name = node.attributes[i].name;
const value = node.attributes[i].value;
const canonicalTagName = node.tagName.toLowerCase();
// TODO: we may want to also disallow class and id attrs
if (
!/^(class|id|name|href|src|alt|align|valign|disabled|checked|start|type)$/i.test(name)
!/^(class|id|name|href|src|alt|align|valign|disabled|checked|start|type|target)$/i.test(name)
) {
node.attributes.removeNamedItem(name);
} else if ((name === 'href' || name === 'src') && !_isHTTPLike(value)) {
let safeURL = false;
try {
let url = new URL(value, window.location.origin);
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;
} else {
node.attributes.removeNamedItem(name);
}
} 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]);
@ -55,7 +66,7 @@ Ext.define('Proxmox.Markdown', {
parse: function(markdown) {
/*global marked*/
let unsafeHTML = marked(markdown);
let unsafeHTML = marked.parse(markdown);
return `<div class="pmx-md">${this.sanitizeHTML(unsafeHTML)}</div>`;
},

View File

@ -4,10 +4,13 @@ Ext.define('Proxmox.Schema', { // a singleton
authDomains: {
pam: {
name: 'Linux PAM',
ipanel: 'pmxAuthSimplePanel',
onlineHelp: 'user-realms-pam',
add: false,
edit: false,
edit: true,
pwchange: true,
sync: false,
useTypeInUrl: false,
},
openid: {
name: gettext('OpenID Connect Server'),
@ -18,6 +21,7 @@ Ext.define('Proxmox.Schema', { // a singleton
pwchange: false,
sync: false,
iconCls: 'pmx-itype-icon-openid-logo',
useTypeInUrl: true,
},
ldap: {
name: gettext('LDAP Server'),
@ -28,6 +32,18 @@ Ext.define('Proxmox.Schema', { // a singleton
tfa: true,
pwchange: false,
sync: true,
useTypeInUrl: true,
},
ad: {
name: gettext('Active Directory Server'),
ipanel: 'pmxAuthADPanel',
syncipanel: 'pmxAuthADSyncPanel',
add: true,
edit: true,
tfa: true,
pwchange: false,
sync: true,
useTypeInUrl: true,
},
},
// to add or change existing for product specific ones
@ -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: {
b: { icon: 'cube', label: gettext('Block Device') },
c: { icon: 'tty', label: gettext('Character Device') },

View File

@ -21,7 +21,7 @@ Ext.apply(Ext.form.field.VTypes, {
IPCIDRAddressMask: /[\d./]/i,
IP6Address: function(v) {
return Proxmox.Utils.IP6_match.test(v);
return Proxmox.Utils.IP6_match.test(v);
},
IP6AddressText: gettext('Example') + ': 2001:DB8::42',
IP6AddressMask: /[A-Fa-f0-9:]/,
@ -42,7 +42,7 @@ Ext.apply(Ext.form.field.VTypes, {
IP6PrefixLengthMask: /[0-9]/,
IP64Address: function(v) {
return Proxmox.Utils.IP64_match.test(v);
return Proxmox.Utils.IP64_match.test(v);
},
IP64AddressText: gettext('Example') + ': 192.168.1.1 2001:DB8::42',
IP64AddressMask: /[A-Fa-f0-9.:]/,
@ -76,25 +76,25 @@ Ext.apply(Ext.form.field.VTypes, {
MacPrefixText: gettext('Example') + ': 02:8f - ' + gettext('only unicast addresses are allowed'),
BridgeName: function(v) {
return (/^vmbr\d{1,4}$/).test(v);
return (/^[a-zA-Z][a-zA-Z0-9_]{0,9}$/).test(v);
},
VlanName: function(v) {
if (Proxmox.Utils.VlanInterface_match.test(v)) {
return true;
return true;
} else if (Proxmox.Utils.Vlan_match.test(v)) {
return true;
return true;
}
return true;
},
BridgeNameText: gettext('Format') + ': vmbr<b>N</b>, where 0 <= <b>N</b> <= 9999',
BridgeNameText: gettext('Format') + ': alphanumeric string starting with a character',
BondName: function(v) {
return (/^bond\d{1,4}$/).test(v);
return (/^bond\d{1,4}$/).test(v);
},
BondNameText: gettext('Format') + ': bond<b>N</b>, where 0 <= <b>N</b> <= 9999',
InterfaceName: function(v) {
return (/^[a-z][a-z0-9_]{1,20}$/).test(v);
return (/^[a-z][a-z0-9_]{1,20}$/).test(v);
},
InterfaceNameText: gettext("Allowed characters") + ": 'a-z', '0-9', '_'<br />" +
gettext("Minimum characters") + ": 2<br />" +
@ -102,7 +102,7 @@ Ext.apply(Ext.form.field.VTypes, {
gettext("Must start with") + ": 'a-z'",
StorageId: function(v) {
return (/^[a-z][a-z0-9\-_.]*[a-z0-9]$/i).test(v);
return (/^[a-z][a-z0-9\-_.]*[a-z0-9]$/i).test(v);
},
StorageIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '-', '_', '.'<br />" +
gettext("Minimum characters") + ": 2<br />" +
@ -110,14 +110,14 @@ Ext.apply(Ext.form.field.VTypes, {
gettext("Must end with") + ": 'A-Z', 'a-z', '0-9'<br />",
ConfigId: function(v) {
return (/^[a-z][a-z0-9_-]+$/i).test(v);
return (/^[a-z][a-z0-9_-]+$/i).test(v);
},
ConfigIdText: gettext("Allowed characters") + ": 'A-Z', 'a-z', '0-9', '_'<br />" +
gettext("Minimum characters") + ": 2<br />" +
gettext("Must start with") + ": " + gettext("letter"),
HttpProxy: function(v) {
return (/^http:\/\/.*$/).test(v);
return (/^http:\/\/.*$/).test(v);
},
HttpProxyText: gettext('Example') + ": http://username:password&#64;host:port/",
@ -129,16 +129,16 @@ Ext.apply(Ext.form.field.VTypes, {
DnsName: function(v) {
return Proxmox.Utils.DnsName_match.test(v);
},
DnsNameText: gettext('This is not a valid DNS name'),
DnsNameText: gettext('This is not a valid hostname'),
DnsNameOrWildcard: function(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
proxmoxMail: function(v) {
return (/^[\w+-~]+(\.[\w+-~]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$/).test(v);
return (/^[\w+-~]+(\.[\w+-~]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$/).test(v);
},
proxmoxMailText: gettext('Example') + ": user@example.com",
@ -179,11 +179,11 @@ Ext.apply(Ext.form.field.VTypes, {
HostListText: gettext('Not a valid list of hosts'),
password: function(val, field) {
if (field.initialPassField) {
let pwd = field.up('form').down(`[name=${field.initialPassField}]`);
return val === pwd.getValue();
}
return true;
if (field.initialPassField) {
let pwd = field.up('form').down(`[name=${field.initialPassField}]`);
return val === pwd.getValue();
}
return true;
},
passwordText: gettext('Passwords do not match'),
@ -216,30 +216,30 @@ Ext.define('Proxmox.UnderlayPool', {
override: 'Ext.dom.UnderlayPool',
checkOut: function() {
let cache = this.cache,
len = cache.length,
el;
let cache = this.cache,
len = cache.length,
el;
// do cleanup because some of the objects might have been destroyed
// do cleanup because some of the objects might have been destroyed
while (len--) {
if (cache[len].destroyed) {
cache.splice(len, 1);
}
}
// end do cleanup
if (cache[len].destroyed) {
cache.splice(len, 1);
}
}
// end do cleanup
el = cache.shift();
if (!el) {
el = Ext.Element.create(this.elementConfig);
el.setVisibilityMode(2);
//<debug>
// tell the spec runner to ignore this element when checking if the dom is clean
if (!el) {
el = Ext.Element.create(this.elementConfig);
el.setVisibilityMode(2);
//<debug>
// tell the spec runner to ignore this element when checking if the dom is clean
el.dom.setAttribute('data-sticky', true);
//</debug>
//</debug>
}
return el;
return el;
},
});
@ -515,28 +515,28 @@ Ext.define('Proxmox.selection.CheckboxModel', {
// [ P: optimized to remove all records at once as single remove is O(n^3) slow ]
// records can be an index, a record or an array of records
doDeselect: function(records, suppressEvent) {
var me = this,
selected = me.selected,
i = 0,
len, record,
commit;
if (me.locked || !me.store) {
return false;
}
if (typeof records === "number") {
// No matching record, jump out
record = me.store.getAt(records);
if (!record) {
return false;
}
records = [
record,
];
} else if (!Ext.isArray(records)) {
records = [
records,
];
}
var me = this,
selected = me.selected,
i = 0,
len, record,
commit;
if (me.locked || !me.store) {
return false;
}
if (typeof records === "number") {
// No matching record, jump out
record = me.store.getAt(records);
if (!record) {
return false;
}
records = [
record,
];
} else if (!Ext.isArray(records)) {
records = [
records,
];
}
// [P] a beforedeselection, triggered by me.onSelectChange below, can block removal by
// returning false, thus the original implementation removed only here in the commit fn,
// which has an abysmal performance O(n^3). As blocking removal is not the norm, go do the
@ -550,119 +550,119 @@ Ext.define('Proxmox.selection.CheckboxModel', {
}
};
let removalBlocked = [];
len = records.length;
me.suspendChanges();
for (; i < len; i++) {
record = records[i];
if (me.isSelected(record)) {
len = records.length;
me.suspendChanges();
for (; i < len; i++) {
record = records[i];
if (me.isSelected(record)) {
committed = false;
me.onSelectChange(record, false, suppressEvent, commit);
me.onSelectChange(record, false, suppressEvent, commit);
if (!committed) {
removalBlocked.push(record);
}
if (me.destroyed) {
return false;
}
}
}
if (me.destroyed) {
return false;
}
}
}
if (removalBlocked.length > 0) {
records.remove(removalBlocked);
}
selected.remove(records); // [P] FAST(er)
me.lastSelected = selected.last();
me.resumeChanges();
// fire selchange if there was a change and there is no suppressEvent flag
me.resumeChanges();
// fire selchange if there was a change and there is no suppressEvent flag
me.maybeFireSelectionChange(records.length > 0 && !suppressEvent);
return records.length;
},
doMultiSelect: function(records, keepExisting, suppressEvent) {
var me = this,
selected = me.selected,
change = false,
result, i, len, record, commit;
var me = this,
selected = me.selected,
change = false,
result, i, len, record, commit;
if (me.locked) {
return;
}
if (me.locked) {
return;
}
records = !Ext.isArray(records) ? [records] : records;
len = records.length;
if (!keepExisting && selected.getCount() > 0) {
result = me.deselectDuringSelect(records, suppressEvent);
if (me.destroyed) {
return;
}
if (result[0]) {
// We had a failure during selection, so jump out
// Fire selection change if we did deselect anything
me.maybeFireSelectionChange(result[1] > 0 && !suppressEvent);
return;
} else {
// Means something has been deselected, so we've had a change
change = result[1] > 0;
}
}
records = !Ext.isArray(records) ? [records] : records;
len = records.length;
if (!keepExisting && selected.getCount() > 0) {
result = me.deselectDuringSelect(records, suppressEvent);
if (me.destroyed) {
return;
}
if (result[0]) {
// We had a failure during selection, so jump out
// Fire selection change if we did deselect anything
me.maybeFireSelectionChange(result[1] > 0 && !suppressEvent);
return;
} else {
// Means something has been deselected, so we've had a change
change = result[1] > 0;
}
}
let gotBlocked, blockedRecords = [];
commit = function() {
if (!selected.getCount()) {
me.selectionStart = record;
}
commit = function() {
if (!selected.getCount()) {
me.selectionStart = record;
}
gotBlocked = false;
change = true;
};
change = true;
};
for (i = 0; i < len; i++) {
record = records[i];
if (me.isSelected(record)) {
continue;
}
for (i = 0; i < len; i++) {
record = records[i];
if (me.isSelected(record)) {
continue;
}
gotBlocked = true;
me.onSelectChange(record, true, suppressEvent, commit);
if (me.destroyed) {
return;
}
me.onSelectChange(record, true, suppressEvent, commit);
if (me.destroyed) {
return;
}
if (gotBlocked) {
blockedRecords.push(record);
}
}
}
if (blockedRecords.length > 0) {
records.remove(blockedRecords);
}
selected.add(records);
me.lastSelected = record;
selected.add(records);
me.lastSelected = record;
// fire selchange if there was a change and there is no suppressEvent flag
me.maybeFireSelectionChange(change && !suppressEvent);
// fire selchange if there was a change and there is no suppressEvent flag
me.maybeFireSelectionChange(change && !suppressEvent);
},
deselectDuringSelect: function(toSelect, suppressEvent) {
var me = this,
selected = me.selected.getRange(),
changed = 0,
failed = false;
// Prevent selection change events from firing, will happen during select
me.suspendChanges();
me.deselectingDuringSelect = true;
var me = this,
selected = me.selected.getRange(),
changed = 0,
failed = false;
// Prevent selection change events from firing, will happen during select
me.suspendChanges();
me.deselectingDuringSelect = true;
let toDeselect = selected.filter(item => !Ext.Array.contains(toSelect, item));
if (toDeselect.length > 0) {
changed = me.doDeselect(toDeselect, suppressEvent);
if (!changed) {
failed = true;
}
if (me.destroyed) {
failed = true;
changed = 0;
}
}
me.deselectingDuringSelect = false;
me.resumeChanges();
return [
failed,
changed,
];
}
if (me.destroyed) {
failed = true;
changed = 0;
}
}
me.deselectingDuringSelect = false;
me.resumeChanges();
return [
failed,
changed,
];
},
});
@ -678,11 +678,11 @@ Ext.define('Proxmox.view.DragZone', {
override: 'Ext.view.DragZone',
onItemMouseDown: function(view, record, item, index, e) {
// Ignore touchstart.
// For touch events, we use longpress.
if (e.pointerType !== 'touch') {
this.onTriggerGesture(view, record, item, index, e);
}
// Ignore touchstart.
// For touch events, we use longpress.
if (e.pointerType !== 'touch') {
this.onTriggerGesture(view, record, item, index, e);
}
},
});
@ -702,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
// since Ext.Msg is an object and not a prototype, we need to override it
// 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', {
extend: 'Ext.Component',
@ -733,154 +771,84 @@ Ext.define('Ext.ux.IFrame', {
src: 'about:blank',
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'],
initComponent: function() {
this.callParent();
this.callParent();
this.frameName = this.frameName || this.id + '-frame';
this.frameName = this.frameName || this.id + '-frame';
},
initEvents: function() {
let me = this;
me.callParent();
me.iframeEl.on('load', me.onLoad, me);
let me = this;
me.callParent();
me.iframeEl.on('load', me.onLoad, me);
},
initRenderData: function() {
return Ext.apply(this.callParent(), {
src: this.src,
frameName: this.frameName,
});
return Ext.apply(this.callParent(), {
src: this.src,
frameName: this.frameName,
});
},
getBody: function() {
let doc = this.getDoc();
return doc.body || doc.documentElement;
let doc = this.getDoc();
return doc.body || doc.documentElement;
},
getDoc: function() {
try {
return this.getWin().document;
} catch (ex) {
return null;
}
try {
return this.getWin().document;
} catch (ex) {
return null;
}
},
getWin: function() {
let me = this,
name = me.frameName,
win = Ext.isIE
? me.iframeEl.dom.contentWindow
: window.frames[name];
return win;
let me = this,
name = me.frameName,
win = Ext.isIE ? me.iframeEl.dom.contentWindow : window.frames[name];
return win;
},
getFrame: function() {
let me = this;
return me.iframeEl.dom;
},
let me = this;
beforeDestroy: function() {
this.cleanupListeners(true);
this.callParent();
},
cleanupListeners: function(destroying) {
let doc, prop;
if (this.rendered) {
try {
doc = this.getDoc();
if (doc) {
Ext.get(doc).un(this._docListeners);
if (destroying && doc.hasOwnProperty) {
for (prop in doc) {
if (Object.prototype.hasOwnProperty.call(doc, prop)) {
delete doc[prop];
}
}
}
}
} catch (e) {
// do nothing
}
}
return me.iframeEl.dom;
},
onLoad: function() {
let me = this,
doc = me.getDoc(),
fn = me.onRelayedEvent;
let me = this,
doc = me.getDoc();
if (doc) {
try {
// These events need to be relayed from the inner document (where they stop
// bubbling) up to the outer document. This has to be done at the DOM level so
// the event reaches listeners on elements like the document body. The effected
// mechanisms that depend on this bubbling behavior are listed to the right
// of the event.
Ext.get(doc).on(
me._docListeners = {
mousedown: fn, // menu dismisal (MenuManager) and Window onMouseDown (toFront)
mousemove: fn, // window resize drag detection
mouseup: fn, // window resize termination
click: fn, // not sure, but just to be safe
dblclick: fn, // not sure again
scope: me,
},
);
} catch (e) {
// cannot do this xss
}
// We need to be sure we remove all our events from the iframe on unload or we're going to LEAK!
Ext.get(this.getWin()).on('beforeunload', me.cleanupListeners, me);
this.el.unmask();
this.fireEvent('load', this);
} else if (me.src) {
this.el.unmask();
this.fireEvent('error', this);
}
},
onRelayedEvent: function(event) {
// relay event from the iframe's document to the document that owns the iframe...
let iframeEl = this.iframeEl,
// Get the left-based iframe position
iframeXY = iframeEl.getTrueXY(),
originalEventXY = event.getXY(),
// Get the left-based XY position.
// This is because the consumer of the injected event will
// perform its own RTL normalization.
eventXY = event.getTrueXY();
// the event from the inner document has XY relative to that document's origin,
// so adjust it to use the origin of the iframe in the outer document:
event.xy = [iframeXY[0] + eventXY[0], iframeXY[1] + eventXY[1]];
event.injectEvent(iframeEl); // blame the iframe for the event...
event.xy = originalEventXY; // restore the original XY (just for safety)
if (doc) {
this.el.unmask();
this.fireEvent('load', this);
} else if (me.src) {
this.el.unmask();
this.fireEvent('error', this);
}
},
load: function(src) {
let me = this,
text = me.loadMask,
frame = me.getFrame();
let me = this,
text = me.loadMask,
frame = me.getFrame();
if (me.fireEvent('beforeload', me, src) !== false) {
if (text && me.el) {
me.el.mask(text);
}
if (me.fireEvent('beforeload', me, src) !== false) {
if (text && me.el) {
me.el.mask(text);
}
frame.src = me.src = src || me.src;
}
frame.src = me.src = src || me.src;
}
},
});

View File

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

View File

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

View File

@ -110,6 +110,7 @@ Ext.define('Proxmox.button.StdRemoveButton', {
config: {
baseurl: undefined,
customConfirmationMessage: undefined,
},
getUrl: function(rec) {
@ -133,7 +134,14 @@ Ext.define('Proxmox.button.StdRemoveButton', {
let me = this;
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) {

View File

@ -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-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;
}
@ -25,8 +27,10 @@
}
.proxmox-tags-circle .proxmox-tag-light,
.proxmox-tags-circle .proxmox-tag-dark {
.proxmox-tags-circle :not(span.proxmox-tags-full) > .proxmox-tag-light,
.proxmox-tags-circle :not(span.proxmox-tags-full) > .proxmox-tag-dark,
.proxmox-tags-circle > .proxmox-tag-light,
.proxmox-tags-circle > .proxmox-tag-dark {
margin: 0px 1px;
position: relative;
top: 2px;
@ -38,13 +42,17 @@
overflow: hidden;
}
.proxmox-tags-none .proxmox-tag-light,
.proxmox-tags-none .proxmox-tag-dark {
.proxmox-tags-none :not(span.proxmox-tags-full) > .proxmox-tag-light,
.proxmox-tags-none :not(span.proxmox-tags-full) > .proxmox-tag-dark,
.proxmox-tags-none > .proxmox-tag-light,
.proxmox-tags-none > .proxmox-tag-dark {
display: none;
}
.proxmox-tags-dense .proxmox-tag-light,
.proxmox-tags-dense .proxmox-tag-dark {
.proxmox-tags-dense :not(span.proxmox-tags-full) > .proxmox-tag-light,
.proxmox-tags-dense :not(span.proxmox-tags-full) > .proxmox-tag-dark,
.proxmox-tags-dense > .proxmox-tag-light,
.proxmox-tags-dense > .proxmox-tag-dark {
width: 6px;
margin-right: 1px;
display: inline-block;
@ -66,6 +74,9 @@
.x-mask-msg-text {
text-align: center;
}
.x-window-header-default-top .x-mask {
background-color: #f5f5f5B4; /* ~ 0.7 opacity */
}
.proxmox-disabled-row, .proxmox-disabled-row td {
/*color: #a0a0a0;*/
@ -113,12 +124,14 @@
color: #3892d4;
}
.pwt-eol-icon {
.eol-notice a:visited {
color: inherit;
}
.eol-notice > i.fa {
position: relative;
float: left;
margin-right: 5px;
font-size: 1.3em;
color: #FF6C59;
}
/* reduce chart legend space usage to something more sane */

View 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',
});

View File

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

View File

@ -1,151 +1,169 @@
Ext.define('Proxmox.DateTimeField', {
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',
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() {
let me = this;
let d = me.lookupReference('dateentry').getValue();
if (d === undefined || d === null) { return null; }
let t = me.lookupReference('timeentry').getValue();
if (t === undefined || t === null) { return null; }
let offset = (t.getHours() * 3600 + t.getMinutes() * 60) * 1000;
return new Date(d.getTime() + offset);
return this.getViewModel().get('datetime');
},
getSubmitValue: function() {
let me = this;
let format = me.submitFormat;
let value = me.getValue();
let me = this;
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: [
{
xtype: 'datefield',
editable: false,
reference: 'dateentry',
flex: 1,
format: 'Y-m-d',
bind: {
value: '{date}',
minValue: '{minDate}',
maxValue: '{maxDate}',
},
},
{
xtype: 'timefield',
reference: 'timeentry',
format: 'H:i',
width: 80,
value: '00:00',
increment: 60,
bind: {
value: '{time}',
minValue: '{minTime}',
maxValue: '{maxTime}',
},
},
],
setMinValue: function(value) {
let me = this;
let current = me.getValue();
if (!value || !current) {
return;
}
let minhours = value.getHours();
let minminutes = value.getMinutes();
let hours = current.getHours();
let minutes = current.getMinutes();
value.setHours(0);
value.setMinutes(0);
value.setSeconds(0);
current.setHours(0);
current.setMinutes(0);
current.setSeconds(0);
let time = new Date();
if (current-value > 0) {
time.setHours(0);
time.setMinutes(0);
time.setSeconds(0);
time.setMilliseconds(0);
} else {
time.setHours(minhours);
time.setMinutes(minminutes);
}
me.lookup('timeentry').setMinValue(time);
// current time is smaller than the time part of the new minimum
// so we have to add 1 to the day
if (minhours*60+minminutes > hours*60+minutes) {
value.setDate(value.getDate()+1);
}
me.lookup('dateentry').setMinValue(value);
},
setMaxValue: function(value) {
let me = this;
let current = me.getValue();
if (!value || !current) {
return;
}
let maxhours = value.getHours();
let maxminutes = value.getMinutes();
let hours = current.getHours();
let minutes = current.getMinutes();
value.setHours(0);
value.setMinutes(0);
current.setHours(0);
current.setMinutes(0);
let time = new Date();
if (value-current > 0) {
time.setHours(23);
time.setMinutes(59);
time.setSeconds(59);
} else {
time.setHours(maxhours);
time.setMinutes(maxminutes);
}
me.lookup('timeentry').setMaxValue(time);
// current time is biger than the time part of the new maximum
// so we have to subtract 1 to the day
if (maxhours*60+maxminutes < hours*60+minutes) {
value.setDate(value.getDate()-1);
}
me.lookup('dateentry').setMaxValue(value);
},
initComponent: function() {
let me = this;
me.callParent();
let value = me.value || new Date();
me.lookupReference('dateentry').setValue(value);
me.lookupReference('timeentry').setValue(value);
if (me.minValue) {
me.setMinValue(me.minValue);
}
if (me.maxValue) {
me.setMaxValue(me.maxValue);
}
me.relayEvents(me.lookupReference('dateentry'), ['change']);
me.relayEvents(me.lookupReference('timeentry'), ['change']);
},
});

View File

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

View File

@ -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,
},
});

View File

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

View File

@ -45,10 +45,6 @@ Ext.define('Proxmox.form.NetworkSelector', {
networkSelectorStore.load();
}
},
// set default value to empty array, else it inits it with
// null and after the store load it is an empty array,
// triggering dirtychange
value: [],
valueField: 'cidr',
displayField: 'cidr',
store: {
@ -123,6 +119,7 @@ Ext.define('Proxmox.form.NetworkSelector', {
header: gettext('Comment'),
flex: 2,
dataIndex: 'comments',
renderer: Ext.String.htmlEncode,
},
],
},

View File

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

61
src/form/TextAreaField.js Normal file
View 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();
},
});

View File

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

40
src/form/VlanField.js Normal file
View 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();
},
});

View File

@ -67,7 +67,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
onlineHelp: opts.onlineHelp,
fieldDefaults: {
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) {
@ -100,7 +102,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
onlineHelp: opts.onlineHelp,
fieldDefaults: {
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) {
@ -131,7 +135,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
onlineHelp: opts.onlineHelp,
fieldDefaults: {
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) {
@ -163,7 +169,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
onlineHelp: opts.onlineHelp,
fieldDefaults: {
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

View File

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

View File

@ -270,7 +270,7 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
let txt = [gettext('Warning')];
record.data.warnings.forEach((warning) => {
if (warning.property === 'Suites') {
txt.push(warning.message);
txt.push(Ext.htmlEncode(warning.message));
}
});
metaData.tdAttr = `data-qtip="${Ext.htmlEncode(txt.join('<br>'))}"`;
@ -305,7 +305,7 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
? gettext('The no-subscription repository is NOT production-ready')
: gettext('The test repository may contain unstable updates')
;
metaData.tdAttr = `data-qtip="${Ext.htmlEncode(qtip)}"`;
metaData.tdAttr = `data-qtip="${Ext.htmlEncode(Ext.htmlEncode(qtip))}"`;
}
}
return components.join(' ') + err;
@ -343,14 +343,15 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
header: gettext('Origin'),
dataIndex: 'Origin',
width: 120,
renderer: (value, meta, rec) => {
renderer: function(value, meta, rec) {
if (typeof value !== 'string' || value.length === 0) {
value = gettext('Other');
}
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';
} else if (value.match(/^\s*Debian\s*(:?Backports)?$/i)) {
} else if (originType === 'Debian') {
cls = 'pmx-itype-icon pmx-itype-icon-debian-swirl';
}
return `<i class='${cls}'></i> ${value}`;
@ -360,6 +361,7 @@ Ext.define('Proxmox.node.APTRepositoriesGrid', {
header: gettext('Comment'),
dataIndex: 'Comment',
flex: 2,
renderer: Ext.String.htmlEncode,
},
],
@ -404,6 +406,16 @@ Ext.define('Proxmox.node.APTRepositories', {
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: {
xclass: 'Ext.app.ViewController',
@ -449,7 +461,13 @@ Ext.define('Proxmox.node.APTRepositories', {
let enterprise = vm.get('enterpriseRepo');
let nosubscription = vm.get('noSubscriptionRepo');
let test = vm.get('testRepo');
let cephRepos = {
enterprise: vm.get('cephEnterpriseRepo'),
nosubscription: vm.get('cephNoSubscriptionRepo'),
test: vm.get('cephTestRepo'),
};
let wrongSuites = vm.get('suitesWarning');
let mixedSuites = vm.get('mixedSuites');
if (!enterprise && !nosubscription && !test) {
addCritical(
@ -467,17 +485,37 @@ Ext.define('Proxmox.node.APTRepositories', {
addWarn(gettext('Some suites are misconfigured'));
}
if (!activeSubscription && enterprise) {
addWarn(gettext('The enterprise repository is enabled, but there is no active subscription!'));
if (mixedSuites) {
addWarn(gettext('Detected mixed suites before upgrade'));
}
if (nosubscription) {
addWarn(gettext('The no-subscription repository is not recommended for production use!'));
}
let productionReadyCheck = (repos, type, noSubAlternateName) => {
if (!activeSubscription && repos.enterprise) {
addWarn(Ext.String.format(
gettext('The {0}enterprise repository is enabled, but there is no active subscription!'),
type,
));
}
if (test) {
addWarn(gettext('The test repository may pull in unstable updates and is not recommended for production use!'));
}
if (repos.nosubscription) {
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) {
text = gettext('Fatal parsing error for at least one repository');
@ -497,10 +535,14 @@ Ext.define('Proxmox.node.APTRepositories', {
product: 'Proxmox VE', // default
errors: [],
suitesWarning: false,
mixedSuites: false, // used before major upgrade
subscriptionActive: '',
noSubscriptionRepo: '',
enterpriseRepo: '',
testRepo: '',
cephEnterpriseRepo: '',
cephNoSubscriptionRepo: '',
cephTestRepo: '',
selectionenabled: false,
state: {},
},
@ -610,6 +652,12 @@ Ext.define('Proxmox.node.APTRepositories', {
vm.set('noSubscriptionRepo', status);
} else if (handle === 'test') {
vm.set('testRepo', status);
} else if (handle.match(/^ceph-[a-zA-Z]+-enterprise$/)) {
vm.set('cephEnterpriseRepo', status);
} else if (handle.match(/^ceph-[a-zA-Z]+-no-subscription$/)) {
vm.set('cephNoSubscriptionRepo', status);
} else if (handle.match(/^ceph-[a-zA-Z]+-test$/)) {
vm.set('cephTestRepo', status);
}
me.getController().updateState();
@ -631,6 +679,11 @@ Ext.define('Proxmox.node.APTRepositories', {
let digest;
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) {
let data = records[0].data;
let files = data.files;
@ -649,17 +702,23 @@ Ext.define('Proxmox.node.APTRepositories', {
infos[path][idx] = {
origin: '',
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') {
infos[path][idx].origin = info.message;
} else if (info.kind === 'warning' ||
(info.kind === 'ignore-pre-upgrade-warning' && !repoGrid.majorUpgradeAllowed)
) {
} else if (info.kind === 'warning') {
infos[path][idx].warnings.push(info);
} else {
throw 'unknown info';
} else if (info.kind === 'ignore-pre-upgrade-warning') {
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.Index = 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 || [];
if (repo.Enabled && repo.warnings.some(w => w.property === 'Suites')) {
suitesWarning = true;
if (repo.Enabled) {
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);
@ -690,6 +762,7 @@ Ext.define('Proxmox.node.APTRepositories', {
vm.set('errors', errors);
vm.set('suitesWarning', suitesWarning);
vm.set('mixedSuites', mixedSuites);
me.getController().updateState();
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -220,7 +220,11 @@ Ext.define('Proxmox.DiskList', {
let extendedInfo = '';
if (rec) {
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()}`);
}
if (rec.data.journals > 0) {
@ -321,14 +325,14 @@ Ext.define('Proxmox.DiskList', {
dataIndex: 'status',
},
{
header: 'Mounted',
header: gettext('Mounted'),
width: 60,
align: 'right',
renderer: Proxmox.Utils.format_boolean,
dataIndex: 'mounted',
},
{
header: 'Wearout',
header: gettext('Wearout'),
width: 90,
sortable: true,
align: 'right',
@ -337,7 +341,7 @@ Ext.define('Proxmox.DiskList', {
if (Ext.isNumeric(value)) {
return (100 - value).toString() + '%';
}
return 'N/A';
return gettext('N/A');
},
},
],

View File

@ -3,6 +3,7 @@ Ext.define('Proxmox.EOLNotice', {
extend: 'Ext.Component',
alias: 'widget.proxmoxEOLNotice',
userCls: 'eol-notice',
padding: '0 5',
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."),
},
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() {
let me = this;
let iconCls = me.getIconCls();
let href = me.href.startsWith('http') ? me.href : `https://${me.href}`;
let message = Ext.String.format(
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>
`;

View 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();
},
},
],
},
],
});

View 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;
},
});

View File

@ -22,19 +22,28 @@ Ext.define('Proxmox.panel.LogView', {
updateParams: function() {
let me = this;
let viewModel = me.getViewModel();
let since = viewModel.get('since');
let until = viewModel.get('until');
if (viewModel.get('hide_timespan')) {
if (viewModel.get('hide_timespan') || viewModel.get('livemode')) {
return;
}
let since = viewModel.get('since');
let until = viewModel.get('until');
if (since > until) {
Ext.Msg.alert('Error', 'Since date must be less equal than Until date.');
return;
}
viewModel.set('params.since', Ext.Date.format(since, 'Y-m-d'));
viewModel.set('params.until', Ext.Date.format(until, 'Y-m-d') + ' 23:59:59');
let submitFormat = viewModel.get('submitFormat');
viewModel.set('params.since', Ext.Date.format(since, submitFormat));
if (submitFormat === 'Y-m-d') {
viewModel.set('params.until', Ext.Date.format(until, submitFormat) + ' 23:59:59');
} else {
viewModel.set('params.until', Ext.Date.format(until, submitFormat));
}
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) {
let me = this;
@ -189,6 +219,7 @@ Ext.define('Proxmox.panel.LogView', {
viewModel.set('since', since);
viewModel.set('params.limit', view.pageSize);
viewModel.set('hide_timespan', !view.log_select_timespan);
viewModel.set('submitFormat', view.submitFormat);
me.lookup('content').setStyle('line-height', `${view.lineHeight}px`);
view.loadTask = new Ext.util.DelayedTask(me.doLoad, me);
@ -224,6 +255,8 @@ Ext.define('Proxmox.panel.LogView', {
data: {
until: null,
since: null,
submitFormat: 'Y-m-d',
livemode: true,
hide_timespan: false,
data: {
start: 0,
@ -263,32 +296,70 @@ Ext.define('Proxmox.panel.LogView', {
},
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',
reference: 'since',
format: 'Y-m-d',
bind: {
disabled: '{livemode}',
value: '{since}',
maxValue: '{until}',
submitFormat: '{submitFormat}',
},
},
'Until: ',
{
xtype: 'datefield',
xtype: 'box',
autoEl: { cn: gettext('Until') + ':' },
bind: {
disabled: '{livemode}',
},
},
{
xtype: 'proxmoxDateTimeField',
name: 'until_date',
reference: 'until',
format: 'Y-m-d',
bind: {
disabled: '{livemode}',
value: '{until}',
minValue: '{since}',
submitFormat: '{submitFormat}',
},
},
{
xtype: 'button',
text: 'Update',
handler: 'updateParams',
bind: {
disabled: '{livemode}',
},
},
],
},

View File

@ -7,6 +7,8 @@ Ext.define('Proxmox.panel.NotesView', {
bodyPadding: 10,
scrollable: true,
animCollapse: false,
collapseFirst: false,
maxLength: 64 * 1024,
enableTBar: false,
onlineHelp: 'markdown_basics',
@ -17,6 +19,7 @@ Ext.define('Proxmox.panel.NotesView', {
items: [
{
text: gettext('Edit'),
iconCls: 'fa fa-pencil-square-o',
handler: function() {
let view = this.up('panel');
view.run_editor();
@ -109,7 +112,23 @@ Ext.define('Proxmox.panel.NotesView', {
listeners: {
render: function(c) {
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() {
let me = this;
@ -122,10 +141,11 @@ Ext.define('Proxmox.panel.NotesView', {
tools: [
{
type: 'gear',
handler: function() {
let view = this.up('panel');
view.run_editor();
glyph: 'xf044@FontAwesome', // fa-pencil-square-o
tooltip: gettext('Edit notes'),
callback: view => view.run_editor(),
style: {
paddingRight: '5px',
},
},
],

View 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`);
},
});

View 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
View 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;
},
});

View File

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

View 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();
},
});

View File

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

View File

@ -33,6 +33,13 @@
color: $icon-color;
}
// pve needs to set `color` on this pseudo element, but the api viewer and pbs
// add a filter to the parent. so invert it here again, to fix this for pve.
.x-tree-icon-parent:not(.x-tree-icon-custom)::before,
.x-tree-icon-parent-expanded:not(.x-tree-icon-custom)::before {
filter: invert(90%);
}
// but some are implement as background SVGs or PNGs. invert them via
// filters:
.fa-ceph::before,
@ -104,6 +111,9 @@
}
// pbs show task log in longest task list column
.fa.black,
.fa.black::after,
.fa.black::before,
.x-action-col-icon.fa-chevron-right::before {
filter: none;
}
@ -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
.usage-wrapper {
border: 1px solid $icon-color;

View File

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

View File

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

View File

@ -220,5 +220,6 @@ ${keyString}
printFrame.src = "data:text/html;base64," + btoa(html);
document.body.appendChild(printFrame);
me.on('destroy', () => document.body.removeChild(printFrame));
},
});

View File

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

14
src/window/AuthEditAD.js Normal file
View 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',
});

View File

@ -1,5 +1,8 @@
Ext.define('Proxmox.window.AuthEditBase', {
extend: 'Proxmox.window.Edit',
mixins: ['Proxmox.Mixin.CBind'],
showDefaultRealm: false,
isAdd: true,
@ -29,9 +32,9 @@ Ext.define('Proxmox.window.AuthEditBase', {
let authConfig = Proxmox.Schema.authDomains[me.authType];
if (!authConfig) {
throw 'unknown auth type';
throw `unknown auth type ${me.authType}`;
} 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;
@ -51,7 +54,9 @@ Ext.define('Proxmox.window.AuthEditBase', {
realm: me.realm,
xtype: authConfig.ipanel,
isCreate: me.isCreate,
useTypeInUrl: me.useTypeInUrl,
type: me.authType,
showDefaultRealm: me.showDefaultRealm,
},
{
title: gettext('Sync Options'),
@ -67,7 +72,9 @@ Ext.define('Proxmox.window.AuthEditBase', {
realm: me.realm,
xtype: authConfig.ipanel,
isCreate: me.isCreate,
useTypeInUrl: me.useTypeInUrl,
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
if (!me.useTypeInUrl && data.type !== me.authType) {
me.close();
throw "got wrong auth type";
throw `got wrong auth type '${me.authType}' for realm '${data.type}'`;
}
me.setValues(data);
},

View File

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

View File

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

View 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,
},
],
});

View 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'),
},
],
});

View File

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

View File

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

View 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();
}
},
});

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -7,27 +7,50 @@ Ext.define('Proxmox.window.PasswordEdit', {
url: '/api2/extjs/access/password',
width: 380,
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: [
{
xtype: 'textfield',
inputType: 'password',
fieldLabel: gettext('Password'),
minLength: 5,
fieldLabel: gettext('Your Current Password'),
reference: 'confirmation-password',
name: 'confirmation-password',
allowBlank: false,
vtype: 'password',
cbind: {
hidden: '{!confirmCurrentPassword}',
disabled: '{!confirmCurrentPassword}',
},
},
{
xtype: 'textfield',
inputType: 'password',
fieldLabel: gettext('New Password'),
allowBlank: false,
name: 'password',
listeners: {
change: (field) => field.next().validate(),
blur: (field) => field.next().validate(),
},
cbind: {
minLength: '{minLength}',
},
},
{
xtype: 'textfield',
inputType: 'password',
fieldLabel: gettext('Confirm password'),
fieldLabel: gettext('Confirm New Password'),
name: 'verifypassword',
allowBlank: false,
vtype: 'password',

View File

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

View File

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

View File

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