Compare commits

..

14 Commits

Author SHA1 Message Date
Thomas Lamprecht
a39544136d bump version to 2.6-2
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2022-03-15 17:29:32 +01:00
Thomas Lamprecht
b1c2c8154d add EOL notice component
to avoid copying the same thing to three different product's GUIs
this year..

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2022-03-15 17:28:57 +01:00
Thomas Lamprecht
ac4b6393d6 bump version to 2.6-1
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-06-25 09:34:40 +02:00
Thomas Lamprecht
dfc86baacb buildsys: touch incremental-lint in check target to avoid triggering twice
Without this the check and the, through the 'install' target
triggered, incremental lint target triggered a full eslint run.

Makes it similar to what PBS did from the beginning of eslint
inclusion..

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 557c45056c)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-06-25 09:32:10 +02:00
Thomas Lamprecht
86568d27c1 cbind mixin: also descend in elements with an cbind property
Not only into those with an xtype one, as we can either have a
implicit default xtype (e.g., in tbars for buttons, or set explicitly
via the `defaults` mechanism) or want to apply cbinds to stores or
other objects.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
(cherry picked from commit 5995eddcc4)
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-06-25 09:32:10 +02:00
Thomas Lamprecht
585f3c31e7 combo grid: load: rework auto-selection and validity logic
We do not want to trigger an autoSelect if there's a value set, even
if it isn't found in the store, as that hides the fact that an (now)
invalid valid is configured from the user, which can be confusing if
something is not working, as when editing an object it seems like a
valid value is selected.

Further, if a value is set we mark the field as invalid from the
start, at least if it's neither disabled nor allowed to have a
value which is does not exists in the backing store.

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-06-07 18:07:38 +02:00
Thomas Lamprecht
a0451be0ca combo grid: reformat/place comment
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-06-07 18:07:38 +02:00
Thomas Lamprecht
1b0b0c9c7b object grid: line-wrap cleanup
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-05-23 16:23:17 +02:00
Thomas Lamprecht
dc6f0d6399 bump version to 2.5-6
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-05-21 17:40:26 +02:00
Thomas Lamprecht
bb22262862 object grid: improve/add to documentation
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-05-21 17:39:43 +02:00
Thomas Lamprecht
c92dd68bab object grid: allow one to declaratively specify rows
So that users of this component do not necesacrrily need to add an
initComponent override and make the `me.add_XYZ_row()` there, but
instead can use something like:

  gridRows: [
    {
      xtype: 'text',
      name: 'http-proxy',
      text: gettext('HTTP proxy'),
      defaultValue: Proxmox.Utils.noneText,
      vtype: 'HttpProxy',
      deleteEmpty: true,
    },
  ],

I avoid using `rows` as config key as that is internally used for
quite a few things, and potentially some existing users (did not
checked all). We can still switch to that easily if it is deemed to
be better...

Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-05-21 17:39:43 +02:00
Thomas Lamprecht
22b189e40c object grid: code cleanup
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-05-21 17:39:43 +02:00
Thomas Lamprecht
2e8620aea2 bump version to 2.5-5
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-05-14 10:30:43 +02:00
Lorenz Stechauner
229cf5a35d disk smart: fix non working smart value window
fix regression in refactor from commit 7eb1fb18ad

Reported in forum:
https://forum.proxmox.com/threads/gui-disks-tab-cant-showup-smart-values.89180/
https://forum.proxmox.com/threads/smart-values-bug-in-6-4-6.89179/

Signed-off-by: Lorenz Stechauner <l.stechauner@proxmox.com>
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2021-05-14 10:30:02 +02:00
146 changed files with 1489 additions and 13900 deletions

10
.gitignore vendored
View File

@ -1,10 +0,0 @@
*.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

View File

@ -1,56 +1,46 @@
include /usr/share/dpkg/pkg-info.mk
export DEB_VERSION_UPSTREAM_REVISION
PACKAGE=proxmox-widget-toolkit
export PACKAGE=proxmox-widget-toolkit
BUILDDIR ?= ${PACKAGE}-${DEB_VERSION_UPSTREAM}
DEB=${PACKAGE}_${DEB_VERSION_UPSTREAM_REVISION}_all.deb
DSC=${PACKAGE}_${DEB_VERSION_UPSTREAM_REVISION}.dsc
DEB=$(PACKAGE)_$(DEB_VERSION)_all.deb
DEV_DEB=$(PACKAGE)-dev_$(DEB_VERSION)_all.deb
GITVERSION:=$(shell git rev-parse HEAD)
DEBS=$(DEB) $(DEV_DEB)
DSC=$(PACKAGE)_$(DEB_VERSION).dsc
BUILDDIR ?= $(PACKAGE)-$(DEB_VERSION_UPSTREAM)
$(BUILDDIR): GITVERSION:=$(shell git rev-parse HEAD)
$(BUILDDIR):
rm -rf $(BUILDDIR) $(BUILDDIR).tmp
cp -a src/ $(BUILDDIR).tmp
cp -a debian $(BUILDDIR).tmp/
echo "git clone git://git.proxmox.com/git/proxmox-widget-toolkit.git\\ngit checkout $(GITVERSION)" > $(BUILDDIR).tmp/debian/SOURCE
mv $(BUILDDIR).tmp/ $(BUILDDIR)
${BUILDDIR}:
rm -rf ${BUILDDIR} ${BUILDDIR}.tmp
cp -a src/ ${BUILDDIR}.tmp
cp -a debian ${BUILDDIR}.tmp/
echo "git clone git://git.proxmox.com/git/proxmox-widget-toolkit.git\\ngit checkout ${GITVERSION}" > ${BUILDDIR}.tmp/debian/SOURCE
mv ${BUILDDIR}.tmp/ ${BUILDDIR}
.PHONY: deb
deb: $(DEBS)
$(DEBS): $(BUILDDIR)
cd $(BUILDDIR); dpkg-buildpackage -b -us -uc
lintian $(DEBS)
deb: ${DEB}
${DEB}: ${BUILDDIR}
cd ${BUILDDIR}; dpkg-buildpackage -b -us -uc
lintian ${DEB}
.PHONY: dsc
dsc: $(DSC)
$(MAKE) clean
$(MAKE) $(DSC)
lintian $(DSC)
$(DSC): $(BUILDDIR)
cd $(BUILDDIR); dpkg-buildpackage -S -us -uc -d
sbuild: $(DSC)
sbuild $(DSC)
dsc: ${DSC}
${DSC}: ${BUILDDIR}
cd ${BUILDDIR}; dpkg-buildpackage -S -us -uc -d
lintian ${DSC}
.PHONY: lint
lint: $(JSSRC)
$(MAKE) -C src lint
lint: ${JSSRC}
${MAKE} -C src lint
.PHONY: upload
upload: UPLOAD_DIST ?= $(DEB_DISTRIBUTION)
upload: $(DEBS)
tar cf - $(DEB) | ssh -X repoman@repo.proxmox.com -- upload --product pve,pmg,pbs --dist $(UPLOAD_DIST)
tar cf - $(DEV_DEB) | ssh -X repoman@repo.proxmox.com -- upload --product devel --dist $(UPLOAD_DIST)
upload: ${DEB}
tar cf - ${DEB} | ssh -X repoman@repo.proxmox.com -- upload --product pve,pmg,pbs --dist buster
distclean: clean
clean:
$(MAKE) -C src clean
rm -rf $(PACKAGE)-[0-9]*/ *.tar.* *.dsc *.deb *.changes *.buildinfo *.build
rm -rf ${BUILDDIR} ${BUILDDIR}.tmp *.tar.gz *.dsc *.deb *.changes *.buildinfo
find . -name '*~' -exec rm {} ';'
.PHONY: dinstall
dinstall: $(DEBS)
dpkg -i $(DEBS)
dinstall: ${DEB}
dpkg -i ${DEB}

945
debian/changelog vendored
View File

@ -1,946 +1,29 @@
proxmox-widget-toolkit (4.3.7) bookworm; urgency=medium
proxmox-widget-toolkit (2.6-2) buster; urgency=medium
* authentication realm edit: use correct property to derive the realm type.
* add EOL notice component
* authentication view: allow downstream users to override the API path to
query available authentication realms from.
-- Proxmox Support Team <support@proxmox.com> Tue, 15 Mar 2022 17:29:26 +0100
* from: realm combobox: allow downstream users to override the API path to
query available authentication realms from.
proxmox-widget-toolkit (2.6-1) buster; urgency=medium
-- 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:
- fix focus and focus-over states for tabs
- fix the focused state for background image grid icons
- style the icon for the datastore maintenance mode
- improve apt repo group header contrast ratios
- adjust panel header tool icons
- fix #4618: lighten critical/warning charts/gauges colors
* form: combo grid: use correct method to initialize the picker to ensure
it's cleaned up again after closed.
-- Proxmox Support Team <support@proxmox.com> Wed, 17 May 2023 14:02:50 +0200
proxmox-widget-toolkit (3.6.5) bullseye; urgency=medium
* window: ldap auth edit: avoid relying on the default bind property
* window: ldap auth edit: set view-model form data explicitly on edit to
avoid a data race in chromium based browser that could result in a
mismatch of the configured value and the initially shown one.
-- Proxmox Support Team <support@proxmox.com> Tue, 28 Mar 2023 17:56:10 +0200
proxmox-widget-toolkit (3.6.4) bullseye; urgency=medium
* dark-mode:
- improve contrast on split buttons
- color the custom grid and tree icons
- set boundlist (combo box picker) background so that loading progress or
errors are styled correctly too
- add a small white padding to the TOTP QR-code, as some apps are confused
otherwise
- fix #4617: increase brightness of tree expand/collapse arrows to avoid
overly low contrast
* fix #4612: mobile: avoid crash due to missing getProxy method
* theme edit window: ensure that the saved theme is actually valid
* language selector: translate entries to both native and localized variants
* language selector: increase picker list view width
-- Proxmox Support Team <support@proxmox.com> Sun, 26 Mar 2023 17:52:48 +0200
proxmox-widget-toolkit (3.6.3) bullseye; urgency=medium
* dark-mode fine-tuning:
- fix highlighting of active elements in drop down menus
- set the icon color of filtered column headers properly
- style checkboxes that don't use blueish active states
- style locked guest icons properly
- tone down border on ceph install card-like window
-- Proxmox Support Team <support@proxmox.com> Wed, 22 Mar 2023 13:25:05 +0100
proxmox-widget-toolkit (3.6.2) bullseye; urgency=medium
* network edit: add tooltip to bridge ports inputs
* dark-mode: reduce background mask opacity to 0.5
* dark-mode: make window shadow black again to avoid a backlight that some
have very strong opionions against
* rename "Theme" selector to "Color Theme" to add some context for
translation
-- Proxmox Support Team <support@proxmox.com> Tue, 21 Mar 2023 16:46:27 +0100
proxmox-widget-toolkit (3.6.1) bullseye; urgency=medium
* repo view: replace non-clickable checkbox with icons
* auth ui: add LDAP realm-edit panel and sync UI, refactored & adapted from
the pve-manager implementation for future reuse
* dark-theme: improve help button contrast ratios in focused state
* dark-theme: make "sorted-by" header highlight more subtle
* dark-theme: dim warning and invalid colors more
* dark-theme: let the background "shine through" mask more to avoid that
information on it becomes unreadable
-- Proxmox Support Team <support@proxmox.com> Mon, 20 Mar 2023 14:13:42 +0100
proxmox-widget-toolkit (3.6.0) bullseye; urgency=medium
* node apt: make changelog window taller for 4:3 ratio and cleanup/modernize
code
* ui: SMART: show SMART data in correct columns with correct values
* fix #4421: ui: guard setProxy against races of slow vs fast requests
* dark-theme: add initial version of the proxmox-dark theme
* subscription/summary/backup: stop setting the background color
* gauge widget: add support for a dark theme and dynamic theme switching
* rrd chart: add support for the dark theme and dynamic theme switching
* form: add a theme selector window
* dark-theme:
- fix summary row background
- increase contrast on check-boxes
- visually remove the border around the pve resource tree
- remove thicker borders around content
- re-work buttons colors to appear dimmer
- make windows stand out more against the background mask
* fix #4585 : toolkit: configid type: add missing "-" character support
* input panel: improve validity change check for advanced fields
* auth-realm selector: add custom store filters for callers
-- Proxmox Support Team <support@proxmox.com> Tue, 14 Mar 2023 16:25:37 +0100
proxmox-widget-toolkit (3.5.5) bullseye; urgency=medium
* combobox grid: use the grids view for the error message
* combobox grid: make height for the error configurable
* combobox grid: instead of hiding the picker collapse it, keeping the
internal state consitent which avoids, among other things, the need
fo two clicks after re-selecting an item
* utils: always html-encode response message from errors to avoid rendering
glitches, that while not known to be problematic from a safety POV, are
possibly odd for people to find.
* form: display-edit: add safe default renderer for display field to avoid
unproblematic, but possible odd and glitchy side-effects from the
value-bind if the display-edit field is in iput mode.
* api request: add wide spread alert-error logic as smart-on option
-- Proxmox Support Team <support@proxmox.com> Tue, 31 Jan 2023 17:27:41 +0100
proxmox-widget-toolkit (3.5.4) bullseye; urgency=medium
* api-viewer: allow text selection in the parameter and the return grids
* task viewer: add optional button to download full task-log
* permission role selector: fix renderer for column of included privileges
for Proxmox VE
* permission role selector: make slightly more wide and resizeable
* node network view: rework finding free interface ID and move add-menu
generation to common helper (no semantic change intended)
-- Proxmox Support Team <support@proxmox.com> Wed, 11 Jan 2023 16:09:53 +0100
proxmox-widget-toolkit (3.5.3) bullseye; urgency=medium
* css: do not make full-style tags display as inline-block in the tree
to avoid height jumps
* log, journal view: fix access to `me` after destroying
-- Proxmox Support Team <support@proxmox.com> Mon, 21 Nov 2022 11:14:27 +0100
proxmox-widget-toolkit (3.5.2) bullseye; urgency=medium
* host disks: add 'mounted' column
* host disk: handle partition data from Proxmox Backup Server backend
* number field: avoid that a single up/down arrow key press
increment or decrements twice
* toolkit: make email regex pattern match pve-common
* css: import action column fix from pbs, pmg
* fix #2703: networkedit: limit custom interface name field to 15
characters.
* task progress: show text instead of bogus percentage
* fix #3593: add CPU affinity task set type
* input panel: add onSetValues hook
* add tag related helpers
* toolkit: add override for ExtJS DragDropManager to fix selection behavior
in the drag zone
* rdd charts: don't display power-of-two suffix 'i' for values without unit
suffix
* fix #4271: api-viewer: display nested formats instead of `[object Object]`
* api-viewer: show min/max for values without any other format
-- Proxmox Support Team <support@proxmox.com> Thu, 17 Nov 2022 08:37:26 +0100
proxmox-widget-toolkit (3.5.1) bullseye; urgency=medium
* pxar file types: fix over-eager s/text/label/ so that text file icon is
shown again
* file browser: disable item # size rendering
-- Proxmox Support Team <support@proxmox.com> Mon, 16 May 2022 18:03:35 +0200
proxmox-widget-toolkit (3.5.0) bullseye; urgency=medium
* file browser: try reload again when getting a 503 error
* ui: acl role selector: make the picker grid wider and ensure that the
text wraps in the privilege column
* fix #4001: file browser: add a configurable prefix to downloaded files
* fix #4001: file browser: show number of items in a directory as size, if
available
* file browser: align size column to end/right
* file browser: only disable, not hide button if not downloadable and add
hint for why and what to do in tooltip
* switch to native version format for native package
-- Proxmox Support Team <support@proxmox.com> Sun, 15 May 2022 11:46:54 +0200
proxmox-widget-toolkit (3.4-10) bullseye; urgency=medium
* css: add proxmox-good-row class
* status view: fix usage calculation for fields without valid values, like
for example SWAP can often be.
* buttons: add AltText to unify the hack to detect the max size of a button
that switches its text dynamically
-- Proxmox Support Team <support@proxmox.com> Wed, 27 Apr 2022 18:58:21 +0200
proxmox-widget-toolkit (3.4-9) bullseye; urgency=medium
* file browser: optionally allow showing a "download as tar.zst"
button, if supported
* tab buttons: fix vertial centering of text and reduce padding
slightly
* move over the markdown based notes view panel and edit window from
pve-manager for reuse
-- Proxmox Support Team <support@proxmox.com> Thu, 14 Apr 2022 07:56:54 +0200
proxmox-widget-toolkit (3.4-8) bullseye; urgency=medium
* fix #3919: log view: show first task output line correctly
* combo grid: clear filter on blur
* utils: clear cookies with secure flag set to avoid bogus browser
warning
* node tasks: do not count preset filters as normal filters to avoid
"clear filter" button glitch
* icons: switch cpu and ram bitmaps to svg
* object grid: call rendere with our scope
* dns view: increase api polling intervall from 1s to 10s
-- Proxmox Support Team <support@proxmox.com> Tue, 12 Apr 2022 16:45:50 +0200
proxmox-widget-toolkit (3.4-7) bullseye; urgency=medium
* extjs: fix check for 'touch' input event in drag and drop handler, making
the 'pen' pointer event source type, that chrome/chromium emits in some
setups, work again.
-- Proxmox Support Team <support@proxmox.com> Wed, 23 Feb 2022 12:12:13 +0100
proxmox-widget-toolkit (3.4-6) bullseye; urgency=medium
* utils: render language: fix rendering special default value
* sorters: use correct property 'direction' and keep default 'ASC'
-- Proxmox Support Team <support@proxmox.com> Mon, 14 Feb 2022 11:34:42 +0100
proxmox-widget-toolkit (3.4-5) bullseye; urgency=medium
* login: tfa: hide u2f and yubico-otp if not available
* improve error handling when adding webauthn entries
* toolkit: fix noisy ext warning of feature we do not want/use
* zfs detail: increase default window height
* zfs detail: hide the pool itself in tree view
-- Proxmox Support Team <support@proxmox.com> Thu, 13 Jan 2022 12:52:18 +0100
proxmox-widget-toolkit (3.4-4) bullseye; urgency=medium
* utils: format duration: render years when we can avoid huge day numbers
* journalview: fix wrong initial load with default timespan on widget
creation
* logpanel: fix glitching on fast task logs
* logpanel: actually catch up when following the log for tasks with an
almost artificially high log output traffic
* log viewer: add heuristic for scroll-direction dependent ratio
distribution
* log viewer: add heuristic for triggering a new limit load earlier to
reduce latency on casual scrolling
-- Proxmox Support Team <support@proxmox.com> Wed, 24 Nov 2021 18:29:56 +0100
proxmox-widget-toolkit (3.4-3) bullseye; urgency=medium
* data: diffstore: fix autoDestroyRstore option (regression from ExtJS 7)
* ui: OpenID edit: make username-claim field editable for arbitrary values
* ui: OpenID realm: allow to edit scopes
* ui: OpenID realm: allow to edit prompt
* ui: OpenID realm: allow to edit acr values
* form: copy BandwidthSelector/SizeField from Proxmox VE's manager
* bandwidth/utils: move out SizeUnits definition to more common module
* utils: add size unit related helpers to parse/auto-scale/format
* bandwidth field: allow to submit auto-scaled size-units as string
-- Proxmox Support Team <support@proxmox.com> Sat, 20 Nov 2021 21:41:37 +0100
proxmox-widget-toolkit (3.4-2) bullseye; urgency=medium
* TFA login window: fix a formatted label showed when being low on unused
recovery-keys
* proxmox checkbox: add clearOnDisable config
-- Proxmox Support Team <support@proxmox.com> Mon, 15 Nov 2021 10:23:34 +0100
proxmox-widget-toolkit (3.4-1) bullseye; urgency=medium
* panel/RRDCharts: enable scrolling for RRDCharts on touchscreens
* disk selector: allow requesting partitions too
* fix #3589: show device name in title for SMART values window
* cbind: document cbind by adding a small summary and example
* add common utils used for u2f and webauthn, adapted from PVE and PBS,
respectively
* add TFA-login, TOTP, WebAuthn and recover-key edit windows for better reuse
* disk list: allow wiping individual partitions
-- Proxmox Support Team <support@proxmox.com> Thu, 11 Nov 2021 21:11:16 +0100
proxmox-widget-toolkit (3.3-6) bullseye; urgency=medium
* fix #3542: node: task logs: query correct node for tasks in clusters
* node: add a, by default hidden, MTU column in network view
-- Proxmox Support Team <support@proxmox.com> Tue, 27 Jul 2021 16:41:01 +0200
proxmox-widget-toolkit (3.3-5) bullseye; urgency=medium
* node: repos: add possibility to link online help
* api-viewer: drop extra slash in api path
* apt: match "Debian Backports" origin as Debian one
* add new shared component for the package version window
-- Proxmox Support Team <support@proxmox.com> Mon, 19 Jul 2021 17:52:08 +0200
proxmox-widget-toolkit (3.3-4) bullseye; urgency=medium
* acme: allow wildcards as domain
* service view: avoid showing not installed services as error
* service view: fix stale stop/restart button enabled behavior
* service view: disable all buttons for masked/not-found/unknown services
-- Proxmox Support Team <support@proxmox.com> Tue, 13 Jul 2021 18:42:51 +0200
proxmox-widget-toolkit (3.3-3) bullseye; urgency=medium
* realm view/edit: make more generic for better reuse
-- Proxmox Support Team <support@proxmox.com> Mon, 12 Jul 2021 09:52:27 +0200
proxmox-widget-toolkit (3.3-2) bullseye; urgency=medium
* node: repos: only show suites warning at the top if enabled repository is
affected
* move over authentication-real edit window widget from Proxmox VE
* utils: add helper to format node's repository status
-- Proxmox Support Team <support@proxmox.com> Fri, 09 Jul 2021 17:30:43 +0200
proxmox-widget-toolkit (3.2-5) bullseye; urgency=medium
* network: always ask for confirmation before removing a network
interface from the configuration. While it is not that dangerous as we
have a pending config that needs to get applied, it still is nicer to do
for remove actions.
* node: tasks: use helper to format status again for a localized warnings
text
* window: safe-destroy: add taskDone and apiCallDone callbacks
-- Proxmox Support Team <support@proxmox.com> Thu, 08 Jul 2021 14:30:44 +0200
proxmox-widget-toolkit (3.2-4) bullseye; urgency=medium
* node: APT repositories: upgrade "no Proxmox product repo configured" from
warning to error
* node: task history: deselect entries when filter changes
* node: task history: show errors on store load
* node: task history: add 'clear filter' button
-- Proxmox Support Team <support@proxmox.com> Mon, 05 Jul 2021 16:50:23 +0200
proxmox-widget-toolkit (3.2-3) bullseye; urgency=medium
* node: repos: handle that components can be undefined
* markdown: encode bad nodes HTML instead of pruning it
* markdown: make sanitizer more strict in filtering tags and ensure that the
src and the href attributes point to a HTTP url, or is a data-url on a
image.
* info widget: early return from update if text & value stayed the same
* utils: updateColumnWidth: drop duplicate implementation and allow
overriding tresholdWidth
* utils: updateColumnWidth: directly calculate column count by threshold,
automatically using more columns on wide containers.
-- Proxmox Support Team <support@proxmox.com> Mon, 05 Jul 2021 10:10:47 +0200
proxmox-widget-toolkit (3.2-2) bullseye; urgency=medium
* avoid using unique ids for components that may have more than one instance
at the same time. Fixes and issue with switching between nodes on the new
APT Repository panel.
-- Proxmox Support Team <support@proxmox.com> Mon, 05 Jul 2021 09:47:46 +0200
proxmox-widget-toolkit (3.2-1) bullseye; urgency=medium
* css: some markdown heading and paragraph font-size & padding tuning
* node: services: fix logic for displaying unit state
* add Debian and Proxmox symbol logos and css
* node: apt: spawn a window for adding repository
* utils: add getOpenIDRedirectionAuthorization helper
* factor out userid parsing and username, realm renderer to Utils
* add OpenID icon + css class
* node: APT Repositories: rework top status and error grid
-- Proxmox Support Team <support@proxmox.com> Sat, 03 Jul 2021 00:12:34 +0200
proxmox-widget-toolkit (3.1-4) bullseye; urgency=medium
* combo grid: load: rework auto-selection and validity logic to ensure it's
always shown if a (now) invalid value is configured
* cbind mixin: also descend in elements with an cbind property
* node: tasks: merge improved Filters over from Proxmox Backup Server
-- Proxmox Support Team <support@proxmox.com> Fri, 25 Jun 2021 09:32:40 +0200
-- Proxmox Support Team <support@proxmox.com> Mon, 28 Jun 2021 19:14:10 +0200
proxmox-widget-toolkit (3.1-3) bullseye; urgency=medium
* css: markdown: add some nicer table, blockquote and task-list checkbox
styling
* Journal View: fix flickering in journal livemode
* node/services: optionally show unit-, and active-states
* add initial building blocks for an APT repositories UI
-- Proxmox Support Team <support@proxmox.com> Wed, 23 Jun 2021 23:11:37 +0200
proxmox-widget-toolkit (3.1-2) bullseye; urgency=medium
* ui: network: add, by default hidden, columns for the `vlan-id` and the
`vlan-raw-device`
* add interface for markdown parser and wire-up marked to it
-- Proxmox Support Team <support@proxmox.com> Fri, 18 Jun 2021 15:32:27 +0200
proxmox-widget-toolkit (3.1-1) bullseye; urgency=medium
* support ExtJS 7
proxmox-widget-toolkit (2.5-6) buster; urgency=medium
* object grid: allow one to declaratively specify rows
* disk list: add wipe disk button which users of this widget can opt-in
-- Proxmox Support Team <support@proxmox.com> Fri, 21 May 2021 17:39:59 +0200
* data/ProxmoxProxy: set responseType to undefined for XMLHTTPRequest
* ship api-viewer in new proxmox-widget-toolkit-dev package to improve
possibility for code reuse
-- Proxmox Support Team <support@proxmox.com> Wed, 02 Jun 2021 16:16:02 +0200
proxmox-widget-toolkit (3.0-2) bullseye; urgency=medium
proxmox-widget-toolkit (2.5-5) pve pmg; urgency=medium
* disks: fix regression in S.M.A.R.T. window
-- Proxmox Support Team <support@proxmox.com> Fri, 14 May 2021 10:34:57 +0200
proxmox-widget-toolkit (3.0-1) bullseye; urgency=medium
* re-build for Debian 11 Bullseye based releases
-- Proxmox Support Team <support@proxmox.com> Thu, 13 May 2021 19:46:29 +0200
-- Proxmox Support Team <support@proxmox.com> Fri, 14 May 2021 10:30:39 +0200
proxmox-widget-toolkit (2.5-4) pve pmg; urgency=medium
@ -949,7 +32,7 @@ proxmox-widget-toolkit (2.5-4) pve pmg; urgency=medium
* node disk: S.M.A.R.T.: improve the simple layout and enable autoscroll for
long output
* format/render size: allow one to specify if base 2 or 10 (SI unit) is
* format/render size: allow one to specifiy if base 2 or 10 (SI unit) is
desired
-- Proxmox Support Team <support@proxmox.com> Fri, 07 May 2021 18:00:29 +0200
@ -965,7 +48,7 @@ proxmox-widget-toolkit (2.5-2) pve pmg; urgency=medium
* rrd chart: add option to render values and Y-axis with a power-of-two base
* safe destroy: allow specifying additional items
* safe destroy: allow specifing additional items
* utils: add several render and helper functions from Proxmox VE's manager
@ -1646,7 +729,7 @@ proxmox-widget-toolkit (1.0-6) unstable; urgency=medium
* change 'create' parameter to 'isCreate'
* make network devices types configurable
* make network devices types configureable
* use Proxmox.window.TaskProgress instead of PVE.window.TaskProgress

1
debian/compat vendored Normal file
View File

@ -0,0 +1 @@
12

21
debian/control vendored
View File

@ -2,24 +2,13 @@ Source: proxmox-widget-toolkit
Section: web
Priority: optional
Maintainer: Proxmox Support Team <support@proxmox.com>
Build-Depends: debhelper-compat (= 13),
libjs-marked,
pve-eslint (>= 7.28.0),
sassc,
uglifyjs,
Standards-Version: 4.6.2
Build-Depends: debhelper (>= 12~),
pve-eslint (>= 7.12.1-1),
Standards-Version: 4.5.1
Homepage: https://www.proxmox.com
Package: proxmox-widget-toolkit
Architecture: all
Depends: ${misc:Depends}
Description: Core Widgets and ExtJS Helper Classes for Proxmox Web UIs
The base framework providing widgets, models, and general utilities for the
ExtJS based Web UIs of various Proxmox projects
Package: proxmox-widget-toolkit-dev
Architecture: all
Depends: ${misc:Depends}
Description: ExtJS based widgets and utilities for development
Contains some common JavaScript code that some Proxmox projects might used to
build common interfaces, like the API viewer in each documnetation repo.
Description: ExtJS Helper Classes for Proxmox
ExtJS Helper Classes to easy access to Proxmox APIs.

30
debian/copyright vendored
View File

@ -2,25 +2,15 @@ Copyright (C) 2010-2021 Proxmox Server Solutions GmbH
This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
License: AGPLv3
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Marked:
The Marked JavaScript library is shipped through linkage from the Debian
package unmodified alongside proxmox-widget-toolkit.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
Copyright (c) 2018+, MarkedJS (https://github.com/markedjs/) Copyright (c)
2011-2018, Christopher Jeffrey (https://github.com/chjj/)
For the license and copyright details see `/usr/share/doc/libjs-marked/copyright`
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@ -1,2 +0,0 @@
Toolkit.js /usr/share/javascript/proxmox-widget-toolkit-dev/
api-viewer/APIViewer.js /usr/share/javascript/proxmox-widget-toolkit-dev/

View File

@ -1 +0,0 @@
debian/SOURCE

View File

@ -1 +0,0 @@
/usr/share/javascript/proxmox-widget-toolkit

4
debian/rules vendored
View File

@ -3,10 +3,6 @@
# output every command that modifies files on the build system.
#DH_VERBOSE = 1
include /usr/share/dpkg/pkg-info.mk
export DEB_SOURCE
export DEB_VERSION
%:
dh $@

View File

@ -1 +1 @@
3.0 (native)
1.0

View File

@ -1,19 +1,11 @@
include defines.mk
ESLINT ?= $(if $(shell command -v pve-eslint), pve-eslint, eslint)
SUBDIRS= css images proxmox-dark
# bundle it for now from the libjs-marked debian package to avoid touching our proxies file mapper,
# we could also just ship a link to the packages file and load from same path as the widget-toolkit
MARKEDJS=/usr/share/javascript/marked/marked.js
SUBDIRS= css images
JSSRC= \
Utils.js \
Schema.js \
Toolkit.js \
Logo.js \
Parser.js \
mixin/CBind.js \
data/reader/JsonObject.js \
data/ProxmoxProxy.js \
@ -22,17 +14,13 @@ 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 \
form/BandwidthSelector.js \
form/DisplayEdit.js \
form/ExpireDate.js \
form/IntegerField.js \
form/TextField.js \
form/TextAreaField.js \
form/VlanField.js \
form/DateTimeField.js \
form/Checkbox.js \
form/KVComboBox.js \
@ -48,71 +36,38 @@ JSSRC= \
form/MultiDiskSelector.js \
form/TaskTypeSelector.js \
form/ACME.js \
form/UserSelector.js \
form/ThemeSelector.js \
form/FingerprintField.js \
button/Button.js \
button/AltText.js \
button/HelpButton.js \
grid/ObjectGrid.js \
grid/PendingObjectGrid.js \
panel/AuthView.js \
panel/DiskList.js \
panel/EOLNotice.js \
panel/DiskList.js \
panel/InputPanel.js \
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 \
window/PackageVersions.js \
window/TaskViewer.js \
window/LanguageEdit.js \
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 \
window/AddWebauthn.js \
window/AddYubico.js \
window/TfaEdit.js \
window/NotesEdit.js \
window/ThemeEdit.js \
window/SyncWindow.js \
node/APT.js \
node/APTRepositories.js \
node/NetworkEdit.js \
node/NetworkView.js \
node/DNSEdit.js \
@ -123,38 +78,30 @@ JSSRC= \
node/TimeEdit.js \
node/TimeView.js
all: $(SUBDIRS)
set -e && for i in $(SUBDIRS); do $(MAKE) -C $$i; done
all: ${SUBDIRS}
set -e && for i in ${SUBDIRS}; do ${MAKE} -C $$i; done
.lint-incremental: $(JSSRC)
$(ESLINT) $?
.lint-incremental: ${JSSRC}
eslint $?
touch "$@"
.PHONY: lint
check: lint
$(ESLINT) --strict api-viewer/APIViewer.js
lint: $(JSSRC)
$(ESLINT) --strict $(JSSRC)
lint: ${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)
proxmoxlib.js: .lint-incremental ${JSSRC}
# add the version as comment in the file
echo "// v$(BUILD_VERSION)-t$(BUILD_TIME)" > $@.tmp
cat $(JSSRC) $(MARKEDJS) >> $@.tmp
echo "// ${DEB_VERSION_UPSTREAM_REVISION}" > $@.tmp
cat ${JSSRC} >> $@.tmp
mv $@.tmp $@
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 proxmoxlib.min.js $(WWWBASEDIR)
set -e && for i in $(SUBDIRS); do $(MAKE) -C $$i $@; done
install: proxmoxlib.js
install -d -m 755 ${WWWBASEDIR}
install -m 0644 proxmoxlib.js ${WWWBASEDIR}
set -e && for i in ${SUBDIRS}; do ${MAKE} -C $$i $@; done
.PHONY: clean
clean:
$(MAKE) -C proxmox-dark $@
rm -f proxmoxlib.js* proxmoxlib.min.js* .lint-incremental
rm -f proxmoxlib.js

View File

@ -1,74 +0,0 @@
// NOTE: just relays parsing to markedjs parser
Ext.define('Proxmox.Markdown', {
alternateClassName: 'Px.Markdown', // just trying out something, do NOT copy this line
singleton: true,
// transforms HTML to a DOM tree and recursively descends and HTML-encodes every branch with a
// "bad" node.type and drops "bad" attributes from the remaining nodes.
// "bad" means anything which can do XSS or break the layout of the outer page
sanitizeHTML: function(input) {
if (!input) {
return input;
}
let _isHTTPLike = value => value.match(/^\s*https?:/i); // URL's protocol ends with :
let _sanitize;
_sanitize = (node) => {
if (node.nodeType === 3) return;
if (node.nodeType !== 1 ||
/^(script|style|form|select|option|optgroup|map|area|canvas|textarea|applet|font|iframe|audio|video|object|embed|svg)$/i.test(node.tagName)
) {
// could do node.remove() instead, but it's nicer UX if we keep the (encoded!) html
node.outerHTML = Ext.String.htmlEncode(node.outerHTML);
return;
}
for (let i=node.attributes.length; i--;) {
const name = node.attributes[i].name;
const value = node.attributes[i].value;
const canonicalTagName = node.tagName.toLowerCase();
// TODO: we may want to also disallow class and id attrs
if (
!/^(class|id|name|href|src|alt|align|valign|disabled|checked|start|type|target)$/i.test(name)
) {
node.attributes.removeNamedItem(name);
} else if ((name === 'href' || name === 'src') && !_isHTTPLike(value)) {
let safeURL = false;
try {
let url = new URL(value, window.location.origin);
safeURL = _isHTTPLike(url.protocol);
if (canonicalTagName === 'img' && url.protocol.toLowerCase() === 'data:') {
safeURL = true;
} else if (canonicalTagName === 'a') {
// allow most link protocols so admins can use short-cuts to, e.g., RDP
safeURL = url.protocol.toLowerCase() !== 'javascript:'; // eslint-disable-line no-script-url
}
if (safeURL) {
node.attributes[i].value = url.href;
} else {
node.attributes.removeNamedItem(name);
}
} catch (e) {
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]);
};
const doc = new DOMParser().parseFromString(`<!DOCTYPE html><html><body>${input}`, 'text/html');
doc.normalize();
_sanitize(doc.body);
return doc.body.innerHTML;
},
parse: function(markdown) {
/*global marked*/
let unsafeHTML = marked.parse(markdown);
return `<div class="pmx-md">${this.sanitizeHTML(unsafeHTML)}</div>`;
},
});

View File

@ -1,99 +0,0 @@
Ext.define('Proxmox.Schema', { // a singleton
singleton: true,
authDomains: {
pam: {
name: 'Linux PAM',
ipanel: 'pmxAuthSimplePanel',
onlineHelp: 'user-realms-pam',
add: false,
edit: true,
pwchange: true,
sync: false,
useTypeInUrl: false,
},
openid: {
name: gettext('OpenID Connect Server'),
ipanel: 'pmxAuthOpenIDPanel',
add: true,
edit: true,
tfa: false,
pwchange: false,
sync: false,
iconCls: 'pmx-itype-icon-openid-logo',
useTypeInUrl: true,
},
ldap: {
name: gettext('LDAP Server'),
ipanel: 'pmxAuthLDAPPanel',
syncipanel: 'pmxAuthLDAPSyncPanel',
add: true,
edit: true,
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
overrideAuthDomains: function(extra) {
for (const [key, value] of Object.entries(extra)) {
Proxmox.Schema.authDomains[key] = value;
}
},
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') },
d: { icon: 'folder-o', label: gettext('Directory') },
f: { icon: 'file-text-o', label: gettext('File') },
h: { icon: 'file-o', label: gettext('Hardlink') },
l: { icon: 'link', label: gettext('Softlink') },
p: { icon: 'exchange', label: gettext('Pipe/Fifo') },
s: { icon: 'plug', label: gettext('Socket') },
v: { icon: 'cube', label: gettext('Virtual') },
},
});

File diff suppressed because it is too large Load Diff

View File

@ -62,44 +62,37 @@ utilities: {
stateText: gettext('State'),
groupText: gettext('Group'),
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")}`,
en: `English - ${gettext("English")}`,
es: `Español - ${gettext("Spanish")}`,
eu: `Euskera (Basque) - ${gettext("Euskera (Basque)")}`,
fa: `فارسی - ${gettext("Persian (Farsi)")}`,
fr: `Français - ${gettext("French")}`,
hr: `Hrvatski - ${gettext("Croatian")}`,
he: `עברית - ${gettext("Hebrew")}`,
it: `Italiano - ${gettext("Italian")}`,
ja: `日本語 - ${gettext("Japanese")}`,
ka: `ქართული - ${gettext("Georgian")}`,
ko: `한국어 - ${gettext("Korean")}`,
nb: `Bokmål - ${gettext("Norwegian (Bokmal)")}`,
nl: `Nederlands - ${gettext("Dutch")}`,
nn: `Nynorsk - ${gettext("Norwegian (Nynorsk)")}`,
pl: `Polski - ${gettext("Polish")}`,
pt_BR: `Português Brasileiro - ${gettext("Portuguese (Brazil)")}`,
ru: `Русский - ${gettext("Russian")}`,
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)")}`,
language_map: {
ar: 'Arabic',
ca: 'Catalan',
zh_CN: 'Chinese (Simplified)',
zh_TW: 'Chinese (Traditional)',
da: 'Danish',
nl: 'Dutch',
en: 'English',
eu: 'Euskera (Basque)',
fr: 'French',
de: 'German',
he: 'Hebrew',
it: 'Italian',
ja: 'Japanese',
kr: 'Korean',
nb: 'Norwegian (Bokmal)',
nn: 'Norwegian (Nynorsk)',
fa: 'Persian (Farsi)',
pl: 'Polish',
pt_BR: 'Portuguese (Brazil)',
ru: 'Russian',
sl: 'Slovenian',
es: 'Spanish',
sv: 'Swedish',
tr: 'Turkish',
},
render_language: function(value) {
if (!value || value === '__default__') {
if (!value) {
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 + ')';
@ -107,8 +100,6 @@ utilities: {
return value;
},
renderEnabledIcon: enabled => `<i class="fa fa-${enabled ? 'check' : 'minus'}"></i>`,
language_array: function() {
let data = [['__default__', Proxmox.Utils.render_language('')]];
Ext.Object.each(Proxmox.Utils.language_map, function(key, value) {
@ -118,31 +109,6 @@ utilities: {
return data;
},
theme_map: {
crisp: 'Light theme',
"proxmox-dark": 'Proxmox Dark',
},
render_theme: function(value) {
if (!value || value === '__default__') {
return Proxmox.Utils.defaultText + ' (auto)';
}
let text = Proxmox.Utils.theme_map[value];
if (text) {
return text;
}
return value;
},
theme_array: function() {
let data = [['__default__', Proxmox.Utils.render_theme('')]];
Ext.Object.each(Proxmox.Utils.theme_map, function(key, value) {
data.push([key, Proxmox.Utils.render_theme(value)]);
});
return data;
},
bond_mode_gettext_map: {
'802.3ad': 'LACP (802.3ad)',
'lacp-balance-slb': 'LACP (balance-slb)',
@ -156,11 +122,8 @@ utilities: {
},
getNoSubKeyHtml: function(url) {
let html_url = Ext.String.format('<a target="_blank" href="{0}">www.proxmox.com</a>', url || 'https://www.proxmox.com');
return Ext.String.format(
gettext('You do not have a valid subscription for this server. Please visit {0} to get a list of available options.'),
html_url,
);
// url http://www.proxmox.com/products/proxmox-ve/subscription-service-plans
return Ext.String.format('You do not have a valid subscription for this server. Please visit <a target="_blank" href="{0}">www.proxmox.com</a> to get a list of available options.', url || 'https://www.proxmox.com');
},
format_boolean_with_default: function(value) {
@ -192,7 +155,7 @@ utilities: {
// somewhat like a human would tell durations, omit zero values and do not
// give seconds precision if we talk days already
format_duration_human: function(ut) {
let seconds = 0, minutes = 0, hours = 0, days = 0, years = 0;
let seconds = 0, minutes = 0, hours = 0, days = 0;
if (ut <= 0.1) {
return '<0.1s';
@ -208,11 +171,7 @@ utilities: {
hours = remaining % 24;
remaining = Math.trunc(remaining / 24);
if (remaining > 0) {
days = remaining % 365;
remaining = Math.trunc(remaining / 365); // yea, just lets ignore leap years...
if (remaining > 0) {
years = remaining;
}
days = remaining;
}
}
}
@ -223,14 +182,11 @@ utilities: {
return t > 0;
};
let addMinutes = !add(years, 'y');
let addSeconds = !add(days, 'd');
add(hours, 'h');
if (addMinutes) {
add(minutes, 'm');
if (addSeconds) {
add(seconds, 's');
}
add(minutes, 'm');
if (addSeconds) {
add(seconds, 's');
}
return res.join(' ');
},
@ -282,30 +238,6 @@ utilities: {
return min < width ? width : min;
},
// returns username + realm
parse_userid: function(userid) {
if (!Ext.isString(userid)) {
return [undefined, undefined];
}
let match = userid.match(/^(.+)@([^@]+)$/);
if (match !== null) {
return [match[1], match[2]];
}
return [undefined, undefined];
},
render_username: function(userid) {
let username = Proxmox.Utils.parse_userid(userid)[0] || "";
return Ext.htmlEncode(username);
},
render_realm: function(userid) {
let username = Proxmox.Utils.parse_userid(userid)[1] || "";
return Ext.htmlEncode(username);
},
getStoredAuth: function() {
let storedAuth = JSON.parse(window.localStorage.getItem('ProxmoxUser'));
return storedAuth || {};
@ -318,7 +250,7 @@ utilities: {
// that way the cookie gets deleted after the browser window is closed
if (data.ticket) {
Proxmox.CSRFPreventionToken = data.CSRFPreventionToken;
Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, data.ticket, null, '/', null, true, "lax");
Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, data.ticket, null, '/', null, true);
}
if (data.token) {
@ -343,21 +275,10 @@ utilities: {
if (Proxmox.LoggedOut) {
return;
}
// ExtJS clear is basically the same, but browser may complain if any cookie isn't "secure"
Ext.util.Cookies.set(Proxmox.Setup.auth_cookie_name, "", new Date(0), null, null, true, "lax");
Ext.util.Cookies.clear(Proxmox.Setup.auth_cookie_name);
window.localStorage.removeItem("ProxmoxUser");
},
// The End-User gets redirected back here after login on the OpenID auth. portal, and in the
// redirection URL the state and auth.code are passed as URL GET params, this helper parses those
getOpenIDRedirectionAuthorization: function() {
const auth = Ext.Object.fromQueryString(window.location.search);
if (auth.state !== undefined && auth.code !== undefined) {
return auth;
}
return undefined;
},
// comp.setLoading() is buggy in ExtJS 4.0.7, so we
// use el.mask() instead
setErrorMask: function(comp, msg) {
@ -433,15 +354,16 @@ utilities: {
if (!result.success) {
msg = gettext("Unknown error");
if (result.message) {
msg = Ext.htmlEncode(result.message);
msg = result.message;
if (result.status) {
msg += ` (${result.status})`;
msg += ' (' + result.status + ')';
}
}
if (verbose && Ext.isObject(result.errors)) {
msg += "<br>";
Ext.Object.each(result.errors, (prop, desc) => {
msg += `<br><b>${Ext.htmlEncode(prop)}</b>: ${Ext.htmlEncode(desc)}`;
Ext.Object.each(result.errors, function(prop, desc) {
msg += "<br><b>" + Ext.htmlEncode(prop) + "</b>: " +
Ext.htmlEncode(desc);
});
}
}
@ -455,20 +377,10 @@ utilities: {
waitMsg: gettext('Please wait...'),
}, reqOpts);
// default to enable if user isn't handling the failure already explicitly
let autoErrorAlert = reqOpts.autoErrorAlert ??
(typeof reqOpts.failure !== 'function' && typeof reqOpts.callback !== 'function');
if (!newopts.url.match(/^\/api2/)) {
newopts.url = '/api2/extjs' + newopts.url;
}
delete newopts.callback;
let unmask = (target) => {
if (target.waitMsgTargetCount === undefined || --target.waitMsgTargetCount <= 0) {
target.setLoading(false);
delete target.waitMsgTargetCount;
}
};
let createWrapper = function(successFn, callbackFn, failureFn) {
Ext.apply(newopts, {
@ -477,7 +389,7 @@ utilities: {
if (Proxmox.Utils.toolkit === 'touch') {
options.waitMsgTarget.setMasked(false);
} else {
unmask(options.waitMsgTarget);
options.waitMsgTarget.setLoading(false);
}
}
let result = Ext.decode(response.responseText);
@ -486,9 +398,6 @@ utilities: {
response.htmlStatus = Proxmox.Utils.extractRequestError(result, true);
Ext.callback(callbackFn, options.scope, [options, false, response]);
Ext.callback(failureFn, options.scope, [response, options]);
if (autoErrorAlert) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
}
return;
}
Ext.callback(callbackFn, options.scope, [options, true, response]);
@ -499,7 +408,7 @@ utilities: {
if (Proxmox.Utils.toolkit === 'touch') {
options.waitMsgTarget.setMasked(false);
} else {
unmask(options.waitMsgTarget);
options.waitMsgTarget.setLoading(false);
}
}
response.result = {};
@ -529,16 +438,9 @@ utilities: {
if (target) {
if (Proxmox.Utils.toolkit === 'touch') {
target.setMasked({ xtype: 'loadmask', message: newopts.waitMsg });
} else if (target.rendered) {
target.waitMsgTargetCount = (target.waitMsgTargetCount ?? 0) + 1;
target.setLoading(newopts.waitMsg);
} else {
target.waitMsgTargetCount = (target.waitMsgTargetCount ?? 0) + 1;
target.on('afterlayout', function() {
if ((target.waitMsgTargetCount ?? 0) > 0) {
target.setLoading(newopts.waitMsg);
}
}, target, { single: true });
// Note: ExtJS bug - this does not work when component is not rendered
target.setLoading(newopts.waitMsg);
}
}
Ext.Ajax.request(newopts);
@ -548,7 +450,12 @@ utilities: {
// Proxmox.Async.api2({
// ...
// }).catch(Proxmox.Utils.alertResponseFailure);
alertResponseFailure: res => Ext.Msg.alert(gettext('Error'), res.htmlStatus || res.result.message),
alertResponseFailure: (response) => {
Ext.Msg.alert(
gettext('Error'),
response.htmlStatus || response.result.message,
);
},
checked_command: function(orig_cmd) {
Proxmox.Utils.API2Request(
@ -583,7 +490,7 @@ utilities: {
},
assemble_field_data: function(values, data) {
if (!Ext.isObject(data)) {
if (!Ext.isObject(data)) {
return;
}
Ext.Object.each(data, function(name, val) {
@ -603,7 +510,7 @@ utilities: {
});
},
updateColumnWidth: function(container, thresholdWidth) {
updateColumnWidth: function(container) {
let mode = Ext.state.Manager.get('summarycolumns') || 'auto';
let factor;
if (mode !== 'auto') {
@ -612,15 +519,14 @@ utilities: {
factor = 1;
}
} else {
thresholdWidth = (thresholdWidth || 1400) + 1;
factor = Math.ceil(container.getSize().width / thresholdWidth);
factor = container.getSize().width < 1600 ? 1 : 2;
}
if (container.oldFactor === factor) {
return;
}
let items = container.query('>'); // direct children
let items = container.query('>'); // direct childs
factor = Math.min(factor, items.length);
container.oldFactor = factor;
@ -634,9 +540,6 @@ utilities: {
container.updateLayout();
},
// NOTE: depreacated, use updateColumnWidth
updateColumns: container => Proxmox.Utils.updateColumnWidth(container),
dialog_title: function(subject, create, isAdd) {
if (create) {
if (isAdd) {
@ -665,37 +568,6 @@ utilities: {
Proxmox.Utils.unknownText;
},
// Only add product-agnostic fields here!
notificationFieldName: {
'type': gettext('Notification type'),
'hostname': gettext('Hostname'),
},
formatNotificationFieldName: (value) =>
Proxmox.Utils.notificationFieldName[value] || value,
// to add or change existing for product specific ones
overrideNotificationFieldName: function(extra) {
for (const [key, value] of Object.entries(extra)) {
Proxmox.Utils.notificationFieldName[key] = value;
}
},
// Only add product-agnostic fields here!
notificationFieldValue: {
'system-mail': gettext('Forwarded mails to the local root user'),
},
formatNotificationFieldValue: (value) =>
Proxmox.Utils.notificationFieldValue[value] || value,
// to add or change existing for product specific ones
overrideNotificationFieldValue: function(extra) {
for (const [key, value] of Object.entries(extra)) {
Proxmox.Utils.notificationFieldValue[key] = value;
}
},
// NOTE: only add general, product agnostic, ones here! Else use override helper in product repos
task_desc_table: {
aptupdate: ['', gettext('Update package database')],
@ -737,70 +609,21 @@ utilities: {
},
format_size: function(size, useSI) {
let unitsSI = [gettext('B'), gettext('KB'), gettext('MB'), gettext('GB'),
gettext('TB'), gettext('PB'), gettext('EB'), gettext('ZB'), gettext('YB')];
let unitsIEC = [gettext('B'), gettext('KiB'), gettext('MiB'), gettext('GiB'),
gettext('TiB'), gettext('PiB'), gettext('EiB'), gettext('ZiB'), gettext('YiB')];
let units = ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
let order = 0;
let commaDigits = 2;
const baseValue = useSI ? 1000 : 1024;
while (size >= baseValue && order < unitsSI.length) {
while (size >= baseValue && order < units.length) {
size = size / baseValue;
order++;
}
let unit = useSI ? unitsSI[order] : unitsIEC[order];
let unit = units[order], commaDigits = 2;
if (order === 0) {
commaDigits = 0;
} else if (!useSI) {
unit += 'i';
}
return `${size.toFixed(commaDigits)} ${unit}`;
},
SizeUnits: {
'B': 1,
'KiB': 1024,
'MiB': 1024*1024,
'GiB': 1024*1024*1024,
'TiB': 1024*1024*1024*1024,
'PiB': 1024*1024*1024*1024*1024,
'KB': 1000,
'MB': 1000*1000,
'GB': 1000*1000*1000,
'TB': 1000*1000*1000*1000,
'PB': 1000*1000*1000*1000*1000,
},
parse_size_unit: function(val) {
//let m = val.match(/([.\d])+\s?([KMGTP]?)(i?)B?\s*$/i);
let m = val.match(/(\d+(?:\.\d+)?)\s?([KMGTP]?)(i?)B?\s*$/i);
let size = parseFloat(m[1]);
let scale = m[2].toUpperCase();
let binary = m[3].toLowerCase();
let unit = `${scale}${binary}B`;
let factor = Proxmox.Utils.SizeUnits[unit];
return { size, factor, unit, binary }; // for convenience return all we got
},
size_unit_to_bytes: function(val) {
let { size, factor } = Proxmox.Utils.parse_size_unit(val);
return size * factor;
},
autoscale_size_unit: function(val) {
let { size, factor, binary } = Proxmox.Utils.parse_size_unit(val);
return Proxmox.Utils.format_size(size * factor, binary !== "i");
},
size_unit_ratios: function(a, b) {
a = typeof a !== "undefined" ? a : 0;
b = typeof b !== "undefined" ? b : Infinity;
let aBytes = typeof a === "number" ? a : Proxmox.Utils.size_unit_to_bytes(a);
let bBytes = typeof b === "number" ? b : Proxmox.Utils.size_unit_to_bytes(b);
return aBytes / (bBytes || Infinity); // avoid division by zero
return `${size.toFixed(commaDigits)} ${unit}B`;
},
render_upid: function(value, metaData, record) {
@ -928,7 +751,7 @@ utilities: {
let parsed = Proxmox.Utils.parse_task_status(status);
switch (parsed) {
case 'unknown': return Proxmox.Utils.unknownText;
case 'error': return Proxmox.Utils.errorText + ': ' + Ext.htmlEncode(status);
case 'error': return Proxmox.Utils.errorText + ': ' + status;
case 'warning': return status.replace('WARNINGS', Proxmox.Utils.warningsText);
case 'ok': // fall-through
default: return status;
@ -1254,226 +1077,34 @@ utilities: {
return acme;
},
get_health_icon: function(state, circle) {
if (circle === undefined) {
circle = false;
}
if (state === undefined) {
state = 'uknown';
}
var icon = 'faded fa-question';
switch (state) {
case 'good':
icon = 'good fa-check';
break;
case 'upgrade':
icon = 'warning fa-upload';
break;
case 'old':
icon = 'warning fa-refresh';
break;
case 'warning':
icon = 'warning fa-exclamation';
break;
case 'critical':
icon = 'critical fa-times';
break;
default: break;
}
if (circle) {
icon += '-circle';
}
return icon;
},
formatNodeRepoStatus: function(status, product) {
let fmt = (txt, cls) => `<i class="fa fa-fw fa-lg fa-${cls}"></i>${txt}`;
let getUpdates = Ext.String.format(gettext('{0} updates'), product);
let noRepo = Ext.String.format(gettext('No {0} repository enabled!'), product);
if (status === 'ok') {
return fmt(getUpdates, 'check-circle good') + ' ' +
fmt(gettext('Production-ready Enterprise repository enabled'), 'check-circle good');
} else if (status === 'no-sub') {
return fmt(gettext('Production-ready Enterprise repository enabled'), 'check-circle good') + ' ' +
fmt(gettext('Enterprise repository needs valid subscription'), 'exclamation-circle warning');
} else if (status === 'non-production') {
return fmt(getUpdates, 'check-circle good') + ' ' +
fmt(gettext('Non production-ready repository enabled!'), 'exclamation-circle warning');
} else if (status === 'no-repo') {
return fmt(noRepo, 'exclamation-circle critical');
}
return Proxmox.Utils.unknownText;
},
render_u2f_error: function(error) {
var ErrorNames = {
'1': gettext('Other Error'),
'2': gettext('Bad Request'),
'3': gettext('Configuration Unsupported'),
'4': gettext('Device Ineligible'),
'5': gettext('Timeout'),
};
return "U2F Error: " + ErrorNames[error] || Proxmox.Utils.unknownText;
},
// Convert an ArrayBuffer to a base64url encoded string.
// A `null` value will be preserved for convenience.
bytes_to_base64url: function(bytes) {
if (bytes === null) {
return null;
}
return btoa(Array
.from(new Uint8Array(bytes))
.map(val => String.fromCharCode(val))
.join(''),
)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/[=]/g, '');
},
// Convert an a base64url string to an ArrayBuffer.
// A `null` value will be preserved for convenience.
base64url_to_bytes: function(b64u) {
if (b64u === null) {
return null;
}
return new Uint8Array(
atob(b64u
.replace(/-/g, '+')
.replace(/_/g, '/'),
)
.split('')
.map(val => val.charCodeAt(0)),
);
},
// 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) {
return hash;
}
string += 'prox'; // give short strings more variance
for (let i = 0; i < string.length; i++) {
hash = string.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash; // to int
}
let alpha = 0.7; // make the color a bit brighter
let bg = 255; // assume white background
return [
(hash & 255) * alpha + bg * (1 - alpha),
((hash >> 8) & 255) * alpha + bg * (1 - alpha),
((hash >> 16) & 255) * alpha + bg * (1 - alpha),
];
},
rgbToCss: function(rgb) {
return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
},
rgbToHex: function(rgb) {
let r = Math.round(rgb[0]).toString(16);
let g = Math.round(rgb[1]).toString(16);
let b = Math.round(rgb[2]).toString(16);
return `${r}${g}${b}`;
},
hexToRGB: function(hex) {
if (!hex) {
return undefined;
}
if (hex.length === 7) {
hex = hex.slice(1);
}
let r = parseInt(hex.slice(0, 2), 16);
let g = parseInt(hex.slice(2, 4), 16);
let b = parseInt(hex.slice(4, 6), 16);
return [r, g, b];
},
// optimized & simplified SAPC function
// https://github.com/Myndex/SAPC-APCA
getTextContrastClass: function(rgb) {
const blkThrs = 0.022;
const blkClmp = 1.414;
// linearize & gamma correction
let r = (rgb[0] / 255) ** 2.4;
let g = (rgb[1] / 255) ** 2.4;
let b = (rgb[2] / 255) ** 2.4;
// relative luminance sRGB
let bg = r * 0.2126729 + g * 0.7151522 + b * 0.0721750;
// black clamp
bg = bg > blkThrs ? bg : bg + (blkThrs - bg) ** blkClmp;
// SAPC with white text
let contrastLight = bg ** 0.65 - 1;
// SAPC with black text
let contrastDark = bg ** 0.56 - 0.046134502;
if (Math.abs(contrastLight) >= Math.abs(contrastDark)) {
return 'light';
} else {
return 'dark';
updateColumns: function(container) {
let mode = Ext.state.Manager.get('summarycolumns') || 'auto';
let factor;
if (mode !== 'auto') {
factor = parseInt(mode, 10);
if (Number.isNaN(factor)) {
factor = 1;
}
},
getTagElement: function(string, color_overrides) {
let rgb = color_overrides?.[string] || Proxmox.Utils.stringToRGB(string);
let style = `background-color: ${Proxmox.Utils.rgbToCss(rgb)};`;
let cls;
if (rgb.length > 3) {
style += `color: ${Proxmox.Utils.rgbToCss([rgb[3], rgb[4], rgb[5]])}`;
cls = "proxmox-tag-dark";
} else {
let txtCls = Proxmox.Utils.getTextContrastClass(rgb);
cls = `proxmox-tag-${txtCls}`;
factor = container.getSize().width < 1400 ? 1 : 2;
}
return `<span class="${cls}" style="${style}">${string}</span>`;
},
// Setting filename here when downloading from a remote url sometimes fails in chromium browsers
// because of a bug when using attribute download in conjunction with a self signed certificate.
// For more info see https://bugs.chromium.org/p/chromium/issues/detail?id=993362
downloadAsFile: function(source, fileName) {
let hiddenElement = document.createElement('a');
hiddenElement.href = source;
hiddenElement.target = '_blank';
if (fileName) {
hiddenElement.download = fileName;
if (container.oldFactor === factor) {
return;
}
hiddenElement.click();
let items = container.query('>'); // direct childs
factor = Math.min(factor, items.length);
container.oldFactor = factor;
items.forEach((item) => {
item.columnWidth = 1 / factor;
});
// we have to update the layout twice, since the first layout change
// can trigger the scrollbar which reduces the amount of space left
container.updateLayout();
container.updateLayout();
},
},
@ -1516,35 +1147,12 @@ utilities: {
let DnsName_REGEXP = "(?:(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9\\-]*[a-zA-Z0-9])?)\\.)*(?:[A-Za-z0-9](?:[A-Za-z0-9\\-]*[A-Za-z0-9])?))";
me.DnsName_match = new RegExp("^" + DnsName_REGEXP + "$");
me.DnsName_or_Wildcard_match = new RegExp("^(?:\\*\\.)?" + DnsName_REGEXP + "$");
me.CpuSet_match = /^[0-9]+(?:-[0-9]+)?(?:,[0-9]+(?:-[0-9]+)?)*$/;
me.HostPort_match = new RegExp("^(" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")(?::(\\d+))?$");
me.HostPortBrackets_match = new RegExp("^\\[(" + IPV6_REGEXP + "|" + IPV4_REGEXP + "|" + DnsName_REGEXP + ")\\](?::(\\d+))?$");
me.IP6_dotnotation_match = new RegExp("^(" + IPV6_REGEXP + ")(?:\\.(\\d+))?$");
me.Vlan_match = /^vlan(\d+)/;
me.VlanInterface_match = /(\w+)\.(\d+)/;
// 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._\\-]*)$/;
},
});
@ -1552,7 +1160,7 @@ Ext.define('Proxmox.Async', {
singleton: true,
// Returns a Promise resolving to the result of an `API2Request` or rejecting to the error
// response on failure
// repsonse on failure
api2: function(reqOpts) {
return new Promise((resolve, reject) => {
delete reqOpts.callback; // not allowed in this api
@ -1567,19 +1175,3 @@ Ext.define('Proxmox.Async', {
return new Promise((resolve, _reject) => setTimeout(resolve, millis));
},
});
Ext.override(Ext.data.Store, {
// If the store's proxy is changed while it is waiting for an AJAX
// response, `onProxyLoad` will still be called for the outdated response.
// To avoid displaying inconsistent information, only process responses
// belonging to the current proxy. However, do not apply this workaround
// to the mobile UI, as Sencha Touch has an incompatible internal API.
onProxyLoad: function(operation) {
let me = this;
if (Proxmox.Utils.toolkit === 'touch' || operation.getProxy() === me.getProxy()) {
me.callParent(arguments);
} else {
console.log(`ignored outdated response: ${operation.getRequest().getUrl()}`);
}
},
});

View File

@ -1,552 +0,0 @@
/*global apiSchema*/
Ext.onReady(function() {
Ext.define('pmx-param-schema', {
extend: 'Ext.data.Model',
fields: [
'name', 'type', 'typetext', 'description', 'verbose_description',
'enum', 'minimum', 'maximum', 'minLength', 'maxLength',
'pattern', 'title', 'requires', 'format', 'default',
'disallow', 'extends', 'links', 'instance-types',
{
name: 'optional',
type: 'boolean',
},
],
});
let store = Ext.define('pmx-updated-treestore', {
extend: 'Ext.data.TreeStore',
model: Ext.define('pmx-api-doc', {
extend: 'Ext.data.Model',
fields: [
'path', 'info', 'text',
],
}),
proxy: {
type: 'memory',
data: apiSchema,
},
sorters: [{
property: 'leaf',
direction: 'ASC',
}, {
property: 'text',
direction: 'ASC',
}],
filterer: 'bottomup',
doFilter: function(node) {
this.filterNodes(node, this.getFilters().getFilterFn(), true);
},
filterNodes: function(node, filterFn, parentVisible) {
let me = this;
let match = filterFn(node) && (parentVisible || (node.isRoot() && !me.getRootVisible()));
if (node.childNodes && node.childNodes.length) {
let bottomUpFiltering = me.filterer === 'bottomup';
let childMatch;
for (const child of node.childNodes) {
childMatch = me.filterNodes(child, filterFn, match || bottomUpFiltering) || childMatch;
}
if (bottomUpFiltering) {
match = childMatch || match;
}
}
node.set("visible", match, me._silentOptions);
return match;
},
}).create();
let render_description = function(value, metaData, record) {
let pdef = record.data;
value = pdef.verbose_description || value;
// TODO: try to render asciidoc correctly
metaData.style = 'white-space:pre-wrap;';
return Ext.htmlEncode(value);
};
let render_type = function(value, metaData, record) {
let pdef = record.data;
return pdef.enum ? 'enum' : pdef.type || 'string';
};
const renderFormatString = function(obj) {
if (!Ext.isObject(obj)) {
return obj;
}
const mandatory = [];
const optional = [];
Object.entries(obj).forEach(function([name, param]) {
let list = param.optional ? optional : mandatory;
let str = param.default_key ? `[${name}=]` : `${name}=`;
if (param.alias) {
return;
} else if (param.enum) {
str += `(${param.enum?.join(' | ')})`;
} else {
str += `<${param.format_description || param.pattern || param.type}>`;
}
list.push(str);
});
return mandatory.join(", ") + ' ' + optional.map(each => `[,${each}]`).join(' ');
};
let render_simple_format = function(pdef, type_fallback) {
if (pdef.typetext) {
return pdef.typetext;
}
if (pdef.enum) {
return pdef.enum.join(' | ');
}
if (pdef.format) {
return renderFormatString(pdef.format);
}
if (pdef.pattern) {
return pdef.pattern;
}
if (pdef.type === 'boolean') {
return `<true|false>`;
}
if (type_fallback && pdef.type) {
return `<${pdef.type}>`;
}
if (pdef.minimum || pdef.maximum) {
return `${pdef.minimum || 'N'} - ${pdef.maximum || 'N'}`;
}
return '';
};
let render_format = function(value, metaData, record) {
let pdef = record.data;
metaData.style = 'white-space:normal;';
if (pdef.type === 'array' && pdef.items) {
let format = render_simple_format(pdef.items, true);
return `[${Ext.htmlEncode(format)}, ...]`;
}
return Ext.htmlEncode(render_simple_format(pdef));
};
let real_path = function(path) {
if (!path.match(/^[/]/)) {
path = `/${path}`;
}
return path.replace(/^.*\/_upgrade_(\/)?/, "/");
};
let permission_text = function(permission) {
let permhtml = "";
if (permission.user) {
if (!permission.description) {
if (permission.user === 'world') {
permhtml += "Accessible without any authentication.";
} else if (permission.user === 'all') {
permhtml += "Accessible by all authenticated users.";
} else {
permhtml += `Only accessible by user "${permission.user}"`;
}
}
} else if (permission.check) {
permhtml += `<pre>Check: ${Ext.htmlEncode(JSON.stringify(permission.check))}</pre>`;
} else if (permission.userParam) {
permhtml += `<div>Check if user matches parameter '${permission.userParam}'`;
} else if (permission.or) {
permhtml += "<div>Or<div style='padding-left: 10px;'>";
permhtml += permission.or.map(v => permission_text(v)).join('');
permhtml += "</div></div>";
} else if (permission.and) {
permhtml += "<div>And<div style='padding-left: 10px;'>";
permhtml += permission.and.map(v => permission_text(v)).join('');
permhtml += "</div></div>";
} else {
permhtml += "Unknown syntax!";
}
return permhtml;
};
let render_docu = function(data) {
let md = data.info;
let items = [];
Ext.Array.each(['GET', 'POST', 'PUT', 'DELETE'], function(method) {
let info = md[method];
if (info) {
let endpoint = real_path(data.path);
let usage = `<table><tr><td>HTTP:&nbsp;&nbsp;&nbsp;</td><td>`;
usage += `${method} /api2/json${endpoint}</td></tr>`;
if (typeof cliUsageRenderer === 'function') {
usage += cliUsageRenderer(method, endpoint); // eslint-disable-line no-undef
}
let sections = [
{
title: 'Description',
html: Ext.htmlEncode(info.description),
bodyPadding: 10,
},
{
title: 'Usage',
html: usage,
bodyPadding: 10,
},
];
if (info.parameters && info.parameters.properties) {
let pstore = Ext.create('Ext.data.Store', {
model: 'pmx-param-schema',
proxy: {
type: 'memory',
},
groupField: 'optional',
sorters: [
{
property: 'instance-types',
direction: 'ASC',
},
{
property: 'name',
direction: 'ASC',
},
],
});
let has_type_properties = false;
Ext.Object.each(info.parameters.properties, function(name, pdef) {
if (pdef.oneOf) {
pdef.oneOf.forEach((alternative) => {
alternative.name = name;
pstore.add(alternative);
has_type_properties = true;
});
} else if (pdef['instance-types']) {
pdef['instance-types'].forEach((type) => {
let typePdef = Ext.apply({}, pdef);
typePdef.name = name;
typePdef['instance-types'] = [type];
pstore.add(typePdef);
has_type_properties = true;
});
} else {
pdef.name = name;
pstore.add(pdef);
}
});
pstore.sort();
let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
enableGroupingMenu: false,
groupHeaderTpl: '<tpl if="groupValue">Optional</tpl><tpl if="!groupValue">Required</tpl>',
});
sections.push({
xtype: 'gridpanel',
title: 'Parameters',
features: [groupingFeature],
store: pstore,
viewConfig: {
trackOver: false,
stripeRows: true,
enableTextSelection: true,
},
columns: [
{
header: 'Name',
dataIndex: 'name',
flex: 1,
},
{
header: 'Type',
dataIndex: 'type',
renderer: render_type,
flex: 1,
},
{
header: 'For Types',
dataIndex: 'instance-types',
hidden: !has_type_properties,
flex: 1,
},
{
header: 'Default',
dataIndex: 'default',
flex: 1,
},
{
header: 'Format',
dataIndex: 'type',
renderer: render_format,
flex: 2,
},
{
header: 'Description',
dataIndex: 'description',
renderer: render_description,
flex: 6,
},
],
});
}
if (info.returns) {
let retinf = info.returns;
let rtype = retinf.type;
if (!rtype && retinf.items) {rtype = 'array';}
if (!rtype) {rtype = 'object';}
let rpstore = Ext.create('Ext.data.Store', {
model: 'pmx-param-schema',
proxy: {
type: 'memory',
},
groupField: 'optional',
sorters: [
{
property: 'name',
direction: 'ASC',
},
],
});
let properties;
if (rtype === 'array' && retinf.items.properties) {
properties = retinf.items.properties;
}
if (rtype === 'object' && retinf.properties) {
properties = retinf.properties;
}
Ext.Object.each(properties, function(name, pdef) {
pdef.name = name;
rpstore.add(pdef);
});
rpstore.sort();
let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
enableGroupingMenu: false,
groupHeaderTpl: '<tpl if="groupValue">Optional</tpl><tpl if="!groupValue">Obligatory</tpl>',
});
let returnhtml;
if (retinf.items) {
returnhtml = '<pre>items: ' + Ext.htmlEncode(JSON.stringify(retinf.items, null, 4)) + '</pre>';
}
if (retinf.properties) {
returnhtml = returnhtml || '';
returnhtml += '<pre>properties:' + Ext.htmlEncode(JSON.stringify(retinf.properties, null, 4)) + '</pre>';
}
let rawSection = Ext.create('Ext.panel.Panel', {
bodyPadding: '0px 10px 10px 10px',
html: returnhtml,
hidden: true,
});
sections.push({
xtype: 'gridpanel',
title: 'Returns: ' + rtype,
features: [groupingFeature],
store: rpstore,
viewConfig: {
trackOver: false,
stripeRows: true,
enableTextSelection: true,
},
columns: [
{
header: 'Name',
dataIndex: 'name',
flex: 1,
},
{
header: 'Type',
dataIndex: 'type',
renderer: render_type,
flex: 1,
},
{
header: 'Default',
dataIndex: 'default',
flex: 1,
},
{
header: 'Format',
dataIndex: 'type',
renderer: render_format,
flex: 2,
},
{
header: 'Description',
dataIndex: 'description',
renderer: render_description,
flex: 6,
},
],
bbar: [
{
xtype: 'button',
text: 'Show RAW',
handler: function(btn) {
rawSection.setVisible(!rawSection.isVisible());
btn.setText(rawSection.isVisible() ? 'Hide RAW' : 'Show RAW');
},
},
],
});
sections.push(rawSection);
}
if (!data.path.match(/\/_upgrade_/)) {
let permhtml = '';
if (!info.permissions) {
permhtml = "Root only.";
} else {
if (info.permissions.description) {
permhtml += "<div style='white-space:pre-wrap;padding-bottom:10px;'>" +
Ext.htmlEncode(info.permissions.description) + "</div>";
}
permhtml += permission_text(info.permissions);
}
if (info.allowtoken !== undefined && !info.allowtoken) {
permhtml += "<br />This API endpoint is not available for API tokens.";
}
sections.push({
title: 'Required permissions',
bodyPadding: 10,
html: permhtml,
});
}
items.push({
title: method,
autoScroll: true,
defaults: {
border: false,
},
items: sections,
});
}
});
let ct = Ext.getCmp('docview');
ct.setTitle("Path: " + real_path(data.path));
ct.removeAll(true);
ct.add(items);
ct.setActiveTab(0);
};
Ext.define('Ext.form.SearchField', {
extend: 'Ext.form.field.Text',
alias: 'widget.searchfield',
emptyText: 'Search...',
flex: 1,
inputType: 'search',
listeners: {
'change': function() {
let value = this.getValue();
if (!Ext.isEmpty(value)) {
store.filter({
property: 'path',
value: value,
anyMatch: true,
});
} else {
store.clearFilter();
}
},
},
});
let treePanel = Ext.create('Ext.tree.Panel', {
title: 'Resource Tree',
tbar: [
{
xtype: 'searchfield',
},
],
tools: [
{
type: 'expand',
tooltip: 'Expand all',
tooltipType: 'title',
callback: tree => tree.expandAll(),
},
{
type: 'collapse',
tooltip: 'Collapse all',
tooltipType: 'title',
callback: tree => tree.collapseAll(),
},
],
store: store,
width: 200,
region: 'west',
split: true,
margins: '5 0 5 5',
rootVisible: false,
listeners: {
selectionchange: function(v, selections) {
if (!selections[0]) {return;}
let rec = selections[0];
render_docu(rec.data);
location.hash = '#' + rec.data.path;
},
},
});
Ext.create('Ext.container.Viewport', {
layout: 'border',
renderTo: Ext.getBody(),
items: [
treePanel,
{
xtype: 'tabpanel',
title: 'Documentation',
id: 'docview',
region: 'center',
margins: '5 5 5 0',
layout: 'fit',
items: [],
},
],
});
let deepLink = function() {
let path = window.location.hash.substring(1).replace(/\/\s*$/, '');
let endpoint = store.findNode('path', path);
if (endpoint) {
treePanel.getSelectionModel().select(endpoint);
treePanel.expandPath(endpoint.getPath());
render_docu(endpoint.data);
}
};
window.onhashchange = deepLink;
deepLink();
});

View File

@ -1,22 +0,0 @@
Ext.define('Proxmox.button.AltText', {
extend: 'Proxmox.button.Button',
xtype: 'proxmoxAltTextButton',
defaultText: "",
altText: "",
listeners: {
// HACK: calculate the max button width on first render to avoid toolbar glitches
render: function(button) {
let me = this;
button.setText(me.altText);
let altWidth = button.getSize().width;
button.setText(me.defaultText);
let defaultWidth = button.getSize().width;
button.setWidth(defaultWidth > altWidth ? defaultWidth : altWidth);
},
},
});

View File

@ -110,7 +110,6 @@ Ext.define('Proxmox.button.StdRemoveButton', {
config: {
baseurl: undefined,
customConfirmationMessage: undefined,
},
getUrl: function(rec) {
@ -134,14 +133,7 @@ Ext.define('Proxmox.button.StdRemoveButton', {
let me = this;
let name = me.getRecordName(rec);
let text;
if (me.customConfirmationMessage) {
text = me.customConfirmationMessage;
} else {
text = gettext('Are you sure you want to remove entry {0}');
}
return Ext.String.format(text, Ext.htmlEncode(`'${name}'`));
return Ext.String.format(gettext('Are you sure you want to remove entry {0}'), `'${name}'`);
},
handler: function(btn, event, rec) {
@ -160,7 +152,9 @@ Ext.define('Proxmox.button.StdRemoveButton', {
callback: function(options, success, response) {
Ext.callback(me.callback, me.scope, [options, success, response], 0, me);
},
failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
});
},
initComponent: function() {

View File

@ -5,9 +5,9 @@ CSS=ext6-pmx.css
all:
.PHONY: install
install: $(CSS)
install -d $(WWWCSSDIR)
for i in $(CSS); do install -m 0644 $$i $(WWWCSSDIR)/$$i; done
install: ${CSS}
install -d ${WWWCSSDIR}
for i in ${CSS}; do install -m 0755 $$i ${WWWCSSDIR}/$$i; done
.PHONY: clean
clean:

View File

@ -6,82 +6,9 @@
background-color: LightYellow;
}
.proxmox-tags-full .proxmox-tag-light,
.proxmox-tags-full .proxmox-tag-dark {
border-radius: 3px;
padding: 1px 6px;
margin: 0px 1px;
display: inline-block;
}
.proxmox-tags-full .x-grid-cell-inner-treecolumn .proxmox-tag-light,
.proxmox-tags-full .x-grid-cell-inner-treecolumn .proxmox-tag-dark,
.x-grid-cell-inner-treecolumn .proxmox-tags-full .proxmox-tag-light,
.x-grid-cell-inner-treecolumn .proxmox-tags-full .proxmox-tag-dark {
display: inherit;
}
.x-boundlist-item > .proxmox-tag-light,
.x-boundlist-item > .proxmox-tag-dark {
line-height: 15px;
}
.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;
border-radius: 6px;
height: 12px;
width: 12px;
display: inline-block;
color: transparent !important;
overflow: hidden;
}
.proxmox-tags-none :not(span.proxmox-tags-full) > .proxmox-tag-light,
.proxmox-tags-none :not(span.proxmox-tags-full) > .proxmox-tag-dark,
.proxmox-tags-none > .proxmox-tag-light,
.proxmox-tags-none > .proxmox-tag-dark {
display: none;
}
.proxmox-tags-dense :not(span.proxmox-tags-full) > .proxmox-tag-light,
.proxmox-tags-dense :not(span.proxmox-tags-full) > .proxmox-tag-dark,
.proxmox-tags-dense > .proxmox-tag-light,
.proxmox-tags-dense > .proxmox-tag-dark {
width: 6px;
margin-right: 1px;
display: inline-block;
color: transparent !important;
overflow: hidden;
vertical-align: bottom;
}
.proxmox-tags-full .proxmox-tag-light {
color: #fff;
background-color: #383838;
}
.proxmox-tags-full .proxmox-tag-dark {
color: #000;
background-color: #f0f0f0;
}
.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;*/
color: #666665;
}
.proxmox-invalid-row {
background-color: #f3d6d7;
@ -91,10 +18,6 @@
background-color: #f5e5d8;
}
.proxmox-good-row {
background-color: #21BF4B;
}
/* some icons have to be color manually */
.black {
color: #000;
@ -120,18 +43,12 @@
color: #FF6C59;
}
.info-blue {
color: #3892d4;
}
.eol-notice a:visited {
color: inherit;
}
.eol-notice > i.fa {
.pwt-eol-icon {
position: relative;
float: left;
margin-right: 5px;
font-size: 1.3em;
color: #FF6C59;
}
/* reduce chart legend space usage to something more sane */
@ -187,177 +104,19 @@ div.right-aligned {
}
.pmx-itype-icon-memory,
.pmx-itype-icon-processor,
.pmx-itype-icon /* NOTE: use this one instead of adding new specific ones! */
.pmx-itype-icon-processor
{
background-repeat: no-repeat;
background-position:3px center;
padding-left: 20px;
background-size: 16px 16px; /* Chrom* needs both as else it gets cut-off due do non 1:1 ratio */
}
.pmx-itype-icon-memory {
background-image:url(../images/icon-ram.svg);
.pmx-itype-icon-memory
{
background-image:url(../images/icon-ram.png);
}
.pmx-itype-icon-processor {
background-image:url(../images/icon-cpu.svg);
.pmx-itype-icon-processor
{
background-image:url(../images/icon-cpu.png);
}
.pmx-itype-icon-debian-swirl {
padding-left: 22px;
background-size: 16px 16px; /* Chrom* needs both as else it gets cut-off due do non 1:1 ratio */
background-image:url(../images/debian-swirl-openlogo.svg);
}
.pmx-itype-icon-proxmox-x {
padding-left: 22px;
background-size: 16px 16px; /* Not really required here, as here WxH is 1:1 but cannot hurt */
background-image:url(../images/proxmox-symbol-x.svg);
}
.pmx-itype-icon-openid-logo {
padding-left: 22px;
background-size: 16px 16px;
background-image:url(../images/openid-icon-100x100.png);
}
/* change font for config panel back to fontawesome */
.x-treelist-item-expanded > * > * > .x-treelist-item-expander::after,
.x-treelist-item-expander::after {
font: 16px/1 FontAwesome;
}
.x-treelist-pve-nav {
background-color: #f5f5f5;
}
/* fix padding for legend in header */
.x-legend-inner {
padding: 0;
}
.proxmox-apt-repos .x-grid-group-hd {
color: #000000;
background-color: #f5f5f5;
}
.proxmox-apt-repos .x-grid-group-title {
color: #333;
}
/* some general helper classes */
.centered-flex-column {
display: flex;
justify-content: center;
flex-direction: column;
width: 100%;
height: 100%;
}
/* Fix icon/text baseline */
.x-tab-default {
display: inline-flex;
align-items: center;
justify-content: center;
}
.x-tab-default > span {
text-align: center;
vertical-align: middle;
}
.x-tab-button {
line-height: unset;
}
.x-tab-inner {
display: unset;
vertical-align: text-top;
}
.x-tab-wrap {
display: unset;
}
.x-tab-default-top {
padding: 2px 6px 2px 6px;
}
/* rules for the markdown content, prefix with the .pmx-md class */
.pmx-md {
font-size: 1.0em;
line-height: 1.25em;
}
.pmx-md p {
margin-top: 0.75em;
margin-bottom: 0.75em;
}
.pmx-md :is(h1, h2, h3, h4, h5, h6) {
margin-top: 0.9em;
margin-bottom: 0.75em;
}
.pmx-md h1 { font-size: 175%; }
.pmx-md h2 { font-size: 150%; }
.pmx-md h3 { font-size: 125%; }
.pmx-md h4 { font-size: 110%; }
.pmx-md h5 { font-size: 100%; }
.pmx-md h6 { font-size: 100%; }
.pmx-md code {
white-space: pre;
background-color: #f5f5f5;
padding: 1px;
}
.pmx-md pre code {
display: inline-block;
padding: 5px;
border-left: 3px solid #e0e0e0;
}
.pmx-md strong {
font-weight: bold;
}
.pmx-md blockquote {
border-left: 1px solid #666666;
padding-left: 4px;
margin: 10px 2ch;
}
/* markdown tables */
.pmx-md table {
border-spacing: 0;
border-collapse: collapse;
}
.pmx-md td, .pmx-md th {
padding: 5px;
}
.pmx-md td[align="center"] {
text-align: center;
}
.pmx-md td[align="right"] {
text-align: right;
}
.pmx-md tbody td {
border-bottom: 1px solid #e0e0e0;
}
.pmx-md tbody tr:nth-of-type(even) {
background-color: #f5f5f5;
}
.pmx-md tbody tr:last-of-type td {
border-bottom: 1px solid #666666;
}
.pmx-md tbody tr:hover td {
background-color: #e0e0e0;
}
/* markdown tables end */
/* markdown content end */
/* action column fix start */
.x-action-col-icon {
margin: 0 1px;
font-size: 14px;
}
.x-action-col-icon:before, .x-action-col-icon:after {
font-size: 14px;
}
.x-action-col-icon:hover:before, .x-action-col-icon:hover:after {
text-shadow: 1px 1px 1px #AAA;
font-weight: 800;
}
.x-action-col-icon:before {
color: #555;
}
/* action column fix end */

View File

@ -28,7 +28,7 @@ Ext.define('Proxmox.data.DiffStore', {
// config is passed instead of an existing rstore instance
autoDestroyRstore: false,
doDestroy: function() {
onDestroy: function() {
let me = this;
if (me.autoDestroyRstore) {
if (Ext.isFunction(me.rstore.destroy)) {

View File

@ -17,7 +17,6 @@ Ext.define('Proxmox.RestProxy', {
constructor: function(config) {
Ext.applyIf(config, {
reader: {
responseType: undefined,
type: 'json',
rootProperty: config.root || 'data',
},

View File

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

View File

@ -9,7 +9,7 @@
* example2: [ {data1: "xyz", data2: "abc"} ]
* returns [{key: "data1", value: "xyz"}, {key: "data2", value: "abc"}]
*
* If you set 'readArray', the reader expects the object as array:
* If you set 'readArray', the reader expexts the object as array:
*
* example3: [ { key: "data1", value: "xyz", p2: "cde" }, { key: "data2", value: "abc", p2: "efg" }]
* returns [{key: "data1", value: "xyz", p2: "cde}, {key: "data2", value: "abc", p2: "efg"}]
@ -32,7 +32,6 @@ Ext.define('Proxmox.data.reader.JsonObject', {
alias: 'reader.jsonobject',
readArray: false,
responseType: undefined,
rows: undefined,

View File

@ -1,8 +1,5 @@
PACKAGE ?= $(or $(DEB_SOURCE), proxmox-widget-toolkit)
DESTDIR=
DOCDIR=$(DESTDIR)/usr/share/doc/$(PACKAGE)
WWWBASEDIR=$(DESTDIR)/usr/share/javascript/$(PACKAGE)
WWWCSSDIR=$(WWWBASEDIR)/css
WWWIMAGESDIR=$(WWWBASEDIR)/images
WWWTHEMEDIR=$(WWWBASEDIR)/themes
DOCDIR=${DESTDIR}/usr/share/doc/${PACKAGE}
WWWBASEDIR=${DESTDIR}/usr/share/javascript/${PACKAGE}
WWWCSSDIR=${WWWBASEDIR}/css
WWWIMAGESDIR=${WWWBASEDIR}/images

View File

@ -1,151 +0,0 @@
Ext.define('Proxmox.form.SizeField', {
extend: 'Ext.form.FieldContainer',
alias: 'widget.pmxSizeField',
mixins: ['Proxmox.Mixin.CBind'],
viewModel: {
data: {
unit: 'MiB',
unitPostfix: '',
},
formulas: {
unitlabel: (get) => get('unit') + get('unitPostfix'),
},
},
emptyText: '',
layout: 'hbox',
defaults: {
hideLabel: true,
},
// display unit (TODO: make (optionally) selectable)
unit: 'MiB',
unitPostfix: '',
// use this if the backend saves values in another unit than bytes, e.g.,
// for KiB set it to 'KiB'
backendUnit: undefined,
// submit a canonical size unit, e.g., 20.5 MiB
submitAutoScaledSizeUnit: false,
// allow setting 0 and using it as a submit value
allowZero: false,
emptyValue: null,
items: [
{
xtype: 'numberfield',
cbind: {
name: '{name}',
emptyText: '{emptyText}',
allowZero: '{allowZero}',
emptyValue: '{emptyValue}',
},
minValue: 0,
step: 1,
submitLocaleSeparator: false,
fieldStyle: 'text-align: right',
flex: 1,
enableKeyEvents: true,
setValue: function(v) {
if (!this._transformed) {
let fieldContainer = this.up('fieldcontainer');
let vm = fieldContainer.getViewModel();
let unit = vm.get('unit');
if (typeof v === "string") {
v = Proxmox.Utils.size_unit_to_bytes(v);
}
v /= Proxmox.Utils.SizeUnits[unit];
v *= fieldContainer.backendFactor;
this._transformed = true;
}
if (Number(v) === 0 && !this.allowZero) {
v = undefined;
}
return Ext.form.field.Text.prototype.setValue.call(this, v);
},
getSubmitValue: function() {
let v = this.processRawValue(this.getRawValue());
v = v.replace(this.decimalSeparator, '.');
if (v === undefined || v === '') {
return this.emptyValue;
}
if (Number(v) === 0) {
return this.allowZero ? 0 : null;
}
let fieldContainer = this.up('fieldcontainer');
let vm = fieldContainer.getViewModel();
let unit = vm.get('unit');
v = parseFloat(v) * Proxmox.Utils.SizeUnits[unit];
if (fieldContainer.submitAutoScaledSizeUnit) {
return Proxmox.Utils.format_size(v, !unit.endsWith('iB'));
} else {
return String(Math.floor(v / fieldContainer.backendFactor));
}
},
listeners: {
// our setValue gets only called if we have a value, avoid
// transformation of the first user-entered value
keydown: function() { this._transformed = true; },
},
},
{
xtype: 'displayfield',
name: 'unit',
submitValue: false,
padding: '0 0 0 10',
bind: {
value: '{unitlabel}',
},
listeners: {
change: (f, v) => {
f.originalValue = v;
},
},
width: 40,
},
],
initComponent: function() {
let me = this;
me.unit = me.unit || 'MiB';
if (!(me.unit in Proxmox.Utils.SizeUnits)) {
throw "unknown unit: " + me.unit;
}
me.backendFactor = 1;
if (me.backendUnit !== undefined) {
if (!(me.unit in Proxmox.Utils.SizeUnits)) {
throw "unknown backend unit: " + me.backendUnit;
}
me.backendFactor = Proxmox.Utils.SizeUnits[me.backendUnit];
}
me.callParent(arguments);
me.getViewModel().set('unit', me.unit);
me.getViewModel().set('unitPostfix', me.unitPostfix);
},
});
Ext.define('Proxmox.form.BandwidthField', {
extend: 'Proxmox.form.SizeField',
alias: 'widget.pmxBandwidthField',
unitPostfix: '/s',
});

View File

@ -6,7 +6,6 @@ Ext.define('Proxmox.form.Checkbox', {
defaultValue: undefined,
deleteDefaultValue: false,
deleteEmpty: false,
clearOnDisable: false,
},
inputValue: '1',
@ -32,19 +31,6 @@ Ext.define('Proxmox.form.Checkbox', {
return data;
},
setDisabled: function(disabled) {
let me = this;
// only clear on actual transition
let toClearValue = me.clearOnDisable && !me.disabled && disabled;
me.callParent(arguments);
if (toClearValue) {
me.setValue(false); // TODO: could support other "reset value" or use originalValue?
}
},
// also accept integer 1 as true
setRawValue: function(value) {
let me = this;

View File

@ -31,10 +31,6 @@ Ext.define('Proxmox.form.ComboGrid', {
skipEmptyText: false,
notFoundIsValid: false,
deleteEmpty: false,
errorHeight: 100,
// NOTE: the trigger will always be shown if allowBlank is true, setting showClearTrigger
// to false cannot change that
showClearTrigger: false,
},
// needed to trigger onKeyUp etc.
@ -57,7 +53,7 @@ Ext.define('Proxmox.form.ComboGrid', {
setValue: function(value) {
let me = this;
let empty = Ext.isArray(value) ? !value.length : !value;
me.triggers.clear.setVisible(!empty && (me.allowBlank || me.showClearTrigger));
me.triggers.clear.setVisible(!empty && me.allowBlank);
return me.callParent([value]);
},
@ -293,7 +289,7 @@ Ext.define('Proxmox.form.ComboGrid', {
if (!me.multiSelect) {
picker.on('itemclick', function(sm, record) {
if (picker.getSelection()[0] === record) {
me.collapse();
picker.hide();
}
});
}
@ -304,13 +300,12 @@ Ext.define('Proxmox.form.ComboGrid', {
//
// we save the minheight to reset it after the load
picker.on('show', function() {
me.store.fireEvent('refresh');
if (me.enableLoadMask) {
me.savedMinHeight = me.savedMinHeight ?? picker.getMinHeight();
picker.setMinHeight(me.errorHeight);
me.savedMinHeight = picker.getMinHeight();
picker.setMinHeight(100);
}
if (me.loadError) {
Proxmox.Utils.setErrorMask(picker.getView(), me.loadError);
Proxmox.Utils.setErrorMask(picker, me.loadError);
delete me.loadError;
picker.updateLayout();
}
@ -322,14 +317,15 @@ Ext.define('Proxmox.form.ComboGrid', {
},
clearLocalFilter: function() {
let me = this;
let me = this,
filter = me.queryFilter;
if (me.queryFilter) {
me.changingFilters = true; // FIXME: unused?
me.store.removeFilter(me.queryFilter, true);
me.queryFilter = null;
me.changingFilters = false;
}
if (filter) {
me.queryFilter = null;
me.changingFilters = true;
me.store.removeFilter(filter, true);
me.changingFilters = false;
}
},
isValueInStore: function(value) {
@ -403,7 +399,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 });
@ -411,7 +407,7 @@ Ext.define('Proxmox.form.ComboGrid', {
// Create the picker at an early stage, so it is available to store the previous selection
if (!me.picker) {
me.getPicker();
me.createPicker();
}
me.mon(me.store, 'beforeload', function() {
@ -432,7 +428,7 @@ Ext.define('Proxmox.form.ComboGrid', {
// if the picker exists, we reset its minHeight to the previous saved one or 0
if (me.picker) {
me.picker.setMinHeight(me.savedMinHeight || 0);
Proxmox.Utils.setErrorMask(me.picker.getView());
Proxmox.Utils.setErrorMask(me.picker);
delete me.savedMinHeight;
// we have to update the layout, otherwise the height gets not recalculated
me.picker.updateLayout();
@ -467,10 +463,7 @@ Ext.define('Proxmox.form.ComboGrid', {
} else {
let msg = Proxmox.Utils.getResponseErrorMessage(o.getError());
if (me.picker) {
me.savedMinHeight = me.savedMinHeight ?? me.picker.getMinHeight();
me.picker.setMinHeight(me.errorHeight);
Proxmox.Utils.setErrorMask(me.picker.getView(), msg);
me.picker.updateLayout();
Proxmox.Utils.setErrorMask(me.picker, msg);
}
me.loadError = msg;
}

View File

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

View File

@ -8,10 +8,7 @@ Ext.define('Proxmox.form.DiskSelector', {
// journal_disk: all disks with gpt
diskType: undefined,
// use include-partitions=1 as a parameter
includePartitions: false,
// the property the backend wants for the type ('type' by default)
// the property the backend wnats for the type ('type' by default)
typeProperty: 'type',
valueField: 'devpath',
@ -56,10 +53,6 @@ Ext.define('Proxmox.form.DiskSelector', {
extraParams[me.typeProperty] = me.diskType;
}
if (me.includePartitions) {
extraParams['include-partitions'] = 1;
}
var store = Ext.create('Ext.data.Store', {
filterOnLoad: true,
model: 'pmx-disk-list',

View File

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

View File

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

View File

@ -11,19 +11,20 @@ Ext.define('Proxmox.form.field.Integer', {
step: 1,
getSubmitData: function() {
let me = this;
let data = null;
if (!me.disabled && me.submitValue && !me.isFileUpload()) {
let val = me.getSubmitValue();
if (val !== undefined && val !== null && val !== '') {
let me = this,
data = null,
val;
if (!me.disabled && me.submitValue && !me.isFileUpload()) {
val = me.getSubmitValue();
if (val !== undefined && val !== null && val !== '') {
data = {};
data[me.getName()] = val;
} else if (me.getDeleteEmpty()) {
data = {};
data[me.getName()] = val;
} else if (me.getDeleteEmpty()) {
data = {};
data.delete = me.getName();
data.delete = me.getName();
}
}
return data;
}
return data;
},
});

View File

@ -18,7 +18,7 @@ Ext.define('Proxmox.form.KVComboBox', {
valueField: 'key',
queryMode: 'local',
// override framework function to implement deleteEmpty behaviour
// overide framework function to implement deleteEmpty behaviour
getSubmitData: function() {
let me = this,
data = null,

View File

@ -3,9 +3,4 @@ Ext.define('Proxmox.form.LanguageSelector', {
xtype: 'proxmoxLanguageSelector',
comboItems: Proxmox.Utils.language_array(),
matchFieldWidth: false,
listConfig: {
width: 300,
},
});

View File

@ -24,9 +24,6 @@ Ext.define('Proxmox.form.MultiDiskSelector', {
// the type of disks to show
diskType: 'unused',
// add include-partitions=1 as a request parameter
includePartitions: false,
disks: [],
allowBlank: false,
@ -39,8 +36,6 @@ Ext.define('Proxmox.form.MultiDiskSelector', {
setValue: function(value) {
let me = this;
value ??= [];
if (!Ext.isArray(value)) {
value = value.split(/;, /);
}
@ -146,31 +141,22 @@ Ext.define('Proxmox.form.MultiDiskSelector', {
initComponent: function() {
let me = this;
let extraParams = {};
if (!me.url) {
if (!me.nodename) {
throw "no url or nodename given";
}
me.url = `/api2/json/nodes/${me.nodename}/disks/list`;
extraParams[me.typeParameter] = me.diskType;
if (me.includePartitions) {
extraParams['include-partitions'] = 1;
}
let node = me.nodename;
let param = me.typeParameter;
let type = me.diskType;
me.url = `/api2/json/nodes/${node}/disks/list?${param}=${type}`;
}
me.disks = [];
me.callParent();
let store = me.getStore();
store.setProxy({
type: 'proxmox',
url: me.url,
extraParams,
});
store.getProxy().setUrl(me.url);
store.load();
store.sort({ property: me.valueField });
},

View File

@ -45,6 +45,10 @@ Ext.define('Proxmox.form.NetworkSelector', {
networkSelectorStore.load();
}
},
// set default value to empty array, else it inits it with
// null and after the store load it is an empty array,
// triggering dirtychange
value: [],
valueField: 'cidr',
displayField: 'cidr',
store: {
@ -65,8 +69,8 @@ Ext.define('Proxmox.form.NetworkSelector', {
},
],
listeners: {
load: function(store, records, successful) {
if (successful) {
load: function(store, records, successfull) {
if (successfull) {
records.forEach(function(record) {
if (record.data.cidr6) {
let dest = record.data.cidr ? record.copy(null) : record;
@ -119,7 +123,6 @@ Ext.define('Proxmox.form.NetworkSelector', {
header: gettext('Comment'),
flex: 2,
dataIndex: 'comments',
renderer: Ext.String.htmlEncode,
},
],
},

View File

@ -6,13 +6,7 @@ Ext.define('Proxmox.form.RealmComboBox', {
xclass: 'Ext.app.ViewController',
init: function(view) {
let store = view.getStore();
store.proxy.url = `/api2/json${view.baseUrl}`;
if (view.storeFilter) {
store.setFilters(view.storeFilter);
}
store.on('load', this.onLoad, view);
store.load();
view.store.on('load', this.onLoad, view);
},
onLoad: function(store, records, success) {
@ -33,9 +27,6 @@ Ext.define('Proxmox.form.RealmComboBox', {
},
},
// define custom filters for the underlying store
storeFilter: undefined,
fieldLabel: gettext('Realm'),
name: 'realm',
queryMode: 'local',
@ -46,7 +37,6 @@ Ext.define('Proxmox.form.RealmComboBox', {
triggerAction: 'all',
valueField: 'realm',
displayField: 'descr',
baseUrl: '/access/domains',
getState: function() {
return { value: this.getValue() };
},
@ -62,6 +52,6 @@ Ext.define('Proxmox.form.RealmComboBox', {
store: {
model: 'pmx-domains',
autoLoad: false,
autoLoad: true,
},
});

View File

@ -18,22 +18,17 @@ Ext.define('Proxmox.form.RoleSelector', {
displayField: 'roleid',
listConfig: {
width: 560,
resizable: true,
columns: [
{
header: gettext('Role'),
sortable: true,
dataIndex: 'roleid',
flex: 2,
flex: 1,
},
{
header: gettext('Privileges'),
dataIndex: 'privs',
cellWrap: true,
// join manually here, as ExtJS joins without whitespace which breaks cellWrap
renderer: v => Ext.isArray(v) ? v.join(', ') : v.replaceAll(',', ', '),
flex: 5,
flex: 1,
},
],
},

View File

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

View File

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

View File

@ -1,6 +0,0 @@
Ext.define('Proxmox.form.ThemeSelector', {
extend: 'Proxmox.form.KVComboBox',
xtype: 'proxmoxThemeSelector',
comboItems: Proxmox.Utils.theme_array(),
});

View File

@ -1,50 +0,0 @@
Ext.define('Proxmox.form.UserSelector', {
extend: 'Proxmox.form.ComboGrid',
alias: 'widget.pmxUserSelector',
allowBlank: false,
autoSelect: false,
valueField: 'userid',
displayField: 'userid',
editable: true,
anyMatch: true,
forceSelection: true,
store: {
model: 'pmx-users',
autoLoad: true,
params: {
enabled: 1,
},
sorters: 'userid',
},
listConfig: {
columns: [
{
header: gettext('User'),
sortable: true,
dataIndex: 'userid',
renderer: Ext.String.htmlEncode,
flex: 1,
},
{
header: gettext('Name'),
sortable: true,
renderer: (first, mD, rec) => Ext.String.htmlEncode(
`${first || ''} ${rec.data.lastname || ''}`,
),
dataIndex: 'firstname',
flex: 1,
},
{
header: gettext('Comment'),
sortable: false,
dataIndex: 'comment',
renderer: Ext.String.htmlEncode,
flex: 1,
},
],
},
});

View File

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

View File

@ -1,8 +1,8 @@
/** Renders a list of key values objects
/** Renders a list of key values objets
Mandatory Config Parameters:
rows: an object container where each property is a key-value object we want to render
rows: an object container where each propery is a key-value object we want to render
rows: {
keyboard: {
@ -67,6 +67,7 @@ Ext.define('Proxmox.grid.ObjectGrid', {
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
onlineHelp: opts.onlineHelp,
fieldDefaults: {
labelWidth: opts.labelWidth || 100,
},
@ -83,9 +84,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
},
},
};
if (opts.onlineHelp) {
me.rows[name].editor.onlineHelp = opts.onlineHelp;
}
},
add_text_row: function(name, text, opts) {
@ -102,6 +100,7 @@ Ext.define('Proxmox.grid.ObjectGrid', {
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
onlineHelp: opts.onlineHelp,
fieldDefaults: {
labelWidth: opts.labelWidth || 100,
},
@ -116,9 +115,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
},
},
};
if (opts.onlineHelp) {
me.rows[name].editor.onlineHelp = opts.onlineHelp;
}
},
add_boolean_row: function(name, text, opts) {
@ -135,6 +131,7 @@ Ext.define('Proxmox.grid.ObjectGrid', {
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
onlineHelp: opts.onlineHelp,
fieldDefaults: {
labelWidth: opts.labelWidth || 100,
},
@ -150,9 +147,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
},
},
};
if (opts.onlineHelp) {
me.rows[name].editor.onlineHelp = opts.onlineHelp;
}
},
add_integer_row: function(name, text, opts) {
@ -169,6 +163,7 @@ Ext.define('Proxmox.grid.ObjectGrid', {
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
onlineHelp: opts.onlineHelp,
fieldDefaults: {
labelWidth: opts.labelWidth || 100,
},
@ -185,40 +180,6 @@ Ext.define('Proxmox.grid.ObjectGrid', {
},
},
};
if (opts.onlineHelp) {
me.rows[name].editor.onlineHelp = opts.onlineHelp;
}
},
// adds a row that allows editing in a full TextArea that transparently de/encodes as Base64
add_textareafield_row: function(name, text, opts) {
let me = this;
opts = opts || {};
me.rows = me.rows || {};
let fieldOpts = opts.fieldOpts || {};
me.rows[name] = {
required: true,
defaultValue: "",
header: text,
renderer: value => Ext.htmlEncode(Proxmox.Utils.base64ToUtf8(value)),
editor: {
xtype: 'proxmoxWindowEdit',
subject: text,
fieldDefaults: {
labelWidth: opts.labelWidth || 600,
},
items: {
xtype: 'proxmoxBase64TextArea',
...fieldOpts,
name,
},
},
};
if (opts.onlineHelp) {
me.rows[name].editor.onlineHelp = opts.onlineHelp;
}
},
editorConfig: {}, // default config passed to editor
@ -287,7 +248,7 @@ Ext.define('Proxmox.grid.ObjectGrid', {
let renderer = rowdef.renderer;
if (renderer) {
return renderer.call(me, value, metaData, record, rowIndex, colIndex, store);
return renderer(value, metaData, record, rowIndex, colIndex, store);
}
return value;

View File

@ -1,18 +1,19 @@
# icon-cpu, icon-ram
# are self made (sources as .xcf)
include ../defines.mk
IMAGES=pmx-clear-trigger.png \
openid-icon-100x100.png \
icon-cpu.svg \
icon-ram.svg \
debian-swirl-openlogo.svg \
proxmox-symbol-x.svg \
IMAGES=pmx-clear-trigger.png \
icon-cpu.png \
icon-ram.png \
all:
.PHONY: install
install: $(IMAGES)
install -d $(WWWIMAGESDIR)
for i in $(IMAGES); do install -m 0644 $$i $(WWWIMAGESDIR)/$$i; done
install: ${IMAGES}
install -d ${WWWIMAGESDIR}
for i in ${IMAGES}; do install -m 0755 $$i ${WWWIMAGESDIR}/$$i; done
.PHONY: clean
clean:

View File

@ -1,26 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 10.0, SVG Export Plug-In . SVG Version: 3.0.0 Build 77) -->
<svg enable-background="new 0 0 87.041 108.445" height="108.445" viewBox="0,0,87.041,108.445" width="87.041" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:x="http://ns.adobe.com/Extensibility/1.0/" xmlns:i="http://ns.adobe.com/AdobeIllustrator/10.0/" xmlns:graph="http://ns.adobe.com/Graphs/1.0/" i:viewOrigin="262 450" i:rulerOrigin="0 0" i:pageBounds="0 792 612 0" xmlns:a="http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/">
<metadata>
<sfw>
<slices/>
<sliceSourceBounds bottomLeftOrigin="true"/>
</sfw>
</metadata>
<g id="Layer_1" i:layer="yes" i:dimmedPercent="50" i:rgbTrio="#4F008000FFFF">
<g>
<path d="m51.986 57.297c-1.797.025.34.926 2.686 1.287.648-.506 1.236-1.018 1.76-1.516-1.461.358-2.948.366-4.446.229" fill="#a80030" i:knockout="Off"/>
<path d="m61.631 54.893c1.07-1.477 1.85-3.094 2.125-4.766-.24 1.192-.887 2.221-1.496 3.307-3.359 2.115-.316-1.256-.002-2.537-3.612 4.546-.496 2.726-.627 3.996" fill="#a80030" i:knockout="Off"/>
<path d="m65.191 45.629c.217-3.236-.637-2.213-.924-.978.335.174.6 2.281.924.978" fill="#a80030" i:knockout="Off"/>
<path d="m45.172 1.399c.959.172 2.072.304 1.916.533 1.049-.23 1.287-.442-1.916-.533" fill="#a80030" i:knockout="Off"/>
<path d="m47.088 1.932-.678.14.631-.056z" fill="#a80030" i:knockout="Off"/>
<path d="m76.992 46.856c.107 2.906-.85 4.316-1.713 6.812l-1.553.776c-1.271 2.468.123 1.567-.787 3.53-1.984 1.764-6.021 5.52-7.313 5.863-.943-.021.639-1.113.846-1.541-2.656 1.824-2.131 2.738-6.193 3.846l-.119-.264c-10.018 4.713-23.934-4.627-23.751-17.371-.107.809-.304.607-.526.934-.517-6.557 3.028-13.143 9.007-15.832 5.848-2.895 12.704-1.707 16.893 2.197-2.301-3.014-6.881-6.209-12.309-5.91-5.317.084-10.291 3.463-11.951 7.131-2.724 1.715-3.04 6.611-4.227 7.507-1.597 11.737 3.004 16.808 10.787 22.773 1.225.826.345.951.511 1.58-2.586-1.211-4.954-3.039-6.901-5.277 1.033 1.512 2.148 2.982 3.589 4.137-2.438-.826-5.695-5.908-6.646-6.115 4.203 7.525 17.052 13.197 23.78 10.383-3.113.115-7.068.064-10.566-1.229-1.469-.756-3.467-2.322-3.11-2.615 9.182 3.43 18.667 2.598 26.612-3.771 2.021-1.574 4.229-4.252 4.867-4.289-.961 1.445.164.695-.574 1.971 2.014-3.248-.875-1.322 2.082-5.609l1.092 1.504c-.406-2.696 3.348-5.97 2.967-10.234.861-1.304.961 1.403.047 4.403 1.268-3.328.334-3.863.66-6.609.352.923.814 1.904 1.051 2.878-.826-3.216.848-5.416 1.262-7.285-.408-.181-1.275 1.422-1.473-2.377.029-1.65.459-.865.625-1.271-.324-.186-1.174-1.451-1.691-3.877.375-.57 1.002 1.478 1.512 1.562-.328-1.929-.893-3.4-.916-4.88-1.49-3.114-.527.415-1.736-1.337-1.586-4.947 1.316-1.148 1.512-3.396 2.404 3.483 3.775 8.881 4.404 11.117-.48-2.726-1.256-5.367-2.203-7.922.73.307-1.176-5.609.949-1.691-2.27-8.352-9.715-16.156-16.564-19.818.838.767 1.896 1.73 1.516 1.881-3.406-2.028-2.807-2.186-3.295-3.043-2.775-1.129-2.957.091-4.795.002-5.23-2.774-6.238-2.479-11.051-4.217l.219 1.023c-3.465-1.154-4.037.438-7.782.004-.228-.178 1.2-.644 2.375-.815-3.35.442-3.193-.66-6.471.122.808-.567 1.662-.942 2.524-1.424-2.732.166-6.522 1.59-5.352.295-4.456 1.988-12.37 4.779-16.811 8.943l-.14-.933c-2.035 2.443-8.874 7.296-9.419 10.46l-.544.127c-1.059 1.793-1.744 3.825-2.584 5.67-1.385 2.36-2.03.908-1.833 1.278-2.724 5.523-4.077 10.164-5.246 13.97.833 1.245.02 7.495.335 12.497-1.368 24.704 17.338 48.69 37.785 54.228 2.997 1.072 7.454 1.031 11.245 1.141-4.473-1.279-5.051-.678-9.408-2.197-3.143-1.48-3.832-3.17-6.058-5.102l.881 1.557c-4.366-1.545-2.539-1.912-6.091-3.037l.941-1.229c-1.415-.107-3.748-2.385-4.386-3.646l-1.548.061c-1.86-2.295-2.851-3.949-2.779-5.23l-.5.891c-.567-.973-6.843-8.607-3.587-6.83-.605-.553-1.409-.9-2.281-2.484l.663-.758c-1.567-2.016-2.884-4.6-2.784-5.461.836 1.129 1.416 1.34 1.99 1.533-3.957-9.818-4.179-.541-7.176-9.994l.634-.051c-.486-.732-.781-1.527-1.172-2.307l.276-2.75c-2.849-3.294-.797-14.006-.386-19.881.285-2.389 2.378-4.932 3.97-8.92l-.97-.167c1.854-3.234 10.586-12.988 14.63-12.486 1.959-2.461-.389-.009-.772-.629 4.303-4.453 5.656-3.146 8.56-3.947 3.132-1.859-2.688.725-1.203-.709 5.414-1.383 3.837-3.144 10.9-3.846.745.424-1.729.655-2.35 1.205 4.511-2.207 14.275-1.705 20.617 1.225 7.359 3.439 15.627 13.605 15.953 23.17l.371.1c-.188 3.802.582 8.199-.752 12.238z" fill="#a80030" i:knockout="Off"/>
<path d="m32.372 59.764-.252 1.26c1.181 1.604 2.118 3.342 3.626 4.596-1.085-2.118-1.891-2.993-3.374-5.856" fill="#a80030" i:knockout="Off"/>
<path d="m35.164 59.654c-.625-.691-.995-1.523-1.409-2.352.396 1.457 1.207 2.709 1.962 3.982z" fill="#a80030" i:knockout="Off"/>
<path d="m84.568 48.916-.264.662c-.484 3.438-1.529 6.84-3.131 9.994 1.77-3.328 2.915-6.968 3.395-10.656" fill="#a80030" i:knockout="Off"/>
<path d="m45.527.537c1.215-.445 2.987-.244 4.276-.537-1.68.141-3.352.225-5.003.438z" fill="#a80030" i:knockout="Off"/>
<path d="m2.872 23.219c.28 2.592-1.95 3.598.494 1.889 1.31-2.951-.512-.815-.494-1.889" fill="#a80030" i:knockout="Off"/>
<path d="m0 35.215c.563-1.728.665-2.766.88-3.766-1.556 1.989-.716 2.413-.88 3.766" fill="#a80030" i:knockout="Off"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.0 KiB

BIN
src/images/icon-cpu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

View File

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="100"
height="100"
version="1.1"
>
<g fill="none" stroke="#000" stroke-width="5" shape-rendering="crispEdges">
<!-- base CPU -->
<rect x="15" y="15" rx="0" ry="0" width="70" height="70" />
<rect x="30" y="30" rx="0" ry="0" width="40" height="40" fill="black"/>
<g stroke-width="8"> <!-- pins -->
<!-- left -->
<path d="m14,24.5 h-12.5"/>
<path d="m14,41.5 h-12.5"/>
<path d="m14,58.5 h-12.5"/>
<path d="m14,75.5 h-12.5"/>
<!-- right -->
<path d="m86,24.5 h+12.5"/>
<path d="m86,41.5 h+12.5"/>
<path d="m86,58.5 h+12.5"/>
<path d="m86,75.5 h+12.5"/>
<!-- top -->
<path d="m24.5,14 v-12.5"/>
<path d="m41.5,14 v-12.5"/>
<path d="m58.5,14 v-12.5"/>
<path d="m75.5,14 v-12.5"/>
<!-- bottom -->
<path d="m24.5,86 v+12.5"/>
<path d="m41.5,86 v+12.5"/>
<path d="m58.5,86 v+12.5"/>
<path d="m75.5,86 v+12.5"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

BIN
src/images/icon-cpu.xcf Normal file

Binary file not shown.

BIN
src/images/icon-ram.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
width="100"
height="100"
version="1.1"
>
<g fill="none" stroke="#000" stroke-width="7" shape-rendering="crispEdges">
<rect x="3.0" y="25" rx="0" ry="0" width="94" height="40"/> <!-- outer dimm PCB border -->
<g stroke-width="0"> <!-- dimm chips -->
<rect x="12.5" y="35" rx="0" ry="0" width="20" height="20" fill="black"/>
<rect x="40.0" y="35" rx="0" ry="0" width="20" height="20" fill="black"/>
<rect x="67.5" y="35" rx="0" ry="0" width="20" height="20" fill="black"/>
</g>
<g stroke-width="8"> <!-- pins -->
<path d="m10,67 v+10"/>
<path d="m26,67 v+10"/>
<path d="m42,67 v+10"/>
<path d="m58,67 v+10"/>
<path d="m74,67 v+10"/>
<path d="m90,67 v+10"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

BIN
src/images/icon-ram.xcf Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg id="svg5876" height="260" viewBox="0,0,259.99998,259.99998" width="260" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" inkscape:version="0.92.2 (5c3e80d, 2017-08-06)" sodipodi:docname="Proxmox_symbol_favicon.svg" inkscape:export-filename="S:\Proxmox\Marketing\Logo\Favicon\Proxmox_symbol_favicon_260.png" inkscape:export-xdpi="96" inkscape:export-ydpi="96">
<clipPath id="clipPath3102-4-5">
<rect id="rect3104-1-3" height="326.40991" transform="matrix(.73449161 .67861776 -.78497193 .61953133 0 0)" width="436.40189" x="-82.999916" y="-347.71387"/>
</clipPath>
<sodipodi:namedview pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="1.4" inkscape:cx="-45.895771" inkscape:cy="6.3650844" inkscape:document-units="px" inkscape:current-layer="layer1" showgrid="false" inkscape:window-width="2560" inkscape:window-height="1377" inkscape:window-x="-8" inkscape:window-y="-8" inkscape:window-maximized="1" units="px" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0"/>
<metadata id="metadata5881">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title/>
</cc:Work>
</rdf:RDF>
</metadata>
<g id="layer1" transform="translate(-2.603202 -774.18762)" inkscape:label="Layer 1" inkscape:groupmode="layer">
<g id="g3021" transform="matrix(9.45675 0 0 9.45675 -18.704755 -8896.4297)" inkscape:export-xdpi="300" inkscape:export-ydpi="300">
<g id="g7607-5" clip-path="url(#clipPath3102-4-5)" font-family="Helion" font-size="144" letter-spacing="0" line-height="125%" transform="matrix(-.04778455 0 0 .04778455 29.203266 1024.7667)" word-spacing="0">
<path id="path7609-8" d="m276.30443 226.6231 190.58784-209.602495c-7.39324-7.3925179-16.00465-13.2011729-25.83431-17.42598195-9.8304-4.22414015-20.39156-6.37699515-31.68359-6.45857095-11.99338.0917448-22.98109 2.4680153-32.96314 7.12881886-9.98263 4.66147074-18.655 11.05910904-26.0171 19.19293404l-74.09086 80.915015-73.60342-80.915015c-7.6265-8.133835-16.50198-14.5314802-26.62641-19.1929557-10.12463-4.6608083-21.07169-7.0370717-32.84124-7.1287972-11.29242.0815863-21.8536 2.2344429-31.6836 6.45857839-9.83005 4.22480401-18.441478 10.03345661-25.834313 17.42597451l190.593403 209.596915" inkscape:connector-curvature="0" sodipodi:nodetypes="ccccccccccccc"/>
</g>
<g id="g7611-6" clip-path="url(#clipPath3102-4-5)" font-family="Helion" font-size="144" letter-spacing="0" line-height="125%" transform="matrix(.04778455 0 0 -.04778455 2.796945 1047.9575)" word-spacing="0">
<path id="path7613-7" d="m276.30443 226.6231 190.58784-209.602495c-7.39324-7.3925179-16.00465-13.2011729-25.83431-17.42598195-9.8304-4.22414015-20.39156-6.37699515-31.68359-6.45857095-11.99338.0917448-22.98109 2.4680153-32.96314 7.12881886-9.98263 4.66147074-18.655 11.05910904-26.0171 19.19293404l-74.09086 80.915015-73.60342-80.915015c-7.6265-8.133835-16.50198-14.5314802-26.62641-19.1929557-10.12463-4.6608083-21.07169-7.0370717-32.84124-7.1287972-11.29242.0815863-21.8536 2.2344429-31.6836 6.45857839-9.83005 4.22480401-18.441478 10.03345661-25.834313 17.42597451l190.593403 209.596915" sodipodi:nodetypes="ccccccccccccc" inkscape:connector-curvature="0"/>
</g>
<path id="path7615-7" d="m15.23405 1036.3622-6.8601967-7.5231c-.3990192-.4256-.8633807-.7604-1.3930839-1.0042-.5297195-.2439-1.1024686-.3682-1.7182481-.3731-.590815 0-1.1433739.1168-1.6576769.338-.5143056.221-.9648526.5249-1.3516436.9118l6.9622457 7.6508-6.9622457 7.6508c.38679.3989.837338.7103 1.3516436.934.514303.2237 1.0668619.3373 1.6576769.3411.6173744 0 1.1933086-.129 1.7278076-.373.5344943-.2439.9956679-.5787 1.3835244-1.0041l6.8601817-7.549" fill="#e57000" font-family="Helion" font-size="1059.612793" letter-spacing="0" line-height="125%" word-spacing="0" inkscape:connector-curvature="0" sodipodi:nodetypes="cccccccccscscc"/>
<path id="path7617-2" d="m16.76594 1036.3622 6.860196-7.5231c.399019-.4256.86338-.7604 1.393084-1.0042.529719-.2439 1.10247-.3682 1.718247-.3731.590818 0 1.143377.1168 1.657677.338.51431.221.964857.5249 1.351646.9118l-6.962247 7.6508 6.962247 7.6508c-.386789.3989-.837336.7103-1.351646.934-.5143.2237-1.066859.3373-1.657673.3411-.617375 0-1.193311-.129-1.727809-.373-.534496-.2439-.99567-.5787-1.383526-1.0041l-6.860182-7.549" fill="#e57000" font-family="Helion" font-size="1059.612793" letter-spacing="0" line-height="125%" word-spacing="0" sodipodi:nodetypes="cccccccccscscc" inkscape:connector-curvature="0"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -1,99 +1,3 @@
/*
* The Proxmox CBind mixin is intended to supplement the 'bind' mechanism
* of ExtJS. In contrast to the 'bind', 'cbind' only acts during the creation
* of the component, not during its lifetime. It's only applied once before
* the 'initComponent' method is executed, and thus you have only access
* to the basic initial configuration of it.
*
* You can use it to get a 'declarative' approach to component declaration,
* even when you need to set some properties of sub-components dynamically
* (e.g., the 'nodename'). It overwrites the given properties of the 'cbind'
* object in the component with their computed values of the computed
* cbind configuration object of the 'cbindData' function (or object).
*
* The cbind syntax is inspired by ExtJS' bind syntax ('{property}'), where
* it is possible to negate values ('{!negated}'), access sub-properties of
* objects ('{object.property}') and even use a getter function,
* akin to viewModel formulas ('(get) => get("prop")') to execute more
* complicated dependencies (e.g., urls).
*
* The 'cbind' will be recursively applied to all properties (objects/arrays)
* that contain an 'xtype' or 'cbind' property, but stops for a subtree if the
* object in question does not have either (if you have one or more levels that
* have no cbind/xtype property, you can insert empty cbind objects there to
* reach deeper nested objects).
*
* This reduces the code in the 'initComponent' and instead we can statically
* declare items, buttons, tbars, etc. while the dynamic parts are contained
* in the 'cbind'.
*
* It is used like in the following example:
*
* Ext.define('Some.Component', {
* extend: 'Some.other.Component',
*
* // first it has to be enabled
* mixins: ['Proxmox.Mixin.CBind'],
*
* // then a base config has to be defined. this can be a function,
* // which has access to the initial config and can store persistent
* // properties, as well as return temporary ones (which only exist during
* // the cbind process)
* // this function will be called before 'initComponent'
* cbindData: function(initialconfig) {
* // 'this' here is the same as in 'initComponent'
* let me = this;
* me.persistentProperty = false;
* return {
* temporaryProperty: true,
* };
* },
*
* // if there is no need for persistent properties, it can also simply be an object
* cbindData: {
* temporaryProperty: true,
* // properties itself can also be functions that will be evaluated before
* // replacing the values
* dynamicProperty: (cfg) => !cfg.temporaryProperty,
* numericProp: 0,
* objectProp: {
* foo: 'bar',
* bar: 'baz',
* }
* },
*
* // you can 'cbind' the component itself, here the 'target' property
* // will be replaced with the content of 'temporaryProperty' (true)
* // before the components initComponent
* cbind: {
* target: '{temporaryProperty}',
* },
*
* items: [
* {
* xtype: 'checkbox',
* cbind: {
* value: '{!persistentProperty}',
* object: '{objectProp.foo}'
* dynamic: (get) => get('numericProp') + 1,
* },
* },
* {
* // empty cbind so that subitems are reached
* cbind: {},
* items: [
* {
* xtype: 'textfield',
* cbind: {
* value: '{objectProp.bar}',
* },
* },
* ],
* },
* ],
* });
*/
Ext.define('Proxmox.Mixin.CBind', {
extend: 'Ext.Mixin',

View File

@ -1,9 +1,7 @@
Ext.define('apt-pkglist', {
extend: 'Ext.data.Model',
fields: [
'Package', 'Title', 'Description', 'Section', 'Arch', 'Priority', 'Version', 'OldVersion',
'Origin',
],
fields: ['Package', 'Title', 'Description', 'Section', 'Arch',
'Priority', 'Version', 'OldVersion', 'ChangeLogUrl', 'Origin'],
idProperty: 'Package',
});
@ -58,7 +56,7 @@ Ext.define('Proxmox.node.APT', {
groupField: 'Origin',
proxy: {
type: 'proxmox',
url: `/api2/json/nodes/${me.nodename}/apt/update`,
url: "/api2/json/nodes/" + me.nodename + "/apt/update",
},
sorters: [
{
@ -67,7 +65,6 @@ Ext.define('Proxmox.node.APT', {
},
],
});
Proxmox.Utils.monStoreErrors(me, store, true);
let groupingFeature = Ext.create('Ext.grid.feature.Grouping', {
groupHeaderTpl: '{[ "Origin: " + values.name ]} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})',
@ -79,24 +76,37 @@ Ext.define('Proxmox.node.APT', {
let headerCt = this.view.headerCt;
let colspan = headerCt.getColumnCount();
return {
rowBody: `<div style="padding: 1em">${Ext.htmlEncode(data.Description)}</div>`,
rowBody: '<div style="padding: 1em">' +
Ext.String.htmlEncode(data.Description) +
'</div>',
rowBodyCls: me.full_description ? '' : Ext.baseCSSPrefix + 'grid-row-body-hidden',
rowBodyColspan: colspan,
};
},
});
let reload = function() {
store.load();
};
Proxmox.Utils.monStoreErrors(me, store, true);
let apt_command = function(cmd) {
Proxmox.Utils.API2Request({
url: `/nodes/${me.nodename}/apt/${cmd}`,
url: "/nodes/" + me.nodename + "/apt/" + cmd,
method: 'POST',
success: ({ result }) => Ext.create('Proxmox.window.TaskViewer', {
autoShow: true,
upid: result.data,
listeners: {
close: () => store.load(),
},
}),
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
success: function(response, opts) {
let upid = response.result.data;
let win = Ext.create('Proxmox.window.TaskViewer', {
upid: upid,
});
win.show();
me.mon(win, 'close', reload);
},
});
};
@ -104,18 +114,20 @@ Ext.define('Proxmox.node.APT', {
let update_btn = new Ext.Button({
text: gettext('Refresh'),
handler: () => Proxmox.Utils.checked_command(function() { apt_command('update'); }),
handler: function() {
Proxmox.Utils.checked_command(function() { apt_command('update'); });
},
});
let show_changelog = function(rec) {
if (!rec?.data?.Package) {
console.debug('cannot show changelog, missing Package', rec);
if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) {
return;
}
let view = Ext.createWidget('component', {
autoScroll: true,
style: {
'background-color': 'white',
'white-space': 'pre',
'font-family': 'monospace',
padding: '5px',
@ -125,7 +137,7 @@ Ext.define('Proxmox.node.APT', {
let win = Ext.create('Ext.window.Window', {
title: gettext('Changelog') + ": " + rec.data.Package,
width: 800,
height: 600,
height: 400,
layout: 'fit',
modal: true,
items: [view],
@ -154,8 +166,15 @@ Ext.define('Proxmox.node.APT', {
text: gettext('Changelog'),
selModel: sm,
disabled: true,
enableFn: rec => !!rec?.data?.Package,
handler: (b, e, rec) => show_changelog(rec),
enableFn: function(rec) {
if (!rec || !rec.data || !(rec.data.ChangeLogUrl && rec.data.Package)) {
return false;
}
return true;
},
handler: function(b, e, rec) {
show_changelog(rec);
},
});
let verbose_desc_checkbox = new Ext.form.field.Checkbox({
@ -182,12 +201,14 @@ Ext.define('Proxmox.node.APT', {
selModel: sm,
viewConfig: {
stripeRows: false,
emptyText: `<div style="display:flex;justify-content:center;"><p>${gettext('No updates available.')}</p></div>`,
emptyText: '<div style="display:table; width:100%; height:100%;"><div style="display:table-cell; vertical-align: middle; text-align:center;"><b>' + gettext('No updates available.') + '</div></div>',
},
features: [groupingFeature, rowBodyFeature],
listeners: {
activate: () => store.load(),
itemdblclick: (v, rec) => show_changelog(rec),
activate: reload,
itemdblclick: function(v, rec) {
show_changelog(rec);
},
},
});

View File

@ -1,801 +0,0 @@
Ext.define('apt-repolist', {
extend: 'Ext.data.Model',
fields: [
'Path',
'Index',
'Origin',
'FileType',
'Enabled',
'Comment',
'Types',
'URIs',
'Suites',
'Components',
'Options',
],
});
Ext.define('Proxmox.window.APTRepositoryAdd', {
extend: 'Proxmox.window.Edit',
alias: 'widget.pmxAPTRepositoryAdd',
isCreate: true,
isAdd: true,
subject: gettext('Repository'),
width: 600,
initComponent: function() {
let me = this;
if (!me.repoInfo || me.repoInfo.length === 0) {
throw "repository information not initialized";
}
let description = Ext.create('Ext.form.field.Display', {
fieldLabel: gettext('Description'),
name: 'description',
});
let status = Ext.create('Ext.form.field.Display', {
fieldLabel: gettext('Status'),
name: 'status',
renderer: function(value) {
let statusText = gettext('Not yet configured');
if (value !== '') {
statusText = Ext.String.format(
'{0}: {1}',
gettext('Configured'),
value ? gettext('enabled') : gettext('disabled'),
);
}
return statusText;
},
});
let repoSelector = Ext.create('Proxmox.form.KVComboBox', {
fieldLabel: gettext('Repository'),
xtype: 'proxmoxKVComboBox',
name: 'handle',
allowBlank: false,
comboItems: me.repoInfo.map(info => [info.handle, info.name]),
validator: function(renderedValue) {
let handle = this.value;
// we cannot use this.callParent in instantiations
let valid = Proxmox.form.KVComboBox.prototype.validator.call(this, renderedValue);
if (!valid || !handle) {
return false;
}
const info = me.repoInfo.find(elem => elem.handle === handle);
if (!info) {
return false;
}
if (info.status) {
return Ext.String.format(gettext('{0} is already configured'), renderedValue);
}
return valid;
},
listeners: {
change: function(f, value) {
const info = me.repoInfo.find(elem => elem.handle === value);
description.setValue(info.description);
status.setValue(info.status);
},
},
});
repoSelector.setValue(me.repoInfo[0].handle);
Ext.apply(me, {
items: [
repoSelector,
description,
status,
],
repoSelector: repoSelector,
});
me.callParent();
},
});
Ext.define('Proxmox.node.APTRepositoriesErrors', {
extend: 'Ext.grid.GridPanel',
xtype: 'proxmoxNodeAPTRepositoriesErrors',
store: {},
scrollable: true,
viewConfig: {
stripeRows: false,
getRowClass: (record) => {
switch (record.data.status) {
case 'warning': return 'proxmox-warning-row';
case 'critical': return 'proxmox-invalid-row';
default: return '';
}
},
},
hideHeaders: true,
columns: [
{
dataIndex: 'status',
renderer: (value) => `<i class="fa fa-fw ${Proxmox.Utils.get_health_icon(value, true)}"></i>`,
width: 50,
},
{
dataIndex: 'message',
flex: 1,
},
],
});
Ext.define('Proxmox.node.APTRepositoriesGrid', {
extend: 'Ext.grid.GridPanel',
xtype: 'proxmoxNodeAPTRepositoriesGrid',
mixins: ['Proxmox.Mixin.CBind'],
title: gettext('APT Repositories'),
cls: 'proxmox-apt-repos', // to allow applying styling to general components with local effect
border: false,
tbar: [
{
text: gettext('Reload'),
iconCls: 'fa fa-refresh',
handler: function() {
let me = this;
me.up('proxmoxNodeAPTRepositories').reload();
},
},
{
text: gettext('Add'),
name: 'addRepo',
disabled: true,
repoInfo: undefined,
cbind: {
onlineHelp: '{onlineHelp}',
},
handler: function(button, event, record) {
Proxmox.Utils.checked_command(() => {
let me = this;
let panel = me.up('proxmoxNodeAPTRepositories');
let extraParams = {};
if (panel.digest !== undefined) {
extraParams.digest = panel.digest;
}
Ext.create('Proxmox.window.APTRepositoryAdd', {
repoInfo: me.repoInfo,
url: `/api2/extjs/nodes/${panel.nodename}/apt/repositories`,
method: 'PUT',
extraRequestParams: extraParams,
onlineHelp: me.onlineHelp,
listeners: {
destroy: function() {
panel.reload();
},
},
}).show();
});
},
},
'-',
{
xtype: 'proxmoxAltTextButton',
defaultText: gettext('Enable'),
altText: gettext('Disable'),
name: 'repoEnable',
disabled: true,
bind: {
text: '{enableButtonText}',
},
handler: function(button, event, record) {
let me = this;
let panel = me.up('proxmoxNodeAPTRepositories');
let params = {
path: record.data.Path,
index: record.data.Index,
enabled: record.data.Enabled ? 0 : 1, // invert
};
if (panel.digest !== undefined) {
params.digest = panel.digest;
}
Proxmox.Utils.API2Request({
url: `/nodes/${panel.nodename}/apt/repositories`,
method: 'POST',
params: params,
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
panel.reload();
},
success: function(response, opts) {
panel.reload();
},
});
},
},
],
sortableColumns: false,
viewConfig: {
stripeRows: false,
getRowClass: (record, index) => record.get('Enabled') ? '' : 'proxmox-disabled-row',
},
columns: [
{
header: gettext('Enabled'),
dataIndex: 'Enabled',
align: 'center',
renderer: Proxmox.Utils.renderEnabledIcon,
width: 90,
},
{
header: gettext('Types'),
dataIndex: 'Types',
renderer: function(types, cell, record) {
return types.join(' ');
},
width: 100,
},
{
header: gettext('URIs'),
dataIndex: 'URIs',
renderer: function(uris, cell, record) {
return uris.join(' ');
},
width: 350,
},
{
header: gettext('Suites'),
dataIndex: 'Suites',
renderer: function(suites, metaData, record) {
let err = '';
if (record.data.warnings && record.data.warnings.length > 0) {
let txt = [gettext('Warning')];
record.data.warnings.forEach((warning) => {
if (warning.property === 'Suites') {
txt.push(Ext.htmlEncode(warning.message));
}
});
metaData.tdAttr = `data-qtip="${Ext.htmlEncode(txt.join('<br>'))}"`;
if (record.data.Enabled) {
metaData.tdCls = 'proxmox-invalid-row';
err = '<i class="fa fa-fw critical fa-exclamation-circle"></i> ';
} else {
metaData.tdCls = 'proxmox-warning-row';
err = '<i class="fa fa-fw warning fa-exclamation-circle"></i> ';
}
}
return suites.join(' ') + err;
},
width: 130,
},
{
header: gettext('Components'),
dataIndex: 'Components',
renderer: function(components, metaData, record) {
if (components === undefined) {
return '';
}
let err = '';
if (components.length === 1) {
// FIXME: this should be a flag set to the actual repsotiories, i.e., a tristate
// like production-ready = <yes|no|other> (Option<bool>)
if (components[0].match(/\w+(-no-subscription|test)\s*$/i)) {
metaData.tdCls = 'proxmox-warning-row';
err = '<i class="fa fa-fw warning fa-exclamation-circle"></i> ';
let qtip = components[0].match(/no-subscription/)
? gettext('The no-subscription repository is NOT production-ready')
: gettext('The test repository may contain unstable updates')
;
metaData.tdAttr = `data-qtip="${Ext.htmlEncode(Ext.htmlEncode(qtip))}"`;
}
}
return components.join(' ') + err;
},
width: 170,
},
{
header: gettext('Options'),
dataIndex: 'Options',
renderer: function(options, cell, record) {
if (!options) {
return '';
}
let filetype = record.data.FileType;
let text = '';
options.forEach(function(option) {
let key = option.Key;
if (filetype === 'list') {
let values = option.Values.join(',');
text += `${key}=${values} `;
} else if (filetype === 'sources') {
let values = option.Values.join(' ');
text += `${key}: ${values}<br>`;
} else {
throw "unknown file type";
}
});
return text;
},
flex: 1,
},
{
header: gettext('Origin'),
dataIndex: 'Origin',
width: 120,
renderer: function(value, meta, rec) {
if (typeof value !== 'string' || value.length === 0) {
value = gettext('Other');
}
let cls = 'fa fa-fw fa-question-circle-o';
let originType = this.up('proxmoxNodeAPTRepositories').classifyOrigin(value);
if (originType === 'Proxmox') {
cls = 'pmx-itype-icon pmx-itype-icon-proxmox-x';
} else if (originType === 'Debian') {
cls = 'pmx-itype-icon pmx-itype-icon-debian-swirl';
}
return `<i class='${cls}'></i> ${value}`;
},
},
{
header: gettext('Comment'),
dataIndex: 'Comment',
flex: 2,
renderer: Ext.String.htmlEncode,
},
],
features: [
{
ftype: 'grouping',
groupHeaderTpl: '{[ "File: " + values.name ]} ({rows.length} repositor{[values.rows.length > 1 ? "ies" : "y"]})',
enableGroupingMenu: false,
},
],
store: {
model: 'apt-repolist',
groupField: 'Path',
sorters: [
{
property: 'Index',
direction: 'ASC',
},
],
},
initComponent: function() {
let me = this;
if (!me.nodename) {
throw "no node name specified";
}
me.callParent();
},
});
Ext.define('Proxmox.node.APTRepositories', {
extend: 'Ext.panel.Panel',
xtype: 'proxmoxNodeAPTRepositories',
mixins: ['Proxmox.Mixin.CBind'],
digest: undefined,
onlineHelp: undefined,
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',
selectionChange: function(grid, selection) {
let me = this;
if (!selection || selection.length < 1) {
return;
}
let rec = selection[0];
let vm = me.getViewModel();
vm.set('selectionenabled', rec.get('Enabled'));
vm.notify();
},
updateState: function() {
let me = this;
let vm = me.getViewModel();
let store = vm.get('errorstore');
store.removeAll();
let status = 'good'; // start with best, the helper below will downgrade if needed
let text = gettext('All OK, you have production-ready repositories configured!');
let addGood = message => store.add({ status: 'good', message });
let addWarn = (message, important) => {
if (status !== 'critical') {
status = 'warning';
text = important ? message : gettext('Warning');
}
store.add({ status: 'warning', message });
};
let addCritical = (message, important) => {
status = 'critical';
text = important ? message : gettext('Error');
store.add({ status: 'critical', message });
};
let errors = vm.get('errors');
errors.forEach(error => addCritical(`${error.path} - ${error.error}`));
let activeSubscription = vm.get('subscriptionActive');
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(
Ext.String.format(gettext('No {0} repository is enabled, you do not get any updates!'), vm.get('product')),
);
} else if (errors.length > 0) {
// nothing extra, just avoid that we show "get updates"
} else if (enterprise && !nosubscription && !test && activeSubscription) {
addGood(Ext.String.format(gettext('You get supported updates for {0}'), vm.get('product')));
} else if (nosubscription || test) {
addGood(Ext.String.format(gettext('You get updates for {0}'), vm.get('product')));
}
if (wrongSuites) {
addWarn(gettext('Some suites are misconfigured'));
}
if (mixedSuites) {
addWarn(gettext('Detected mixed suites before upgrade'));
}
let productionReadyCheck = (repos, type, noSubAlternateName) => {
if (!activeSubscription && repos.enterprise) {
addWarn(Ext.String.format(
gettext('The {0}enterprise repository is enabled, but there is no active subscription!'),
type,
));
}
if (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');
}
let iconCls = Proxmox.Utils.get_health_icon(status, true);
vm.set('state', {
iconCls,
text,
});
},
},
viewModel: {
data: {
product: 'Proxmox VE', // default
errors: [],
suitesWarning: false,
mixedSuites: false, // used before major upgrade
subscriptionActive: '',
noSubscriptionRepo: '',
enterpriseRepo: '',
testRepo: '',
cephEnterpriseRepo: '',
cephNoSubscriptionRepo: '',
cephTestRepo: '',
selectionenabled: false,
state: {},
},
formulas: {
enableButtonText: (get) => get('selectionenabled')
? gettext('Disable') : gettext('Enable'),
},
stores: {
errorstore: {
fields: ['status', 'message'],
},
},
},
scrollable: true,
layout: {
type: 'vbox',
align: 'stretch',
},
items: [
{
xtype: 'panel',
border: false,
layout: {
type: 'hbox',
align: 'stretch',
},
height: 200,
title: gettext('Status'),
items: [
{
xtype: 'box',
flex: 2,
margin: 10,
data: {
iconCls: Proxmox.Utils.get_health_icon(undefined, true),
text: '',
},
bind: {
data: '{state}',
},
tpl: [
'<center class="centered-flex-column" style="font-size:15px;line-height: 25px;">',
'<i class="fa fa-4x {iconCls}"></i>',
'{text}',
'</center>',
],
},
{
xtype: 'proxmoxNodeAPTRepositoriesErrors',
name: 'repositoriesErrors',
flex: 7,
margin: 10,
bind: {
store: '{errorstore}',
},
},
],
},
{
xtype: 'proxmoxNodeAPTRepositoriesGrid',
name: 'repositoriesGrid',
flex: 1,
cbind: {
nodename: '{nodename}',
onlineHelp: '{onlineHelp}',
},
majorUpgradeAllowed: false, // TODO get release information from an API call?
listeners: {
selectionchange: 'selectionChange',
},
},
],
check_subscription: function() {
let me = this;
let vm = me.getViewModel();
Proxmox.Utils.API2Request({
url: `/nodes/${me.nodename}/subscription`,
method: 'GET',
failure: (response, opts) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
success: function(response, opts) {
const res = response.result;
const subscription = !(!res || !res.data || res.data.status.toLowerCase() !== 'active');
vm.set('subscriptionActive', subscription);
me.getController().updateState();
},
});
},
updateStandardRepos: function(standardRepos) {
let me = this;
let vm = me.getViewModel();
let addButton = me.down('button[name=addRepo]');
addButton.repoInfo = [];
for (const standardRepo of standardRepos) {
const handle = standardRepo.handle;
const status = standardRepo.status;
if (handle === "enterprise") {
vm.set('enterpriseRepo', status);
} else if (handle === "no-subscription") {
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();
addButton.repoInfo.push(standardRepo);
addButton.digest = me.digest;
}
addButton.setDisabled(false);
},
reload: function() {
let me = this;
let vm = me.getViewModel();
let repoGrid = me.down('proxmoxNodeAPTRepositoriesGrid');
me.store.load(function(records, operation, success) {
let gridData = [];
let errors = [];
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;
errors = data.errors;
digest = data.digest;
let infos = {};
for (const info of data.infos) {
let path = info.path;
let idx = info.index;
if (!infos[path]) {
infos[path] = {};
}
if (!infos[path][idx]) {
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') {
infos[path][idx].warnings.push(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;
}
}
}
files.forEach(function(file) {
for (let n = 0; n < file.repositories.length; n++) {
let repo = file.repositories[n];
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.warnings = infos[file.path][n].warnings || [];
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);
}
});
repoGrid.store.loadData(gridData);
me.updateStandardRepos(data['standard-repos']);
}
me.digest = digest;
vm.set('errors', errors);
vm.set('suitesWarning', suitesWarning);
vm.set('mixedSuites', mixedSuites);
me.getController().updateState();
});
me.check_subscription();
},
listeners: {
activate: function() {
let me = this;
me.reload();
},
},
initComponent: function() {
let me = this;
if (!me.nodename) {
throw "no node name specified";
}
let store = Ext.create('Ext.data.Store', {
proxy: {
type: 'proxmox',
url: `/api2/json/nodes/${me.nodename}/apt/repositories`,
},
});
Ext.apply(me, { store: store });
Proxmox.Utils.monStoreErrors(me, me.store, true);
me.callParent();
me.getViewModel().set('product', me.product);
},
});

View File

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

View File

@ -2,10 +2,6 @@ Ext.define('Proxmox.node.DNSView', {
extend: 'Proxmox.grid.ObjectGrid',
alias: ['widget.proxmoxNodeDNSView'],
// Some longer existing APIs use a brittle "replace whole config" style, you can set this option
// if the DNSView component is used in an API that has more modern, granular update semantics.
deleteEmpty: false,
initComponent: function() {
let me = this;
@ -13,20 +9,21 @@ Ext.define('Proxmox.node.DNSView', {
throw "no node name specified";
}
let run_editor = () => Ext.create('Proxmox.node.DNSEdit', {
autoShow: true,
nodename: me.nodename,
deleteEmpty: me.deleteEmpty,
});
let run_editor = function() {
let win = Ext.create('Proxmox.node.DNSEdit', {
nodename: me.nodename,
});
win.show();
};
Ext.apply(me, {
url: `/api2/json/nodes/${me.nodename}/dns`,
url: "/api2/json/nodes/" + me.nodename + "/dns",
cwidth1: 130,
interval: 10 * 1000,
interval: 1000,
run_editor: run_editor,
rows: {
search: {
header: gettext('Search domain'),
header: 'Search domain',
required: true,
renderer: Ext.htmlEncode,
},

View File

@ -2,9 +2,6 @@ Ext.define('Proxmox.node.NetworkEdit', {
extend: 'Proxmox.window.Edit',
alias: ['widget.proxmoxNodeNetworkEdit'],
// Enable to show the VLAN ID field
enableBridgeVlanIds: false,
initComponent: function() {
let me = this;
@ -60,75 +57,22 @@ 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',
fieldLabel: gettext('Bridge ports'),
name: 'bridge_ports',
autoEl: {
tag: 'div',
'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',
fieldLabel: gettext('Bridge ports'),
name: 'ovs_ports',
autoEl: {
tag: 'div',
'data-qtip': gettext('Space-separated list of interfaces, for example: enp0s0 enp1s0'),
},
});
column2.push({
xtype: 'textfield',
@ -145,7 +89,7 @@ Ext.define('Proxmox.node.NetworkEdit', {
name: 'ovs_bridge',
});
column2.push({
xtype: 'proxmoxvlanfield',
xtype: 'pveVlanField',
deleteEmpty: !me.isCreate,
name: 'ovs_tag',
value: '',
@ -188,7 +132,7 @@ Ext.define('Proxmox.node.NetworkEdit', {
});
column2.push({
xtype: 'proxmoxvlanfield',
xtype: 'pveVlanField',
name: 'vlan-id',
value: me.vlanidvalue,
disabled: me.disablevlanid,
@ -259,7 +203,7 @@ Ext.define('Proxmox.node.NetworkEdit', {
name: 'ovs_bridge',
});
column2.push({
xtype: 'proxmoxvlanfield',
xtype: 'pveVlanField',
deleteEmpty: !me.isCreate,
name: 'ovs_tag',
value: '',
@ -302,7 +246,6 @@ Ext.define('Proxmox.node.NetworkEdit', {
value: me.iface,
vtype: iface_vtype,
allowBlank: false,
maxLength: iface_vtype === 'BridgeName' ? 10 : 15,
autoEl: {
tag: 'div',
'data-qtip': gettext('For example, vmbr0.100, vmbr0, vlan0.100, vlan0'),

View File

@ -16,8 +16,6 @@ Ext.define('proxmox-networks', {
'netmask6',
'slaves',
'type',
'vlan-id',
'vlan-raw-device',
],
idProperty: 'iface',
});
@ -33,9 +31,6 @@ Ext.define('Proxmox.node.NetworkView', {
showApplyBtn: false,
// for options passed down to the network edit window
editOptions: {},
initComponent: function() {
let me = this;
@ -43,7 +38,7 @@ Ext.define('Proxmox.node.NetworkView', {
throw "no node name specified";
}
let baseUrl = `/nodes/${me.nodename}/network`;
let baseUrl = '/nodes/' + me.nodename + '/network';
let store = Ext.create('Ext.data.Store', {
model: 'proxmox-networks',
@ -98,16 +93,13 @@ Ext.define('Proxmox.node.NetworkView', {
return;
}
Ext.create('Proxmox.node.NetworkEdit', {
autoShow: true,
let win = Ext.create('Proxmox.node.NetworkEdit', {
nodename: me.nodename,
iface: rec.data.iface,
iftype: rec.data.type,
...me.editOptions,
listeners: {
destroy: () => reload(),
},
});
win.show();
win.on('destroy', reload);
};
let edit_btn = new Ext.Button({
@ -116,12 +108,31 @@ Ext.define('Proxmox.node.NetworkView', {
handler: run_editor,
});
let sm = Ext.create('Ext.selection.RowModel', {});
let del_btn = new Ext.Button({
text: gettext('Remove'),
disabled: true,
handler: function() {
let grid = me.down('gridpanel');
let sm = grid.getSelectionModel();
let rec = sm.getSelection()[0];
if (!rec) {
return;
}
let del_btn = new Proxmox.button.StdRemoveButton({
selModel: sm,
getUrl: ({ data }) => `${baseUrl}/${data.iface}`,
callback: () => reload(),
let iface = rec.data.iface;
Proxmox.Utils.API2Request({
url: baseUrl + '/' + iface,
method: 'DELETE',
waitMsgTarget: me,
callback: function() {
reload();
},
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
});
},
});
let apply_btn = Ext.create('Proxmox.button.Button', {
@ -135,64 +146,102 @@ Ext.define('Proxmox.node.NetworkView', {
url: baseUrl,
method: 'PUT',
waitMsgTarget: me,
success: function({ result }, opts) {
Ext.create('Proxmox.window.TaskProgress', {
autoShow: true,
success: function(response, opts) {
let upid = response.result.data;
let win = Ext.create('Proxmox.window.TaskProgress', {
taskDone: reload,
upid: result.data,
upid: upid,
});
win.show();
},
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
});
},
});
let set_button_status = function() {
let grid = me.down('gridpanel');
let sm = grid.getSelectionModel();
let rec = sm.getSelection()[0];
edit_btn.setDisabled(!rec);
del_btn.setDisabled(!rec);
};
let findNextFreeInterfaceId = function(prefix) {
for (let next = 0; next <= 9999; next++) {
let id = `${prefix}${next.toString()}`;
if (!store.getById(id)) {
return id;
}
let render_ports = function(value, metaData, record) {
if (value === 'bridge') {
return record.data.bridge_ports;
} else if (value === 'bond') {
return record.data.slaves;
} else if (value === 'OVSBridge') {
return record.data.ovs_ports;
} else if (value === 'OVSBond') {
return record.data.ovs_bonds;
}
Ext.Msg.alert('Error', `No free ID for ${prefix} found!`);
return '';
};
let menu_items = [];
let addEditWindowToMenu = (iType, iDefault) => {
menu_items.push({
text: Proxmox.Utils.render_network_iface_type(iType),
handler: () => Ext.create('Proxmox.node.NetworkEdit', {
autoShow: true,
nodename: me.nodename,
iftype: iType,
iface_default: findNextFreeInterfaceId(iDefault ?? iType),
...me.editOptions,
onlineHelp: 'sysadmin_network_configuration',
listeners: {
destroy: () => reload(),
},
}),
});
let find_next_iface_id = function(prefix) {
let next;
for (next = 0; next <= 9999; next++) {
if (!store.getById(prefix + next.toString())) {
break;
}
}
return prefix + next.toString();
};
let menu_items = [];
if (me.types.indexOf('bridge') !== -1) {
addEditWindowToMenu('bridge', 'vmbr');
menu_items.push({
text: Proxmox.Utils.render_network_iface_type('bridge'),
handler: function() {
let win = Ext.create('Proxmox.node.NetworkEdit', {
nodename: me.nodename,
iftype: 'bridge',
iface_default: find_next_iface_id('vmbr'),
onlineHelp: 'sysadmin_network_configuration',
});
win.on('destroy', reload);
win.show();
},
});
}
if (me.types.indexOf('bond') !== -1) {
addEditWindowToMenu('bond');
menu_items.push({
text: Proxmox.Utils.render_network_iface_type('bond'),
handler: function() {
let win = Ext.create('Proxmox.node.NetworkEdit', {
nodename: me.nodename,
iftype: 'bond',
iface_default: find_next_iface_id('bond'),
onlineHelp: 'sysadmin_network_configuration',
});
win.on('destroy', reload);
win.show();
},
});
}
if (me.types.indexOf('vlan') !== -1) {
addEditWindowToMenu('vlan');
menu_items.push({
text: Proxmox.Utils.render_network_iface_type('vlan'),
handler: function() {
let win = Ext.create('Proxmox.node.NetworkEdit', {
nodename: me.nodename,
iftype: 'vlan',
iface_default: find_next_iface_id('vlan'),
onlineHelp: 'sysadmin_network_configuration',
});
win.on('destroy', reload);
win.show();
},
});
}
if (me.types.indexOf('ovs') !== -1) {
@ -200,20 +249,43 @@ Ext.define('Proxmox.node.NetworkView', {
menu_items.push({ xtype: 'menuseparator' });
}
addEditWindowToMenu('OVSBridge', 'vmbr');
addEditWindowToMenu('OVSBond', 'bond');
menu_items.push({
text: Proxmox.Utils.render_network_iface_type('OVSIntPort'),
handler: () => Ext.create('Proxmox.node.NetworkEdit', {
autoShow: true,
nodename: me.nodename,
iftype: 'OVSIntPort',
listeners: {
destroy: () => reload(),
menu_items.push(
{
text: Proxmox.Utils.render_network_iface_type('OVSBridge'),
handler: function() {
let win = Ext.create('Proxmox.node.NetworkEdit', {
nodename: me.nodename,
iftype: 'OVSBridge',
iface_default: find_next_iface_id('vmbr'),
});
win.on('destroy', reload);
win.show();
},
}),
});
},
{
text: Proxmox.Utils.render_network_iface_type('OVSBond'),
handler: function() {
let win = Ext.create('Proxmox.node.NetworkEdit', {
nodename: me.nodename,
iftype: 'OVSBond',
iface_default: find_next_iface_id('bond'),
});
win.on('destroy', reload);
win.show();
},
},
{
text: Proxmox.Utils.render_network_iface_type('OVSIntPort'),
handler: function() {
let win = Ext.create('Proxmox.node.NetworkEdit', {
nodename: me.nodename,
iftype: 'OVSIntPort',
});
win.on('destroy', reload);
win.show();
},
},
);
}
let renderer_generator = function(fieldname) {
@ -250,7 +322,9 @@ Ext.define('Proxmox.node.NetworkView', {
callback: function() {
reload();
},
failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
failure: function(response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
});
},
},
@ -265,7 +339,6 @@ Ext.define('Proxmox.node.NetworkView', {
stateful: true,
stateId: 'grid-node-network',
store: store,
selModel: sm,
region: 'center',
border: false,
columns: [
@ -314,18 +387,7 @@ Ext.define('Proxmox.node.NetworkView', {
{
header: gettext('Ports/Slaves'),
dataIndex: 'type',
renderer: (value, metaData, { data }) => {
if (value === 'bridge') {
return data.bridge_ports;
} else if (value === 'bond') {
return data.slaves;
} else if (value === 'OVSBridge') {
return data.ovs_ports;
} else if (value === 'OVSBond') {
return data.ovs_bonds;
}
return '';
},
renderer: render_ports,
},
{
header: gettext('Bond Mode'),
@ -367,24 +429,6 @@ Ext.define('Proxmox.node.NetworkView', {
dataIndex: 'gateway',
renderer: renderer_generator('gateway'),
},
{
header: gettext('VLAN ID'),
hidden: true,
sortable: true,
dataIndex: 'vlan-id',
},
{
header: gettext('VLAN raw device'),
hidden: true,
sortable: true,
dataIndex: 'vlan-raw-device',
},
{
header: 'MTU',
hidden: true,
sortable: true,
dataIndex: 'mtu',
},
{
header: gettext('Comment'),
dataIndex: 'comments',

View File

@ -1,6 +1,6 @@
Ext.define('proxmox-services', {
extend: 'Ext.data.Model',
fields: ['service', 'name', 'desc', 'state', 'unit-state', 'active-state'],
fields: ['service', 'name', 'desc', 'state'],
idProperty: 'service',
});
@ -24,13 +24,11 @@ Ext.define('Proxmox.node.ServiceView', {
interval: 1000,
model: 'proxmox-services',
proxy: {
type: 'proxmox',
url: `/api2/json/nodes/${me.nodename}/services`,
type: 'proxmox',
url: "/api2/json/nodes/" + me.nodename + "/services",
},
});
let filterInstalledOnly = record => record.get('unit-state') !== 'not-found';
let store = Ext.create('Proxmox.data.DiffStore', {
rstore: rstore,
sortAfterUpdate: true,
@ -40,45 +38,30 @@ 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() {
let { data: { service } } = me.getSelectionModel().getSelection()[0];
Ext.create('Ext.window.Window', {
title: gettext('Syslog') + ': ' + service,
let sm = me.getSelectionModel();
let rec = sm.getSelection()[0];
let win = Ext.create('Ext.window.Window', {
title: gettext('Syslog') + ': ' + rec.data.service,
modal: true,
width: 800,
height: 400,
layout: 'fit',
items: {
xtype: 'proxmoxLogView',
url: `/api2/extjs/nodes/${me.nodename}/syslog?service=${service}`,
url: "/api2/extjs/nodes/" + me.nodename + "/syslog?service=" +
rec.data.service,
log_select_timespan: 1,
},
autoShow: true,
});
win.show();
};
let service_cmd = function(cmd) {
let { data: { service } } = me.getSelectionModel().getSelection()[0];
let rec = me.getSelectionModel().getSelection()[0];
let service = rec.data.service;
Proxmox.Utils.API2Request({
url: `/nodes/${me.nodename}/services/${service}/${cmd}`,
method: 'POST',
@ -88,10 +71,12 @@ Ext.define('Proxmox.node.ServiceView', {
},
success: function(response, opts) {
rstore.startUpdate();
Ext.create('Proxmox.window.TaskProgress', {
upid: response.result.data,
autoShow: true,
let upid = response.result.data;
let win = Ext.create('Proxmox.window.TaskProgress', {
upid: upid,
});
win.show();
},
});
};
@ -99,18 +84,27 @@ Ext.define('Proxmox.node.ServiceView', {
let start_btn = new Ext.Button({
text: gettext('Start'),
disabled: true,
handler: () => service_cmd("start"),
handler: function() {
service_cmd("start");
},
});
let stop_btn = new Ext.Button({
text: gettext('Stop'),
disabled: true,
handler: () => service_cmd("stop"),
handler: function() {
service_cmd("stop");
},
});
let restart_btn = new Ext.Button({
text: gettext('Restart'),
disabled: true,
handler: () => service_cmd(me.restartCommand || "restart"),
handler: function() {
service_cmd(me.restartCommand || "restart");
},
});
let syslog_btn = new Ext.Button({
text: gettext('Syslog'),
disabled: true,
@ -130,28 +124,23 @@ Ext.define('Proxmox.node.ServiceView', {
}
let service = rec.data.service;
let state = rec.data.state;
let unit = rec.data['unit-state'];
syslog_btn.enable();
if (state === 'running') {
if (me.startOnlyServices[service]) {
stop_btn.disable();
restart_btn.enable();
} else {
stop_btn.enable();
restart_btn.enable();
start_btn.disable();
}
} else if (unit !== undefined && (unit === 'masked' || unit === 'unknown' || unit === 'not-found')) {
start_btn.disable();
restart_btn.disable();
stop_btn.disable();
restart_btn.enable();
} else {
start_btn.enable();
stop_btn.disable();
restart_btn.disable();
}
if (!me.startOnlyServices[service]) {
if (state === 'running') {
stop_btn.enable();
} else {
stop_btn.disable();
}
}
};
me.mon(store, 'refresh', set_button_status);
@ -159,36 +148,9 @@ Ext.define('Proxmox.node.ServiceView', {
Proxmox.Utils.monStoreErrors(me, rstore);
Ext.apply(me, {
viewConfig: {
trackOver: false,
stripeRows: false, // does not work with getRowClass()
getRowClass: function(record, index) {
let unitState = record.get('unit-state');
if (!unitState) {
return '';
}
if (unitState === 'masked' || unitState === 'not-found') {
return "proxmox-disabled-row";
} else if (unitState === 'unknown') {
if (record.get('name') === 'syslog') {
return "proxmox-disabled-row"; // replaced by journal on most hosts
}
return "proxmox-warning-row";
}
return '';
},
},
store: store,
stateful: false,
tbar: [
start_btn,
stop_btn,
restart_btn,
'-',
syslog_btn,
'->',
unHideCB,
],
tbar: [start_btn, stop_btn, restart_btn, syslog_btn],
columns: [
{
header: gettext('Name'),
@ -201,30 +163,6 @@ Ext.define('Proxmox.node.ServiceView', {
width: 100,
sortable: true,
dataIndex: 'state',
renderer: (value, meta, rec) => {
const unitState = rec.get('unit-state');
if (unitState === 'masked') {
return gettext('disabled');
} else if (unitState === 'not-found') {
return gettext('not installed');
} else {
return value;
}
},
},
{
header: gettext('Active'),
width: 100,
sortable: true,
hidden: true,
dataIndex: 'active-state',
},
{
header: gettext('Unit'),
width: 120,
sortable: true,
hidden: !Ext.Array.contains(['PVEAuthCookie', 'PBSAuthCookie'], Proxmox?.Setup?.auth_cookie_name),
dataIndex: 'unit-state',
},
{
header: gettext('Description'),

View File

@ -1,515 +1,217 @@
Ext.define('Proxmox.node.Tasks', {
extend: 'Ext.grid.GridPanel',
alias: 'widget.proxmoxNodeTasks',
alias: ['widget.proxmoxNodeTasks'],
stateful: true,
stateId: 'pve-grid-node-tasks',
stateId: 'grid-node-tasks',
loadMask: true,
sortableColumns: false,
vmidFilter: 0,
// set extra filter components, must have a 'name' property for the parameter, and must
// trigger a 'change' event if the value is 'undefined', it will not be sent to the api
extraFilter: [],
initComponent: function() {
let me = this;
if (!me.nodename) {
throw "no node name specified";
}
// fixed filters which cannot be changed after instantiation, for example:
// { vmid: 100 }
preFilter: {},
let store = Ext.create('Ext.data.BufferedStore', {
pageSize: 500,
autoLoad: true,
remoteFilter: true,
model: 'proxmox-tasks',
proxy: {
type: 'proxmox',
startParam: 'start',
limitParam: 'limit',
url: "/api2/json/nodes/" + me.nodename + "/tasks",
},
});
controller: {
xclass: 'Ext.app.ViewController',
store.on('prefetch', function() {
// we want to update the scrollbar on every store load
// since the total count might be different
// the buffered grid plugin does this only on scrolling itself
// and even reduces the scrollheight again when scrolling up
me.updateLayout();
});
showTaskLog: function() {
let me = this;
let selection = me.getView().getSelection();
if (selection.length < 1) {
let userfilter = '';
let filter_errors = 0;
let updateProxyParams = function() {
let params = {
errors: filter_errors,
};
if (userfilter) {
params.userfilter = userfilter;
}
if (me.vmidFilter) {
params.vmid = me.vmidFilter;
}
store.proxy.extraParams = params;
};
updateProxyParams();
let reload_task = Ext.create('Ext.util.DelayedTask', function() {
updateProxyParams();
store.reload();
});
let run_task_viewer = function() {
let sm = me.getSelectionModel();
let rec = sm.getSelection()[0];
if (!rec) {
return;
}
let rec = selection[0];
Ext.create('Proxmox.window.TaskViewer', {
let win = Ext.create('Proxmox.window.TaskViewer', {
upid: rec.data.upid,
endtime: rec.data.endtime,
}).show();
},
updateLayout: function(store, records, success, operation) {
let me = this;
let view = me.getView().getView(); // the table view, not the whole grid
Proxmox.Utils.setErrorMask(view, false);
// update the scrollbar on every store load since the total count might be different.
// the buffered grid plugin does this only on (user) scrolling itself and even reduces
// the scrollheight again when scrolling up
me.getView().updateLayout();
if (!success) {
Proxmox.Utils.setErrorMask(view, Proxmox.Utils.getResponseErrorMessage(operation.getError()));
}
},
refresh: function() {
let me = this;
let view = me.getView();
let selection = view.getSelection();
let store = me.getViewModel().get('bufferedstore');
if (selection && selection.length > 0) {
// deselect if selection is not there anymore
if (!store.contains(selection[0])) {
view.setSelection(undefined);
}
}
},
sinceChange: function(field, newval) {
let me = this;
let vm = me.getViewModel();
vm.set('since', newval);
},
untilChange: function(field, newval, oldval) {
let me = this;
let vm = me.getViewModel();
vm.set('until', newval);
},
reload: function() {
let me = this;
let view = me.getView();
view.getStore().load();
},
showFilter: function(btn, pressed) {
let me = this;
let vm = me.getViewModel();
vm.set('showFilter', pressed);
},
clearFilter: function() {
let me = this;
me.lookup('filtertoolbar').query('field').forEach((field) => {
field.setValue(undefined);
});
},
},
win.show();
};
listeners: {
itemdblclick: 'showTaskLog',
},
let view_btn = new Ext.Button({
text: gettext('View'),
disabled: true,
handler: run_task_viewer,
});
viewModel: {
data: {
typefilter: '',
statusfilter: '',
showFilter: false,
extraFilter: {},
since: null,
until: null,
},
Proxmox.Utils.monStoreErrors(me, store, true);
formulas: {
filterIcon: (get) => 'fa fa-filter' + (get('showFilter') ? ' info-blue' : ''),
extraParams: function(get) {
let me = this;
let params = {};
if (get('typefilter')) {
params.typefilter = get('typefilter');
}
if (get('statusfilter')) {
params.statusfilter = get('statusfilter');
}
Ext.apply(me, {
store: store,
viewConfig: {
trackOver: false,
stripeRows: false, // does not work with getRowClass()
if (get('extraFilter')) {
let extraFilter = get('extraFilter');
for (const [name, value] of Object.entries(extraFilter)) {
if (value !== undefined && value !== null && value !== "") {
params[name] = value;
getRowClass: function(record, index) {
let status = record.get('status');
if (status) {
let parsed = Proxmox.Utils.parse_task_status(status);
if (parsed === 'error') {
return "proxmox-invalid-row";
} else if (parsed === 'warning') {
return "proxmox-warning-row";
}
}
}
if (get('since')) {
params.since = get('since').valueOf()/1000;
}
if (get('until')) {
let until = new Date(get('until').getTime()); // copy object
until.setDate(until.getDate() + 1); // end of the day
params.until = until.valueOf()/1000;
}
me.getView().getStore().load();
return params;
},
filterCount: function(get) {
let count = 0;
if (get('typefilter')) {
count++;
}
let status = get('statusfilter');
if ((Ext.isArray(status) && status.length > 0) ||
(!Ext.isArray(status) && status)) {
count++;
}
if (get('since')) {
count++;
}
if (get('until')) {
count++;
}
if (get('extraFilter')) {
let preFilter = get('preFilter') || {};
let extraFilter = get('extraFilter');
for (const [name, value] of Object.entries(extraFilter)) {
if (value !== undefined && value !== null && value !== "" &&
preFilter[name] === undefined
) {
count++;
}
}
}
return count;
},
clearFilterText: function(get) {
let count = get('filterCount');
let fieldMsg = '';
if (count > 1) {
fieldMsg = ` (${count} ${gettext('Fields')})`;
} else if (count > 0) {
fieldMsg = ` (1 ${gettext('Field')})`;
}
return gettext('Clear Filter') + fieldMsg;
},
},
stores: {
bufferedstore: {
type: 'buffered',
pageSize: 500,
autoLoad: true,
remoteFilter: true,
model: 'proxmox-tasks',
proxy: {
type: 'proxmox',
startParam: 'start',
limitParam: 'limit',
extraParams: '{extraParams}',
url: '{url}',
},
listeners: {
prefetch: 'updateLayout',
refresh: 'refresh',
return '';
},
},
},
},
bind: {
store: '{bufferedstore}',
},
dockedItems: [
{
xtype: 'toolbar',
items: [
tbar: [
view_btn,
{
xtype: 'proxmoxButton',
text: gettext('View'),
iconCls: 'fa fa-window-restore',
disabled: true,
handler: 'showTaskLog',
},
{
xtype: 'button',
text: gettext('Reload'),
iconCls: 'fa fa-refresh',
handler: 'reload',
text: gettext('Refresh'), // FIXME: smart-auto-refresh store
handler: () => store.reload(),
},
'->',
gettext('User name') +':',
' ',
{
xtype: 'button',
bind: {
text: '{clearFilterText}',
disabled: '{!filterCount}',
},
text: gettext('Clear Filter'),
enabled: false,
handler: 'clearFilter',
},
{
xtype: 'button',
enableToggle: true,
bind: {
iconCls: '{filterIcon}',
},
text: gettext('Filter'),
stateful: true,
stateId: 'task-showfilter',
stateEvents: ['toggle'],
applyState: function(state) {
if (state.pressed !== undefined) {
this.setPressed(state.pressed);
}
},
getState: function() {
return {
pressed: this.pressed,
};
},
xtype: 'textfield',
width: 200,
value: userfilter,
enableKeyEvents: true,
listeners: {
toggle: 'showFilter',
keyup: function(field, e) {
userfilter = field.getValue();
reload_task.delay(500);
},
},
}, ' ', gettext('Only Errors') + ':', ' ',
{
xtype: 'checkbox',
hideLabel: true,
checked: filter_errors,
listeners: {
change: function(field, checked) {
filter_errors = checked ? 1 : 0;
reload_task.delay(10);
},
},
}, ' ',
],
columns: [
{
header: gettext("Start Time"),
dataIndex: 'starttime',
width: 130,
renderer: function(value) {
return Ext.Date.format(value, "M d H:i:s");
},
},
{
header: gettext("End Time"),
dataIndex: 'endtime',
width: 130,
renderer: function(value, metaData, record) {
if (!value) {
metaData.tdCls = "x-grid-row-loading";
return '';
}
return Ext.Date.format(value, "M d H:i:s");
},
},
{
header: gettext("Duration"),
hidden: true,
width: 80,
renderer: function(value, metaData, record) {
let start = record.data.starttime;
if (start) {
let end = record.data.endtime || Date.now();
let duration = end - start;
if (duration > 0) {
duration /= 1000;
}
return Proxmox.Utils.format_duration_human(duration);
}
return Proxmox.Utils.unknownText;
},
},
{
header: gettext("Node"),
dataIndex: 'node',
width: 120,
},
{
header: gettext("User name"),
dataIndex: 'user',
width: 150,
},
{
header: gettext("Description"),
dataIndex: 'upid',
flex: 1,
renderer: Proxmox.Utils.render_upid,
},
{
header: gettext("Status"),
dataIndex: 'status',
width: 200,
renderer: function(value, metaData, record) {
if (value === undefined && !record.data.endtime) {
metaData.tdCls = "x-grid-row-loading";
return '';
}
return Proxmox.Utils.format_task_status(value);
},
},
],
},
{
xtype: 'toolbar',
dock: 'top',
reference: 'filtertoolbar',
layout: {
type: 'hbox',
align: 'top',
},
bind: {
hidden: '{!showFilter}',
},
items: [
{
xtype: 'container',
padding: 10,
layout: {
type: 'vbox',
align: 'stretch',
},
defaults: {
labelWidth: 80,
},
// cannot bind the values directly, as it then changes also
// on blur, causing wrong reloads of the store
items: [
{
xtype: 'datefield',
fieldLabel: gettext('Since'),
format: 'Y-m-d',
bind: {
maxValue: '{until}',
},
listeners: {
change: 'sinceChange',
},
},
{
xtype: 'datefield',
fieldLabel: gettext('Until'),
format: 'Y-m-d',
bind: {
minValue: '{since}',
},
listeners: {
change: 'untilChange',
},
},
],
listeners: {
itemdblclick: run_task_viewer,
selectionchange: function(v, selections) {
view_btn.setDisabled(!(selections && selections[0]));
},
{
xtype: 'container',
padding: 10,
layout: {
type: 'vbox',
align: 'stretch',
},
defaults: {
labelWidth: 80,
},
items: [
{
xtype: 'pmxTaskTypeSelector',
fieldLabel: gettext('Task Type'),
emptyText: gettext('All'),
bind: {
value: '{typefilter}',
},
},
{
xtype: 'combobox',
fieldLabel: gettext('Task Result'),
emptyText: gettext('All'),
multiSelect: true,
store: [
['ok', gettext('OK')],
['unknown', Proxmox.Utils.unknownText],
['warning', gettext('Warnings')],
['error', gettext('Errors')],
],
bind: {
value: '{statusfilter}',
},
},
],
},
],
},
],
viewConfig: {
trackOver: false,
stripeRows: false, // does not work with getRowClass()
emptyText: gettext('No Tasks found'),
getRowClass: function(record, index) {
let status = record.get('status');
if (status) {
let parsed = Proxmox.Utils.parse_task_status(status);
if (parsed === 'error') {
return "proxmox-invalid-row";
} else if (parsed === 'warning') {
return "proxmox-warning-row";
}
}
return '';
},
},
columns: [
{
header: gettext("Start Time"),
dataIndex: 'starttime',
width: 130,
renderer: function(value) {
return Ext.Date.format(value, "M d H:i:s");
show: function() { reload_task.delay(10); },
destroy: function() { reload_task.cancel(); },
},
},
{
header: gettext("End Time"),
dataIndex: 'endtime',
width: 130,
renderer: function(value, metaData, record) {
if (!value) {
metaData.tdCls = "x-grid-row-loading";
return '';
}
return Ext.Date.format(value, "M d H:i:s");
},
},
{
header: gettext("Duration"),
hidden: true,
width: 80,
renderer: function(value, metaData, record) {
let start = record.data.starttime;
if (start) {
let end = record.data.endtime || Date.now();
let duration = end - start;
if (duration > 0) {
duration /= 1000;
}
return Proxmox.Utils.format_duration_human(duration);
}
return Proxmox.Utils.unknownText;
},
},
{
header: gettext("User name"),
dataIndex: 'user',
width: 150,
},
{
header: gettext("Description"),
dataIndex: 'upid',
flex: 1,
renderer: Proxmox.Utils.render_upid,
},
{
header: gettext("Status"),
dataIndex: 'status',
width: 200,
renderer: function(value, metaData, record) {
if (value === undefined && !record.data.endtime) {
metaData.tdCls = "x-grid-row-loading";
return '';
}
return Proxmox.Utils.format_task_status(value);
},
},
],
initComponent: function() {
const me = this;
let nodename = me.nodename || 'localhost';
let url = me.url || `/api2/json/nodes/${nodename}/tasks`;
me.getViewModel().set('url', url);
let updateExtraFilters = function(name, value) {
let vm = me.getViewModel();
let extraFilter = Ext.clone(vm.get('extraFilter'));
extraFilter[name] = value;
vm.set('extraFilter', extraFilter);
};
for (const [name, value] of Object.entries(me.preFilter)) {
updateExtraFilters(name, value);
}
me.getViewModel().set('preFilter', me.preFilter);
});
me.callParent();
let addFields = function(items) {
me.lookup('filtertoolbar').add({
xtype: 'container',
padding: 10,
layout: {
type: 'vbox',
align: 'stretch',
},
defaults: {
labelWidth: 80,
},
items,
});
};
// start with a userfilter
me.extraFilter = [
{
xtype: 'textfield',
fieldLabel: gettext('User name'),
changeOptions: {
buffer: 500,
},
name: 'userfilter',
},
...me.extraFilter,
];
let items = [];
for (const filterTemplate of me.extraFilter) {
let filter = Ext.clone(filterTemplate);
filter.listeners = filter.listeners || {};
filter.listeners.change = Ext.apply(filter.changeOptions || {}, {
fn: function(field, value) {
updateExtraFilters(filter.name, value);
},
});
items.push(filter);
if (items.length === 2) {
addFields(items);
items = [];
}
}
addFields(items);
},
});

View File

@ -9,19 +9,21 @@ Ext.define('Proxmox.node.TimeView', {
throw "no node name specified";
}
let tzOffset = new Date().getTimezoneOffset() * 60000;
let renderLocaltime = function(value) {
let servertime = new Date((value * 1000) + tzOffset);
let tzoffset = new Date().getTimezoneOffset()*60000;
let renderlocaltime = function(value) {
let servertime = new Date((value * 1000) + tzoffset);
return Ext.Date.format(servertime, 'Y-m-d H:i:s');
};
let run_editor = () => Ext.create('Proxmox.node.TimeEdit', {
autoShow: true,
nodename: me.nodename,
});
let run_editor = function() {
let win = Ext.create('Proxmox.node.TimeEdit', {
nodename: me.nodename,
});
win.show();
};
Ext.apply(me, {
url: `/api2/json/nodes/${me.nodename}/time`,
url: "/api2/json/nodes/" + me.nodename + "/time",
cwidth1: 150,
interval: 1000,
run_editor: run_editor,
@ -33,7 +35,7 @@ Ext.define('Proxmox.node.TimeView', {
localtime: {
header: gettext('Server time'),
required: true,
renderer: renderLocaltime,
renderer: renderlocaltime,
},
},
tbar: [

View File

@ -1,182 +0,0 @@
Ext.define('Proxmox.panel.AuthView', {
extend: 'Ext.grid.GridPanel',
alias: 'widget.pmxAuthView',
mixins: ['Proxmox.Mixin.CBind'],
showDefaultRealm: false,
stateful: true,
stateId: 'grid-authrealms',
viewConfig: {
trackOver: false,
},
baseUrl: '/access/domains',
storeBaseUrl: '/access/domains',
columns: [
{
header: gettext('Realm'),
width: 100,
sortable: true,
dataIndex: 'realm',
},
{
header: gettext('Type'),
width: 100,
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,
dataIndex: 'comment',
renderer: Ext.String.htmlEncode,
flex: 1,
},
],
openEditWindow: function(authType, realm) {
let me = this;
const { useTypeInUrl, onlineHelp } = Proxmox.Schema.authDomains[authType];
Ext.create('Proxmox.window.AuthEditBase', {
baseUrl: me.baseUrl,
useTypeInUrl,
onlineHelp,
authType,
realm,
showDefaultRealm: me.showDefaultRealm,
listeners: {
destroy: () => me.reload(),
},
}).show();
},
reload: function() {
let me = this;
me.getStore().load();
},
run_editor: function() {
let me = this;
let rec = me.getSelection()[0];
if (!rec) {
return;
}
if (!Proxmox.Schema.authDomains[rec.data.type].edit) {
return;
}
me.openEditWindow(rec.data.type, rec.data.realm);
},
open_sync_window: function() {
let rec = this.getSelection()[0];
if (!rec) {
return;
}
if (!Proxmox.Schema.authDomains[rec.data.type].sync) {
return;
}
Ext.create('Proxmox.window.SyncWindow', {
type: rec.data.type,
realm: rec.data.realm,
listeners: {
destroy: () => this.reload(),
},
}).show();
},
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; }
menuitems.push({
text: config.name,
iconCls: 'fa fa-fw ' + (config.iconCls || 'fa-address-book-o'),
handler: () => me.openEditWindow(authType),
});
}
let tbar = [
{
text: gettext('Add'),
menu: {
items: menuitems,
},
},
{
xtype: 'proxmoxButton',
text: gettext('Edit'),
disabled: true,
enableFn: (rec) => Proxmox.Schema.authDomains[rec.data.type].edit,
handler: () => me.run_editor(),
},
{
xtype: 'proxmoxStdRemoveButton',
getUrl: (rec) => {
let url = me.baseUrl;
if (Proxmox.Schema.authDomains[rec.data.type].useTypeInUrl) {
url += `/${rec.get('type')}`;
}
url += `/${rec.getId()}`;
return url;
},
enableFn: (rec) => Proxmox.Schema.authDomains[rec.data.type].add,
callback: () => me.reload(),
},
{
xtype: 'proxmoxButton',
text: gettext('Sync'),
disabled: true,
enableFn: (rec) => Proxmox.Schema.authDomains[rec.data.type].sync,
handler: () => me.open_sync_window(),
},
];
if (me.extraButtons) {
tbar.push('-');
for (const button of me.extraButtons) {
tbar.push(button);
}
}
Ext.apply(me, {
tbar,
listeners: {
activate: () => me.reload(),
itemdblclick: () => me.run_editor(),
},
});
me.callParent();
},
});

View File

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

View File

@ -35,7 +35,7 @@ Ext.define('pmx-disk-list', {
return undefined;
},
},
'vendor', 'model', 'serial', 'rpm', 'type', 'wearout', 'health', 'mounted',
'vendor', 'model', 'serial', 'rpm', 'type', 'wearout', 'health',
],
idProperty: 'devpath',
});
@ -44,8 +44,6 @@ Ext.define('Proxmox.DiskList', {
extend: 'Ext.tree.Panel',
alias: 'widget.pmxDiskList',
supportsWipeDisk: false,
rootVisible: false,
emptyText: gettext('No Disks found'),
@ -99,40 +97,18 @@ Ext.define('Proxmox.DiskList', {
waitMsgTarget: view,
method: 'POST',
params: { disk: rec.data.name },
failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
success: function(response, options) {
Ext.create('Proxmox.window.TaskProgress', {
upid: response.result.data,
taskDone: function() {
me.reload();
},
autoShow: true,
});
failure: function(response, options) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
});
},
wipeDisk: function() {
let me = this;
let view = me.getView();
let selection = view.getSelection();
if (!selection || selection.length < 1) return;
let rec = selection[0];
Proxmox.Utils.API2Request({
url: `${view.exturl}/wipedisk`,
waitMsgTarget: view,
method: 'PUT',
params: { disk: rec.data.name },
failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
success: function(response, options) {
Ext.create('Proxmox.window.TaskProgress', {
upid: response.result.data,
var upid = response.result.data;
var win = Ext.create('Proxmox.window.TaskProgress', {
upid: upid,
taskDone: function() {
me.reload();
},
autoShow: true,
});
win.show();
},
});
},
@ -167,19 +143,10 @@ Ext.define('Proxmox.DiskList', {
for (const item of records) {
let data = item.data;
data.leaf = true;
data.expanded = true;
data.children = data.partitions ?? [];
for (let p of data.children) {
p['disk-type'] = 'partition';
p.iconCls = 'fa fa-fw fa-hdd-o x-fa-tree';
p.used = p.used === 'filesystem' ? p.filesystem : p.used;
p.parent = data.devpath;
p.children = [];
p.leaf = true;
}
data.children = [];
data.iconCls = 'fa fa-fw fa-hdd-o x-fa-tree';
data.leaf = data.children.length === 0;
if (!data.parent) {
disks[data.devpath] = data;
}
@ -220,11 +187,7 @@ Ext.define('Proxmox.DiskList', {
let extendedInfo = '';
if (rec) {
let types = [];
if (rec.data['osdid-list'] && rec.data['osdid-list'].length > 0) {
for (const id of rec.data['osdid-list'].sort()) {
types.push(`OSD.${id.toString()}`);
}
} else if (rec.data.osdid !== undefined && rec.data.osdid >= 0) {
if (rec.data.osdid !== undefined && rec.data.osdid >= 0) {
types.push(`OSD.${rec.data.osdid.toString()}`);
}
if (rec.data.journals > 0) {
@ -240,18 +203,45 @@ Ext.define('Proxmox.DiskList', {
extendedInfo = `, Ceph (${types.join(', ')})`;
}
}
const formatMap = {
'bios': 'BIOS boot',
'zfsreserved': 'ZFS reserved',
'efi': 'EFI',
'lvm': 'LVM',
'zfs': 'ZFS',
};
v = formatMap[v] || v;
return v ? `${v}${extendedInfo}` : Proxmox.Utils.noText;
},
tbar: [
{
text: gettext('Reload'),
handler: 'reload',
},
{
xtype: 'proxmoxButton',
text: gettext('Show S.M.A.R.T. values'),
parentXType: 'treepanel',
disabled: true,
enableFn: function(rec) {
if (!rec || rec.data.parent) {
return false;
} else {
return true;
}
},
handler: 'openSmartWindow',
},
{
xtype: 'proxmoxButton',
text: gettext('Initialize Disk with GPT'),
parentXType: 'treepanel',
disabled: true,
enableFn: function(rec) {
if (!rec || rec.data.parent ||
(rec.data.used && rec.data.used !== 'unused')) {
return false;
} else {
return true;
}
},
handler: 'initGPT',
},
],
columns: [
{
xtype: 'treecolumn',
@ -325,14 +315,7 @@ Ext.define('Proxmox.DiskList', {
dataIndex: 'status',
},
{
header: gettext('Mounted'),
width: 60,
align: 'right',
renderer: Proxmox.Utils.format_boolean,
dataIndex: 'mounted',
},
{
header: gettext('Wearout'),
header: 'Wearout',
width: 90,
sortable: true,
align: 'right',
@ -341,7 +324,7 @@ Ext.define('Proxmox.DiskList', {
if (Ext.isNumeric(value)) {
return (100 - value).toString() + '%';
}
return gettext('N/A');
return 'N/A';
},
},
],
@ -349,91 +332,4 @@ Ext.define('Proxmox.DiskList', {
listeners: {
itemdblclick: 'openSmartWindow',
},
initComponent: function() {
let me = this;
let tbar = [
{
text: gettext('Reload'),
handler: 'reload',
},
{
xtype: 'proxmoxButton',
text: gettext('Show S.M.A.R.T. values'),
parentXType: 'treepanel',
disabled: true,
enableFn: function(rec) {
if (!rec || rec.data.parent) {
return false;
} else {
return true;
}
},
handler: 'openSmartWindow',
},
{
xtype: 'proxmoxButton',
text: gettext('Initialize Disk with GPT'),
parentXType: 'treepanel',
disabled: true,
enableFn: function(rec) {
if (!rec || rec.data.parent ||
(rec.data.used && rec.data.used !== 'unused')) {
return false;
} else {
return true;
}
},
handler: 'initGPT',
},
];
if (me.supportsWipeDisk) {
tbar.push('-');
tbar.push({
xtype: 'proxmoxButton',
text: gettext('Wipe Disk'),
parentXType: 'treepanel',
dangerous: true,
confirmMsg: function(rec) {
const data = rec.data;
let mainMessage = Ext.String.format(
gettext('Are you sure you want to wipe {0}?'),
data.devpath,
);
mainMessage += `<br> ${gettext('All data on the device will be lost!')}`;
const type = me.renderDiskType(data["disk-type"]);
let usage;
if (data.children.length > 0) {
const partitionUsage = data.children.map(
partition => me.renderDiskUsage(partition.used),
).join(', ');
usage = `${gettext('Partitions')} (${partitionUsage})`;
} else {
usage = me.renderDiskUsage(data.used, undefined, rec);
}
const size = Proxmox.Utils.format_size(data.size);
const serial = Ext.String.htmlEncode(data.serial);
let additionalInfo = `${gettext('Type')}: ${type}<br>`;
additionalInfo += `${gettext('Usage')}: ${usage}<br>`;
additionalInfo += `${gettext('Size')}: ${size}<br>`;
additionalInfo += `${gettext('Serial')}: ${serial}`;
return `${mainMessage}<br><br>${additionalInfo}`;
},
disabled: true,
handler: 'wipeDisk',
});
}
me.tbar = tbar;
me.callParent();
},
});

View File

@ -3,7 +3,6 @@ Ext.define('Proxmox.EOLNotice', {
extend: 'Ext.Component',
alias: 'widget.proxmoxEOLNotice',
userCls: 'eol-notice',
padding: '0 5',
config: {
@ -18,25 +17,14 @@ 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 ${iconCls}"></i>
me.html = `<i class="fa pwt-eol-icon fa-exclamation-triangle"></i>
<a href="${href}" target="_blank">${message} <i class="fa fa-external-link"></i></a>
`;

View File

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

View File

@ -20,8 +20,6 @@ Ext.define('Proxmox.panel.GaugeWidget', {
xtype: 'polar',
height: 120,
border: false,
// set to '-' to suppress warning in debug mode
downloadServerUrl: '-',
itemId: 'chart',
series: [{
type: 'gauge',
@ -61,36 +59,6 @@ Ext.define('Proxmox.panel.GaugeWidget', {
initialValue: 0,
checkThemeColors: function() {
let me = this;
let rootStyle = getComputedStyle(document.documentElement);
// get colors
let panelBg = rootStyle.getPropertyValue("--pwt-panel-background").trim() || "#ffffff";
let textColor = rootStyle.getPropertyValue("--pwt-text-color").trim() || "#000000";
me.defaultColor = rootStyle.getPropertyValue("--pwt-gauge-default").trim() || '#c2ddf2';
me.criticalColor = rootStyle.getPropertyValue("--pwt-gauge-crit").trim() || '#ff6c59';
me.warningColor = rootStyle.getPropertyValue("--pwt-gauge-warn").trim() || '#fc0';
me.backgroundColor = rootStyle.getPropertyValue("--pwt-gauge-back").trim() || '#f5f5f5';
// set gauge colors
let value = me.chart.series[0].getValue() / 100;
let color = me.defaultColor;
if (value >= me.criticalThreshold) {
color = me.criticalColor;
} else if (value >= me.warningThreshold) {
color = me.warningColor;
}
me.chart.series[0].setColors([color, me.backgroundColor]);
// set text and background colors
me.chart.setBackground(panelBg);
me.valueSprite.setAttributes({ fillStyle: textColor }, true);
me.chart.redraw();
},
updateValue: function(value, text) {
let me = this;
@ -130,20 +98,5 @@ Ext.define('Proxmox.panel.GaugeWidget', {
me.text = me.getComponent('text');
me.chart = me.getComponent('chart');
me.valueSprite = me.chart.getSurface('chart').get('valueSprite');
me.checkThemeColors();
// switch colors on media query changes
me.mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
me.themeListener = (e) => { me.checkThemeColors(); };
me.mediaQueryList.addEventListener("change", me.themeListener);
},
doDestroy: function() {
let me = this;
me.mediaQueryList.removeEventListener("change", me.themeListener);
me.callParent();
},
});

View File

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

View File

@ -59,14 +59,7 @@ Ext.define('Proxmox.widget.Info', {
},
updateValue: function(text, usage) {
let me = this;
if (me.lastText === text && me.lastUsage === usage) {
return;
}
me.lastText = text;
me.lastUsage = usage;
var me = this;
var label = me.getComponent('label');
label.update(Ext.apply(label.data, { title: me.title, usage: text }));

View File

@ -23,7 +23,8 @@ Ext.define('Proxmox.panel.InputPanel', {
// will be set if the inputpanel has advanced items
hasAdvanced: false,
// if the panel has advanced items, this will determine if they are shown by default
// if the panel has advanced items,
// this will determine if they are shown by default
showAdvanced: false,
// overwrite this to modify submit data
@ -57,18 +58,12 @@ Ext.define('Proxmox.panel.InputPanel', {
}
},
onSetValues: function(values) {
return values;
},
setValues: function(values) {
let me = this;
let form = me.up('form');
values = me.onSetValues(values);
Ext.iterate(values, function(fieldId, val) {
Ext.iterate(values, function(fieldId, val) {
let fields = me.query('[isFormField][name=' + fieldId + ']');
for (const field of fields) {
if (field) {

View File

@ -1,7 +1,7 @@
/*
* Display log entries in a panel with scrollbar
* The log entries are automatically refreshed via a background task,
* with newest entries coming at the bottom
* with newest entries comming at the bottom
*/
Ext.define('Proxmox.panel.JournalView', {
extend: 'Ext.panel.Panel',
@ -77,8 +77,6 @@ Ext.define('Proxmox.panel.JournalView', {
let num = lines.length;
let text = lines.map(Ext.htmlEncode).join('<br>');
let contentChanged = true;
if (!livemode) {
if (num) {
view.content = text;
@ -91,8 +89,6 @@ Ext.define('Proxmox.panel.JournalView', {
view.content = view.content ? text + '<br>' + view.content : text;
} else if (!top && num) {
view.content = view.content ? view.content + '<br>' + text : text;
} else {
contentChanged = false;
}
// update cursors
@ -105,9 +101,7 @@ Ext.define('Proxmox.panel.JournalView', {
}
}
if (contentChanged) {
contentEl.update(view.content);
}
contentEl.update(view.content);
me.updateScroll(livemode, num, scrollPos, scrollPosTop);
},
@ -143,9 +137,6 @@ Ext.define('Proxmox.panel.JournalView', {
waitMsgTarget: !livemode ? view : undefined,
method: 'GET',
success: function(response) {
if (me.isDestroyed) {
return;
}
Proxmox.Utils.setErrorMask(me, false);
let lines = response.result.data;
me.updateView(lines, livemode, top);
@ -201,6 +192,7 @@ Ext.define('Proxmox.panel.JournalView', {
view.loadTask = new Ext.util.DelayedTask(me.doLoad, me, [true, false]);
me.updateParams();
view.task = Ext.TaskManager.start({
run: function() {
if (!view.isVisible() || !view.scrollToEnd || !viewmodel.get('livemode')) {

View File

@ -1,13 +1,13 @@
/*
* Display log entries in a panel with scrollbar
* The log entries are automatically refreshed via a background task,
* with newest entries coming at the bottom
* with newest entries comming at the bottom
*/
Ext.define('Proxmox.panel.LogView', {
extend: 'Ext.panel.Panel',
xtype: 'proxmoxLogView',
pageSize: 510,
pageSize: 500,
viewBuffer: 50,
lineHeight: 16,
@ -22,28 +22,19 @@ Ext.define('Proxmox.panel.LogView', {
updateParams: function() {
let me = this;
let viewModel = me.getViewModel();
if (viewModel.get('hide_timespan') || viewModel.get('livemode')) {
return;
}
let since = viewModel.get('since');
let until = viewModel.get('until');
if (viewModel.get('hide_timespan')) {
return;
}
if (since > until) {
Ext.Msg.alert('Error', 'Since date must be less equal than Until date.');
return;
}
let submitFormat = viewModel.get('submitFormat');
viewModel.set('params.since', Ext.Date.format(since, submitFormat));
if (submitFormat === 'Y-m-d') {
viewModel.set('params.until', Ext.Date.format(until, submitFormat) + ' 23:59:59');
} else {
viewModel.set('params.until', Ext.Date.format(until, submitFormat));
}
viewModel.set('params.since', Ext.Date.format(since, 'Y-m-d'));
viewModel.set('params.until', Ext.Date.format(until, 'Y-m-d') + ' 23:59:59');
me.getView().loadTask.delay(200);
},
@ -54,42 +45,29 @@ Ext.define('Proxmox.panel.LogView', {
return maxPos - pos;
},
updateView: function(lines, first, total) {
updateView: function(text, first, total) {
let me = this;
let view = me.getView();
let viewModel = me.getViewModel();
let content = me.lookup('content');
let data = viewModel.get('data');
if (first === data.first && total === data.total && lines.length === data.lines) {
// before there is any real output, we get 'no output' as a single line, so always
// update if we only have one to be sure to catch the first real line of output
if (total !== 1) {
return; // same content, skip setting and scrolling
}
if (first === data.first && total === data.total && text.length === data.textlen) {
return; // same content, skip setting and scrolling
}
viewModel.set('data', {
first: first,
total: total,
lines: lines.length,
textlen: text.length,
});
let scrollPos = me.scrollPosBottom();
let scrollToBottom = view.scrollToEnd && scrollPos <= 5;
if (!scrollToBottom) {
// so that we have the 'correct' height for the text
lines.length = total;
}
content.update(text);
content.update(lines.join('<br>'));
if (scrollToBottom) {
let scroller = view.getScrollable();
scroller.suspendEvent('scroll');
view.scrollTo(0, Infinity);
me.updateStart(true);
scroller.resumeEvent('scroll');
if (view.scrollToEnd && scrollPos <= 5) {
// we use setTimeout to work around scroll handling on touchscreens
setTimeout(function() { view.scrollTo(0, Infinity); }, 10);
}
},
@ -107,9 +85,6 @@ Ext.define('Proxmox.panel.LogView', {
params: viewModel.get('params'),
method: 'GET',
success: function(response) {
if (me.isDestroyed) {
return;
}
Proxmox.Utils.setErrorMask(me, false);
let total = response.result.total;
let lines = [];
@ -122,7 +97,8 @@ Ext.define('Proxmox.panel.LogView', {
lines[line.n - 1] = Ext.htmlEncode(line.t);
});
me.updateView(lines, first - 1, total);
lines.length = total;
me.updateView(lines.join('<br>'), first - 1, total);
me.running = false;
if (me.requested) {
me.requested = false;
@ -145,64 +121,25 @@ Ext.define('Proxmox.panel.LogView', {
});
},
updateStart: function(scrolledToBottom, targetLine) {
let me = this;
let view = me.getView(), viewModel = me.getViewModel();
let limit = viewModel.get('params.limit');
let total = viewModel.get('data.total');
// heuristic: scroll up? -> load more in front; scroll down? -> load more at end
let startRatio = view.lastTargetLine && view.lastTargetLine > targetLine ? 2/3 : 1/3;
view.lastTargetLine = targetLine;
let newStart = scrolledToBottom
? Math.trunc(total - limit, 10)
: Math.trunc(targetLine - (startRatio * limit) + 10);
viewModel.set('params.start', Math.max(newStart, 0));
view.loadTask.delay(200);
},
onScroll: function(x, y) {
let me = this;
let view = me.getView(), viewModel = me.getViewModel();
let line = view.getScrollY() / view.lineHeight;
let viewLines = view.getHeight() / view.lineHeight;
let viewStart = Math.max(Math.trunc(line - 1 - view.viewBuffer), 0);
let viewEnd = Math.trunc(line + viewLines + 1 + view.viewBuffer);
let { start, limit } = viewModel.get('params');
let margin = start < 20 ? 0 : 20;
if (viewStart < start + margin || viewEnd > start + limit - margin) {
me.updateStart(false, line);
}
},
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);
},
let viewModel = me.getViewModel();
onTimespan: function() {
let me = this;
me.getViewModel().set('livemode', false);
me.updateView([], false);
// Directly apply currently selected values without update
// button click.
me.updateParams();
let lineHeight = view.lineHeight;
let line = view.getScrollY()/lineHeight;
let start = viewModel.get('params.start');
let limit = viewModel.get('params.limit');
let viewLines = view.getHeight()/lineHeight;
let viewStart = Math.max(parseInt(line - 1 - view.viewBuffer, 10), 0);
let viewEnd = parseInt(line + viewLines + 1 + view.viewBuffer, 10);
if (viewStart < start || viewEnd > start+limit) {
viewModel.set('params.start',
Math.max(parseInt(line - (limit / 2) + 10, 10), 0));
view.loadTask.delay(200);
}
},
init: function(view) {
@ -219,17 +156,17 @@ 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`);
me.lookup('content').setStyle('line-height', view.lineHeight + 'px');
view.loadTask = new Ext.util.DelayedTask(me.doLoad, me);
me.updateParams();
view.task = Ext.TaskManager.start({
run: () => {
run: function() {
if (!view.isVisible() || !view.scrollToEnd) {
return;
}
if (me.scrollPosBottom() <= 5) {
view.loadTask.delay(200);
}
@ -255,8 +192,6 @@ Ext.define('Proxmox.panel.LogView', {
data: {
until: null,
since: null,
submitFormat: 'Y-m-d',
livemode: true,
hide_timespan: false,
data: {
start: 0,
@ -265,7 +200,7 @@ Ext.define('Proxmox.panel.LogView', {
},
params: {
start: 0,
limit: 510,
limit: 500,
},
},
},
@ -276,8 +211,9 @@ Ext.define('Proxmox.panel.LogView', {
x: 'auto',
y: 'auto',
listeners: {
// we have to have this here, since we cannot listen to events of the scroller in
// the viewcontroller (extjs bug?), nor does the panel have a 'scroll' event'
// we have to have this here, since we cannot listen to events
// of the scroller in the viewcontroller (extjs bug?), nor does
// the panel have a 'scroll' event'
scroll: {
fn: function(scroller, x, y) {
let controller = this.component.getController();
@ -296,70 +232,32 @@ Ext.define('Proxmox.panel.LogView', {
},
items: [
'->',
'Since: ',
{
xtype: 'segmentedbutton',
items: [
{
text: gettext('Live Mode'),
bind: {
pressed: '{livemode}',
},
handler: 'onLiveMode',
},
{
text: gettext('Select Timespan'),
bind: {
pressed: '{!livemode}',
},
handler: 'onTimespan',
},
],
},
{
xtype: 'box',
autoEl: { cn: gettext('Since') + ':' },
bind: {
disabled: '{livemode}',
},
},
{
xtype: 'proxmoxDateTimeField',
xtype: 'datefield',
name: 'since_date',
reference: 'since',
format: 'Y-m-d',
bind: {
disabled: '{livemode}',
value: '{since}',
maxValue: '{until}',
submitFormat: '{submitFormat}',
},
},
'Until: ',
{
xtype: 'box',
autoEl: { cn: gettext('Until') + ':' },
bind: {
disabled: '{livemode}',
},
},
{
xtype: 'proxmoxDateTimeField',
xtype: 'datefield',
name: 'until_date',
reference: 'until',
format: 'Y-m-d',
bind: {
disabled: '{livemode}',
value: '{until}',
minValue: '{since}',
submitFormat: '{submitFormat}',
},
},
{
xtype: 'button',
text: 'Update',
handler: 'updateParams',
bind: {
disabled: '{livemode}',
},
},
],
},

View File

@ -1,102 +0,0 @@
Ext.define('Proxmox.widget.NodeInfoRepoStatus', {
extend: 'Proxmox.widget.Info',
alias: 'widget.pmxNodeInfoRepoStatus',
title: gettext('Repository Status'),
colspan: 2,
printBar: false,
product: undefined,
repoLink: undefined,
viewModel: {
data: {
subscriptionActive: '',
noSubscriptionRepo: '',
enterpriseRepo: '',
testRepo: '',
},
formulas: {
repoStatus: function(get) {
if (get('subscriptionActive') === '' || get('enterpriseRepo') === '') {
return '';
}
if (get('noSubscriptionRepo') || get('testRepo')) {
return 'non-production';
} else if (get('subscriptionActive') && get('enterpriseRepo')) {
return 'ok';
} else if (!get('subscriptionActive') && get('enterpriseRepo')) {
return 'no-sub';
} else if (!get('enterpriseRepo') || !get('noSubscriptionRepo') || !get('testRepo')) {
return 'no-repo';
}
return 'unknown';
},
repoStatusMessage: function(get) {
let me = this;
let view = me.getView();
const status = get('repoStatus');
let repoLink = ` <a data-qtip="${gettext("Open Repositories Panel")}"
href="${view.repoLink}">
<i class="fa black fa-chevron-right txt-shadow-hover"></i>
</a>`;
return Proxmox.Utils.formatNodeRepoStatus(status, view.product) + repoLink;
},
},
},
setValue: function(value) { // for binding below
this.updateValue(value);
},
bind: {
value: '{repoStatusMessage}',
},
setRepositoryInfo: function(standardRepos) {
let me = this;
let vm = me.getViewModel();
for (const standardRepo of standardRepos) {
const handle = standardRepo.handle;
const status = standardRepo.status || 0;
if (handle === "enterprise") {
vm.set('enterpriseRepo', status);
} else if (handle === "no-subscription") {
vm.set('noSubscriptionRepo', status);
} else if (handle === "test") {
vm.set('testRepo', status);
}
}
},
setSubscriptionStatus: function(status) {
let me = this;
let vm = me.getViewModel();
vm.set('subscriptionActive', status);
},
initComponent: function() {
let me = this;
if (me.product === undefined) {
throw "no product name provided";
}
if (me.repoLink === undefined) {
throw "no repo link href provided";
}
me.callParent();
},
});

View File

@ -1,172 +0,0 @@
Ext.define('Proxmox.panel.NotesView', {
extend: 'Ext.panel.Panel',
xtype: 'pmxNotesView',
mixins: ['Proxmox.Mixin.CBind'],
title: gettext("Notes"),
bodyPadding: 10,
scrollable: true,
animCollapse: false,
collapseFirst: false,
maxLength: 64 * 1024,
enableTBar: false,
onlineHelp: 'markdown_basics',
tbar: {
itemId: 'tbar',
hidden: true,
items: [
{
text: gettext('Edit'),
iconCls: 'fa fa-pencil-square-o',
handler: function() {
let view = this.up('panel');
view.run_editor();
},
},
],
},
cbindData: function(initalConfig) {
let me = this;
let type = '';
if (me.node) {
me.url = `/api2/extjs/nodes/${me.node}/config`;
} else if (me.pveSelNode?.data?.id === 'root') {
me.url = '/api2/extjs/cluster/options';
type = me.pveSelNode?.data?.type;
} else {
const nodename = me.pveSelNode?.data?.node;
type = me.pveSelNode?.data?.type;
if (!nodename) {
throw "no node name specified";
}
if (!Ext.Array.contains(['node', 'qemu', 'lxc'], type)) {
throw 'invalid type specified';
}
const vmid = me.pveSelNode?.data?.vmid;
if (!vmid && type !== 'node') {
throw "no VM ID specified";
}
me.url = `/api2/extjs/nodes/${nodename}/`;
// add the type specific path if qemu/lxc and set the backend's maxLen
if (type === 'qemu' || type === 'lxc') {
me.url += `${type}/${vmid}/`;
me.maxLength = 8 * 1024;
}
me.url += 'config';
}
me.pveType = type;
me.load();
return {};
},
run_editor: function() {
let me = this;
Ext.create('Proxmox.window.NotesEdit', {
url: me.url,
onlineHelp: me.onlineHelp,
listeners: {
destroy: () => me.load(),
},
autoShow: true,
}).setMaxLength(me.maxLength);
},
setNotes: function(value = '') {
let me = this;
let mdHtml = Proxmox.Markdown.parse(value);
me.update(mdHtml);
if (me.collapsible && me.collapseMode === 'auto') {
me.setCollapsed(!value);
}
},
load: function() {
let me = this;
Proxmox.Utils.API2Request({
url: me.url,
waitMsgTarget: me,
failure: (response, opts) => {
me.update(gettext('Error') + " " + response.htmlStatus);
me.setCollapsed(false);
},
success: ({ result }) => me.setNotes(result.data.description),
});
},
listeners: {
render: function(c) {
let me = this;
let sp = Ext.state.Manager.getProvider();
// to cover live changes to the browser setting
me.mon(sp, 'statechange', function(provider, key, value) {
if (value === null || key !== 'edit-notes-on-double-click') {
return;
}
if (value) {
me.getEl().on('dblclick', me.run_editor, me);
} else {
// there's only the me.run_editor listener, and removing just that did not work
me.getEl().clearListeners();
}
});
// to cover initial setting value
if (sp.get('edit-notes-on-double-click', false)) {
me.getEl().on('dblclick', me.run_editor, me);
}
},
afterlayout: function() {
let me = this;
if (me.collapsible && !me.getCollapsed() && me.collapseMode === 'always') {
me.setCollapsed(true);
me.collapseMode = ''; // only once, on initial load!
}
},
},
tools: [
{
glyph: 'xf044@FontAwesome', // fa-pencil-square-o
tooltip: gettext('Edit notes'),
callback: view => view.run_editor(),
style: {
paddingRight: '5px',
},
},
],
initComponent: function() {
let me = this;
me.callParent();
// '' is for datacenter
if (me.enableTBar === true || me.pveType === 'node' || me.pveType === '') {
me.down('#tbar').setVisible(true);
} else if (me.pveSelNode?.data?.template !== 1) {
me.setCollapsible(true);
me.collapseDirection = 'right';
let sp = Ext.state.Manager.getProvider();
me.collapseMode = sp.get('guest-notes-collapse', 'never');
if (me.collapseMode === 'auto') {
me.setCollapsed(true);
}
}
},
});

View File

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

View File

@ -1,6 +1,3 @@
// override the download server url globally, for privacy reasons
Ext.draw.Container.prototype.defaultDownloadServerUrl = "-";
Ext.define('Proxmox.chart.axis.segmenter.NumericBase2', {
extend: 'Ext.chart.axis.segmenter.Numeric',
alias: 'segmenter.numericBase2',
@ -64,9 +61,6 @@ Ext.define('Proxmox.widget.RRDChart', {
powerOfTwo: false,
// set to empty string to suppress warning in debug mode
downloadServerUrl: '-',
controller: {
xclass: 'Ext.app.ViewController',
@ -92,7 +86,7 @@ Ext.define('Proxmox.widget.RRDChart', {
value = Ext.util.Format.number(value, format);
let unit = units[si];
if (unit && this.powerOfTwo) unit += 'i';
if (this.powerOfTwo) unit += 'i';
return `${value.toString()} ${unit}`;
},
@ -124,9 +118,6 @@ Ext.define('Proxmox.widget.RRDChart', {
},
onAfterAnimation: function(chart, eopts) {
if (!chart.header || !chart.header.tools) {
return;
}
// if the undo button is disabled, disable our tool
let ourUndoZoomButton = chart.header.tools[0];
let undoButton = chart.interactions[0].getUndoButton();
@ -143,21 +134,10 @@ Ext.define('Proxmox.widget.RRDChart', {
},
],
legend: {
type: 'dom',
padding: 0,
},
listeners: {
redraw: {
fn: 'onAfterAnimation',
options: {
buffer: 500,
},
},
},
touchAction: {
panX: true,
panY: true,
animationend: 'onAfterAnimation',
},
constructor: function(config) {
@ -183,27 +163,6 @@ Ext.define('Proxmox.widget.RRDChart', {
me.callParent([config]);
},
checkThemeColors: function() {
let me = this;
let rootStyle = getComputedStyle(document.documentElement);
// get colors
let background = rootStyle.getPropertyValue("--pwt-panel-background").trim() || "#ffffff";
let text = rootStyle.getPropertyValue("--pwt-text-color").trim() || "#000000";
let primary = rootStyle.getPropertyValue("--pwt-chart-primary").trim() || "#000000";
let gridStroke = rootStyle.getPropertyValue("--pwt-chart-grid-stroke").trim() || "#dddddd";
// set the colors
me.setBackground(background);
me.axes.forEach((axis) => {
axis.setLabel({ color: text });
axis.setTitle({ color: text });
axis.setStyle({ strokeStyle: primary });
axis.setGrid({ stroke: gridStroke });
});
me.redraw();
},
initComponent: function() {
let me = this;
@ -238,7 +197,6 @@ Ext.define('Proxmox.widget.RRDChart', {
if (me.header && me.legend) {
me.header.padding = '4 9 4';
me.header.add(me.legend);
me.legend = undefined;
}
if (!me.noTool) {
@ -275,6 +233,10 @@ Ext.define('Proxmox.widget.RRDChart', {
marker: {
opacity: 0,
scaling: 0.01,
fx: {
duration: 200,
easing: 'easeOut',
},
},
highlightCfg: {
opacity: 1,
@ -291,26 +253,7 @@ Ext.define('Proxmox.widget.RRDChart', {
// enable animation after the store is loaded
me.store.onAfter('load', function() {
me.setAnimation({
duration: 200,
easing: 'easeIn',
});
me.setAnimation(true);
}, this, { single: true });
me.checkThemeColors();
// switch colors on media query changes
me.mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
me.themeListener = (e) => { me.checkThemeColors(); };
me.mediaQueryList.addEventListener("change", me.themeListener);
},
doDestroy: function() {
let me = this;
me.mediaQueryList.removeEventListener("change", me.themeListener);
me.callParent();
},
});

View File

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

View File

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

View File

@ -47,7 +47,7 @@ Ext.define('Proxmox.panel.StatusView', {
*/
if (used.used !== undefined &&
used.total !== undefined) {
return used.total > 0 ? used.used/used.total : 0;
return used.used/used.total;
}
}

View File

@ -1,304 +0,0 @@
Ext.define('pmx-tfa-users', {
extend: 'Ext.data.Model',
fields: ['userid'],
idProperty: 'userid',
proxy: {
type: 'proxmox',
url: '/api2/json/access/tfa',
},
});
Ext.define('pmx-tfa-entry', {
extend: 'Ext.data.Model',
fields: ['fullid', 'userid', 'type', 'description', 'created', 'enable'],
idProperty: 'fullid',
});
Ext.define('Proxmox.panel.TfaView', {
extend: 'Ext.grid.GridPanel',
alias: 'widget.pmxTfaView',
mixins: ['Proxmox.Mixin.CBind'],
title: gettext('Second Factors'),
reference: 'tfaview',
issuerName: 'Proxmox',
yubicoEnabled: false,
cbindData: function(initialConfig) {
let me = this;
return {
yubicoEnabled: me.yubicoEnabled,
};
},
store: {
type: 'diff',
autoDestroy: true,
autoDestroyRstore: true,
model: 'pmx-tfa-entry',
rstore: {
type: 'store',
proxy: 'memory',
storeid: 'pmx-tfa-entry',
model: 'pmx-tfa-entry',
},
},
controller: {
xclass: 'Ext.app.ViewController',
init: function(view) {
let me = this;
view.tfaStore = Ext.create('Proxmox.data.UpdateStore', {
autoStart: true,
interval: 5 * 1000,
storeid: 'pmx-tfa-users',
model: 'pmx-tfa-users',
});
view.tfaStore.on('load', this.onLoad, this);
view.on('destroy', view.tfaStore.stopUpdate);
Proxmox.Utils.monStoreErrors(view, view.tfaStore);
},
reload: function() { this.getView().tfaStore.load(); },
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}`,
userid: user.id,
type: entry.type,
description: entry.description,
created: entry.created,
enable: entry.enable,
locked: tfa_locked || (entry.type === 'totp' && totp_locked),
});
});
});
let rstore = this.getView().store.rstore;
rstore.loadData(records);
rstore.fireEvent('load', rstore, records, true);
},
addTotp: function() {
let me = this;
Ext.create('Proxmox.window.AddTotp', {
isCreate: true,
issuerName: me.getView().issuerName,
listeners: {
destroy: function() {
me.reload();
},
},
}).show();
},
addWebauthn: function() {
let me = this;
Ext.create('Proxmox.window.AddWebauthn', {
isCreate: true,
autoShow: true,
listeners: {
destroy: () => me.reload(),
},
});
},
addRecovery: async function() {
let me = this;
Ext.create('Proxmox.window.AddTfaRecovery', {
autoShow: true,
listeners: {
destroy: () => me.reload(),
},
});
},
addYubico: function() {
let me = this;
Ext.create('Proxmox.window.AddYubico', {
isCreate: true,
autoShow: true,
listeners: {
destroy: () => me.reload(),
},
});
},
editItem: function() {
let me = this;
let view = me.getView();
let selection = view.getSelection();
if (selection.length !== 1 || selection[0].id.endsWith("/recovery")) {
return;
}
Ext.create('Proxmox.window.TfaEdit', {
'tfa-id': selection[0].data.fullid,
autoShow: true,
listeners: {
destroy: () => me.reload(),
},
});
},
renderUser: fullid => fullid.split('/')[0],
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);
}
},
onRemoveButton: function(btn, event, record) {
let me = this;
Ext.create('Proxmox.tfa.confirmRemove', {
...record.data,
callback: password => me.removeItem(password, record),
autoShow: true,
});
},
removeItem: async function(password, record) {
let me = this;
if (password !== null) {
password = '?password=' + encodeURIComponent(password);
} else {
password = '';
}
try {
me.getView().mask(gettext('Please wait...'), 'x-mask-loading');
await Proxmox.Async.api2({
url: `/api2/extjs/access/tfa/${record.id}${password}`,
method: 'DELETE',
});
me.reload();
} catch (response) {
Ext.Msg.alert(gettext('Error'), response.result.message);
} finally {
me.getView().unmask();
}
},
},
viewConfig: {
trackOver: false,
},
listeners: {
itemdblclick: 'editItem',
},
columns: [
{
header: gettext('User'),
width: 200,
sortable: true,
dataIndex: 'fullid',
renderer: 'renderUser',
},
{
header: gettext('Enabled'),
width: 80,
sortable: true,
dataIndex: 'enable',
renderer: 'renderEnabled',
},
{
header: gettext('TFA Type'),
width: 80,
sortable: true,
dataIndex: 'type',
},
{
header: gettext('Created'),
width: 150,
sortable: true,
dataIndex: 'created',
renderer: t => !t ? 'N/A' : Proxmox.Utils.render_timestamp(t),
},
{
header: gettext('Description'),
width: 300,
sortable: true,
dataIndex: 'description',
renderer: Ext.String.htmlEncode,
flex: 1,
},
],
tbar: [
{
text: gettext('Add'),
cbind: {},
menu: {
xtype: 'menu',
items: [
{
text: gettext('TOTP'),
itemId: 'totp',
iconCls: 'fa fa-fw fa-clock-o',
handler: 'addTotp',
},
{
text: gettext('WebAuthn'),
itemId: 'webauthn',
iconCls: 'fa fa-fw fa-shield',
handler: 'addWebauthn',
},
{
text: gettext('Recovery Keys'),
itemId: 'recovery',
iconCls: 'fa fa-fw fa-file-text-o',
handler: 'addRecovery',
},
{
text: gettext('Yubico OTP'),
itemId: 'yubico',
iconCls: 'fa fa-fw fa-yahoo', // close enough
handler: 'addYubico',
cbind: {
hidden: '{!yubicoEnabled}',
},
},
],
},
},
'-',
{
xtype: 'proxmoxButton',
text: gettext('Edit'),
handler: 'editItem',
enableFn: rec => !rec.id.endsWith("/recovery"),
disabled: true,
},
{
xtype: 'proxmoxButton',
disabled: true,
text: gettext('Remove'),
getRecordName: rec => rec.data.description,
handler: 'onRemoveButton',
},
],
});

View File

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

View File

@ -1,47 +0,0 @@
include ../defines.mk
SCSSSRC=scss/ProxmoxDark.scss \
scss/abstracts/_mixins.scss \
scss/abstracts/_variables.scss \
scss/extjs/_body.scss \
scss/extjs/form/_button.scss \
scss/extjs/form/_combobox.scss \
scss/extjs/form/_formfield.scss \
scss/extjs/_grid.scss \
scss/extjs/_menu.scss \
scss/extjs/_panel.scss \
scss/extjs/_presentation.scss \
scss/extjs/_progress.scss \
scss/extjs/_splitter.scss \
scss/extjs/_tabbar.scss \
scss/extjs/_tip.scss \
scss/extjs/_toolbar.scss \
scss/extjs/_treepanel.scss \
scss/extjs/_window.scss \
scss/other/_charts.scss \
scss/other/_icons.scss \
scss/proxmox/_general.scss \
scss/proxmox/_helpbutton.scss \
scss/proxmox/_loadingindicator.scss \
scss/proxmox/_markdown.scss \
scss/proxmox/_nodes.scss \
scss/proxmox/_quarantine.scss \
scss/proxmox/_storages.scss \
scss/proxmox/_tags.scss \
scss/proxmox/_datepicker.scss
.PHONY: all
all: theme-proxmox-dark.css
.PHONY: install
install: theme-proxmox-dark.css
install -d $(WWWTHEMEDIR)/
install -m 0664 theme-proxmox-dark.css $(WWWTHEMEDIR)/
theme-proxmox-dark.css: $(SCSSSRC)
sassc -t compressed $< $@.tmp
mv $@.tmp $@
.PHONY: clean
clean:
rm -rf theme-proxmox-dark.css

View File

@ -1,37 +0,0 @@
@charset "utf-8";
// Abstracts
@import "abstracts/mixins";
@import "abstracts/variables";
// Chart, Icon, Keyboar-mode fixups
@import "other/charts";
@import "other/icons";
// ExtJS re-stylings
@import "extjs/form/button";
@import "extjs/form/combobox";
@import "extjs/form/formfield";
@import "extjs/grid";
@import "extjs/menu";
@import "extjs/panel";
@import "extjs/progress";
@import "extjs/splitter";
@import "extjs/tabbar";
@import "extjs/tip";
@import "extjs/toolbar";
@import "extjs/treepanel";
@import "extjs/window";
@import "extjs/body";
@import "extjs/presentation";
// Proxmox re-stylings
@import "proxmox/general";
@import "proxmox/helpbutton";
@import "proxmox/loadingindicator";
@import "proxmox/markdown";
@import "proxmox/nodes";
@import "proxmox/quarantine";
@import "proxmox/storages";
@import "proxmox/tags";
@import "proxmox/datepicker";

View File

@ -1,5 +0,0 @@
// selected items in dropdown etc
@mixin selection {
background-color: $selection-background-color;
color: $selection-background-text-color;
}

View File

@ -1,67 +0,0 @@
// Primary colors
$primary-color: hsl(205deg, 100%, 32.25%);
$primary-light: hsl(205deg, 100%, 40.5%);
$primary-dark: hsl(205deg, 100%, 25%);
// Hightlighted Text (Links, Headers, etc.)
$highlighted-text: hsl(205deg, 100%, 65%);
$highlighted-text-alt: hsl(205deg, 100%, 80%);
$highlighted-text-crit: hsl(360deg, 100%, 65%);
// Icon and Text colors
$text-color: hsl(0deg, 0%, 95%);
$text-color-inactive: hsl(0deg, 0%, 60%);
$icon-color: hsl(0deg, 0%, 90%);
$icon-color-alt: hsl(0deg, 0%, 55%);
// Borders
$border-color: hsl(0deg, 0%, 40%);
$border-color-alt: hsl(0deg, 0%, 25%);
// Backgrounds
$content-background-color: hsl(0deg, 0%, 20%);
$content-background-selected: hsl(0deg, 0%, 30%);
$background-dark: hsl(0deg, 0%, 20%);
$background-darker: hsl(0deg, 0%, 15%);
$background-darkest: hsl(0deg, 0%, 10%);
$background-invalid: hsl(360deg, 60%, 20%);
$background-warning: hsl(40deg, 100%, 20%);
// Buttons
$neutral-button-color: hsl(0deg, 0%, 25%);
$neutral-button-color-alt: hsl(0deg, 0%, 35%);
$neutral-button-text-color: hsl(0deg, 0%, 95%);
$neutral-button-icon-color: $neutral-button-text-color;
// Help Buttons
$help-button-color: hsl(0deg, 0%, 65%);
$help-button-color-alt: hsl(0deg, 0%, 75%);
$help-button-text-color: hsl(0deg, 0%, 10%);
$help-button-icon-color: $help-button-text-color;
// Selection Colors
$selection-background-color: hsl(0deg, 0%, 35%);
$selection-background-text-color: hsl(0deg, 0%, 100%);
// Other
$form-field-body-color: $background-dark;
$bottom-splitter-color: hsl(0deg, 0%, 5%);
// Some icons are black and do not respect the 'color' style property.
// For the dark mode these can be turned grey or white with the
// 'filter: invert(value)' attribute
$icon-brightness: lightness($icon-color);
// Spam score colors
// for spam scores with an absolute score >= 3
$spam-high-neg: hsl(205deg, 65%, 20%);
$spam-high-pos: hsl(360deg, 55%, 20%);
// for spam scores with an absolute score between 0.1 and 3
$spam-mid-neg: hsl(205deg, 65%, 30%);
$spam-mid-pos: hsl(360deg, 55%, 30%);
// for spam scores with an absolute score <= 0.1
$spam-low-neg: hsl(205deg, 65%, 40%);
$spam-low-pos: hsl(360deg, 55%, 40%);

View File

@ -1,23 +0,0 @@
// Chrome 81, Firefox 96 and Safari 13 support a dark version of the
// scrollbar and form controls source
// https://stackoverflow.com/q/65940522
:root {
color-scheme: dark;
}
.x-body {
color: $text-color;
background-color: $background-darkest;
}
// Should be the absolute background of the document
.x-viewport > .x-body {
background-color: $background-darkest;
}
// necessary for some masks to work properly (e.g. when hidding the
// attachment grid in pmg)
body.x-border-layout-ct,
div.x-border-layout-ct {
background-color: $background-darkest;
}

View File

@ -1,148 +0,0 @@
.x-column-header {
border-color: $border-color-alt;
}
.x-grid-item,
.x-column-header-default,
// the row number field (e.g. in the ipsets in pve)
.x-grid-cell-row-numberer {
color: $text-color;
background-color: $background-darker;
}
// Trigger in grid/table header cells
.x-column-header-trigger {
border-color: $border-color;
}
.x-grid-cell-special {
border-color: $border-color-alt;
}
.x-grid-group-hd {
background-color: $background-darker;
border-color: $border-color-alt;
}
.x-grid-group-title {
color: $text-color;
}
// Border-top in tables
.x-grid-header-ct {
border: solid 1px $background-dark;
background-color: $background-dark;
}
// alternating row colors
.x-grid-item-alt {
background-color: $background-darkest;
}
.x-grid-with-row-lines {
.x-grid-item {
border-color: $border-color-alt;
border-right: 0;
// A border at the bottom of tables
&:last-child {
border-color: $border-color-alt;
}
// A border at the top of tables
&:first-child {
border-color: $border-color-alt;
}
// hovered row in a grid
&.x-grid-item-over,
&.x-grid-item-selected {
background-color: $selection-background-color;
border-color: $border-color-alt;
}
}
// borders on selected elements
.x-grid-item-selected + .x-grid-item,
.x-grid-item-over + .x-grid-item {
border-color: $border-color-alt;
}
}
// Sometimes a selected node in the ResourceTree loses the
// selection-background-color
.x-grid-item-over,
.x-grid-item-selected {
// Otherwise .x-grid-item overrides the background color
background-color: $selection-background-color;
}
// Hovering over a grid/table header cell
.x-column-header-over,
// When opening the sort/settings header of a table/grid header cell
.x-column-header-open,
.x-column-header-last .x-column-header-over .x-column-header-trigger, {
background-color: $content-background-selected;
}
// header element that the grid is currently sorted by
.x-column-header-sort-ASC,
.x-column-header-sort-DESC {
background-color: mix($background-darker, $primary-color, 70%);
}
// summary rows (e.g. ceph pools last row)
.x-grid-row-summary {
.x-grid-cell,
.x-grid-rowwrap,
.x-grid-cell-rowbody {
// the "!important" is needed here, because crisp also sets this
// as important
background-color: $background-darker !important;
border-color: $border-color-alt;
}
}
.x-grid-with-col-lines {
.x-grid-cell,
.x-grid-item-over .x-grid-cell,
.x-grid-item-selected .x-grid-cell {
border-color: $border-color-alt;
}
}
// drag and drop proxy
.x-dd-drag-proxy {
background-color: $background-darkest;
border-color: $border-color-alt;
color: $text-color;
}
.x-keyboard-mode .x-grid-item-focused {
@include selection;
.x-grid-cell-inner::before {
border-color: $primary-color;
}
}
// Grid/table headers that are selected and active
.x-keyboard-mode .x-column-header.x-column-header-focus {
color: $text-color;
// Elements in table
.x-column-header-inner::after {
border-color: $primary-color;
}
}
.x-keyboard-mode .proxmox-invalid-row .x-grid-item-focused {
background-color: $background-invalid;
}
// As far as I can tell only used under Node > "System" >
// "Certificates"
.x-grid-empty {
background-color: $background-darker;
color: $text-color;
}

View File

@ -1,40 +0,0 @@
.x-menu-default {
border-color: $form-field-body-color;
}
.x-menu-body-default {
background-color: $form-field-body-color;
}
// E.g. the content menu in the resource tree displays a header
.x-menu-header {
background-color: $primary-color;
}
.x-menu-item-default {
// Horizontal divider in menu (e.g. in UserInfo above "Logout")
&.x-menu-item-separator {
background-color: $background-dark;
border-color: $border-color;
}
// When hovering over a menu item
&.x-menu-item-focus,
&.x-menu-item-active {
@include selection;
}
}
.x-menu-item-text-default {
color: $text-color;
}
.x-menu-item-icon-default {
color: $icon-color;
}
// Vertical divider (e.g. in UserInfo between icons and text)
.x-menu-icon-separator-default {
background-color: $background-dark;
border-color: $border-color;
}

View File

@ -1,58 +0,0 @@
.x-panel-header-default {
background-color: $content-background-color;
border: none;
.x-tool-tool-el {
background-color: transparent;
}
}
// The small navigation elements in the panel header bar e.g. to
// collapse a panel
.x-tool-img {
filter: brightness(175%);
// these are brighter per default, so they don't need to be
// brigthened as much
&.x-tool-expand,
&.x-tool-collapse,
&.x-tool-refresh {
filter: brightness(125%);
}
// this icon uses multiple tones, to have them behave appropriatelly
// invert them before brightening them
&.x-tool-print {
filter: invert(100%) hue-rotate(180deg) brightness(125%);
}
.x-tool-over & {
filter: brightness(200%);
}
.x-tool-over &.x-tool-expand,
.x-tool-over &.x-tool-collapse,
.x-tool-over &.x-tool-refresh {
filter: brightness(150%);
}
.x-tool-over &.x-tool-print {
filter: invert(100%) hue-rotate(180deg) brightness(150%);
}
}
.x-panel-header-title-default {
color: $highlighted-text;
}
.x-panel-body-default {
background-color: $background-darker;
border-color: $border-color-alt;
color: $text-color;
}
// override the border around the pve-resource-tree specifically to be
// more consistent with crisp, while keep allignments "correct"
div[id^="pveResourceTree-"][id$="-body"] {
border-color: $background-darker;
}

View File

@ -1,13 +0,0 @@
// The mask that is applied when the window is unaccessible (Login
// screen, Loading, ...)
.x-mask {
background-color: rgba($background-darker, 0.5);
}
// Shadows of floating windows like window modals, form selectors and
// message boxes
.x-css-shadow {
// the additional styling from the pve css overwrites the setting on
// the element with "!important", that's why we need it here.
box-shadow: black 0 -1px 15px 5px !important;
}

View File

@ -1,19 +0,0 @@
.x-progress-default {
background-color: $form-field-body-color;
.x-progress-bar-default {
background-color: $primary-color; // Taken from the chart
}
.x-progress-text {
color: $text-color;
}
}
.x-progress.warning .x-progress-bar {
background-color: var(--pwt-gauge-warn);
}
.x-progress.critical .x-progress-bar {
background-color: var(--pwt-gauge-crit);
}

View File

@ -1,18 +0,0 @@
// Splitters separating two views (e.g. Firewall > "Security Group",
// "IPSet", ...)
.x-splitter {
background-color: $background-darkest;
}
.x-splitter-horizontal {
background-color: $bottom-splitter-color;
}
// Splitters that separate content and resize parts of the window
.x-keyboard-mode .x-splitter-focus::after {
border-color: $primary-color;
}
.x-layout-split-bottom {
opacity: 0.7;
}

View File

@ -1,45 +0,0 @@
// The header of the tabbar
.x-tab-bar-default {
background-color: $background-darker;
}
.x-tab-default {
// Hovering over a tab button
&.x-tab-over {
background-color: $primary-dark;
border-color: $primary-dark;
}
// Selected tab buttons
&.x-tab.x-tab-active {
background-color: $primary-light;
border-color: $primary-light;
}
// Disabled tab buttons
&.x-tab.x-tab-disabled {
background-color: $background-darker;
// make the border invisible so it matches the light theme, setting
// it to none messes with the allignment of the elements.
border-color: transparent;
color: $text-color;
}
.x-keyboard-mode &.x-tab-focus,
.x-keyboard-mode &.x-tab-focus.x-tab-over,
.x-keyboard-mode &.x-tab-focus.x-tab-active {
background-color: $primary-color;
border-color: $primary-color;
}
}
// Not selected tab buttons
.x-tab-default-top {
background-color: $background-darker;
border-color: $background-darker;
}
.x-tab-inner-default {
color: $text-color;
}

View File

@ -1,18 +0,0 @@
.x-tip-default {
background-color: $background-darkest;
border-color: $border-color-alt;
}
.x-tip-body-default {
color: $text-color;
}
// Form error tip
.x-tip-form-invalid {
background-color: $background-dark;
border-color: $border-color-alt;
}
.x-tip-body-form-invalid {
color: $text-color;
}

Some files were not shown because too many files have changed in this diff Show More