Author SHA1 Message Date
alu 989f59223a reimplement updating instance to loading when power action is taken 2025-05-13 17:35:11 +00:00
alu 233d4255ba fixes #3 by using /status/current for recently modified instances 2025-05-13 17:33:59 +00:00
alu 2b5c1bbf11 various code cleanup,
add required tag to select template
2025-05-12 20:59:48 +00:00
alu 800ad7cd60 cleanup various unused code,
simplify instance action hiding when node is not online
2025-04-29 17:44:02 +00:00
alu bb0d363298 update readme 2025-04-22 17:13:31 +00:00
alu 8530b50f9a implement SSR for instance config page 2025-04-22 17:09:27 +00:00
alu 099f9c4e42 simplify front end routes,
simplify ssr fragment fetch methods
2025-04-15 17:13:22 +00:00
alu 844cf4dfb9 move instance card icon logic to template 2025-04-14 19:24:49 +00:00
alu 02baa71622 update readme 2025-04-09 00:04:40 +00:00
alu 38849c1c41 update go mod, add verbose to make build 2025-04-08 23:58:54 +00:00
alu 455ed353c6 remove commented code,
remove Name field from select using ID instead
2025-04-07 23:17:23 +00:00
alu 13339cc56d improve auth interface, fix issue in meta 2025-04-02 21:18:29 +00:00
alu bd0387976f rename instance to config,
simplify resource-chart code
2025-04-02 20:39:38 +00:00
alu add58d849e implement server side rendering for account page,
remove chartjs module
2025-04-01 17:34:56 +00:00
alu a58620eacb update go mod 2025-03-31 20:41:35 +00:00
alu 79567ea58e reorganize app files 2025-03-27 22:46:54 +00:00
alu b8ebbf6c3d implement server side auth checking,
implement some account server side rendering
2025-03-25 19:24:25 +00:00
alu 75330e8a59 fix bug preventing opening instance console 2025-03-20 19:01:50 +00:00
alu 6e4f2505a2 fix linting 2025-03-13 21:59:36 +00:00
alu 6fdc647413 cleanup unreferenced metadata from utils,
improve setting settings values
2025-03-13 21:19:56 +00:00
alu f37f89d5c7 cleanup code 2025-03-12 23:40:33 +00:00
alu 59d12d2e99 implement partial SSR of instances 2025-03-12 20:16:51 +00:00
alu 8b508d14cc remove extra print statement 2025-03-06 01:09:01 +00:00
alu 247f602359 convert spaces to tabs 2025-03-06 00:45:12 +00:00
alu 209a8cc071 cleanup go mod 2025-03-04 22:42:02 +00:00
alu 9bd1fc8321 switch to gin, add basic server redering to login 2025-03-04 22:40:22 +00:00
alu 323129185b add module preload for each page 2025-02-27 22:13:37 +00:00
alu 7f7e17e8ee add minification 2025-02-27 00:53:19 +00:00
alu 73df998985 add go sum to gitignore 2025-02-25 22:47:53 +00:00
alu cfceb32134 implement basic web server for dashboard,
use templates to do basic SSR on head and header
2025-02-25 21:35:11 +00:00
alu 84cbe0e45d move web source files to web/,
move dev configs to config/
2025-02-24 20:36:51 +00:00
alu a80492605d add hostpci number to devices 2025-02-20 23:34:51 +00:00
alu e73e494f65 improved various error messages 2025-02-10 20:33:46 +00:00
alu 510da85c02 update disk operations to use scsi,
sata still supported for compatibility
2025-02-07 23:39:36 +00:00
alu ce2e8867f3 fix possible error when instance has no name 2025-01-06 20:34:09 +00:00
alu 2170077522 fix confirm password showing up in VM creation form 2024-12-22 07:02:29 +00:00
alu c57e2fe19f remove admin and user page (to be implemented in another project) 2024-12-22 06:59:01 +00:00
alu 48afad63e7 update wfa.wasm 2024-11-13 23:00:31 +00:00
alu e2706ce731 fix linting 2024-11-07 21:27:33 +00:00
alu 7c53cf5efe update wfa module and index search 2024-11-07 21:23:38 +00:00
alu 68fafc1e37 simplify addResourceLine interface,
add update modules script to npm scripts,
add value to DraggableItem
2024-11-07 07:37:16 +00:00
alu 9b3d9767e1 add user form,
implement some admin handling,
improve addResourceLine
2024-10-28 20:03:54 +00:00
alu e2f9f8e352 start implement of admin page 2024-10-03 21:02:31 +00:00
alu dc2dcf85cd update account to api change 2024-10-02 22:58:30 +00:00
alu 7d64715c30 update config to api change 2024-10-01 18:19:59 +00:00
alu f09ca0507e fix style issues 2024-09-27 20:42:41 +00:00
alu 39822819cc fix issue in input grid style 2024-09-05 16:37:57 +00:00
alu 912cee33c7 move addResourceLine to utils,
localize  page specific resource line values in page script
2024-08-31 12:31:29 +00:00
alu 746136724f implement proper match highlighting in vm search feature 2024-08-25 03:09:41 +00:00
alu 07bf4b0967 update search icon link 2024-08-24 04:45:49 +00:00
alu 4b57c5c8e8 move search icon to common 2024-08-13 19:03:11 +00:00
alu 7ec72a7443 simplify buttons in config/index,
include form.css in login
2024-08-12 17:11:44 +00:00
alu 58007a38b2 readd style to svg for external use 2024-08-09 19:59:03 +00:00
alu d574df8410 remove custom background from select,
move select styles to form.css
2024-08-08 19:50:16 +00:00
alu e1ef7998f6 update wfa.js 2024-08-07 19:54:33 +00:00
alu 3a8d9df103 add password confirm to create new container instance 2024-08-05 20:45:37 +00:00
alu b4fd1278cd add config icons to users/groups 2024-08-02 20:14:37 +00:00
alu c11c3579f3 replace wfa.js with v0.0.2 (minified) 2024-07-30 20:52:27 +00:00
alu 9a6a9e4473 move w3.css to modules,
move InstanceCard to index.js,
move ResourceChart to account.js
2024-07-26 18:34:14 +00:00
alu 4a69c58125 switch img icons to svg to fix issues with user selected themes 2024-07-24 19:32:40 +00:00
alu bb3edcb005 add local chartjs module 2024-07-22 21:36:44 +00:00
alu 77a060db6b fix issues in boot order drag and drop selector 2024-07-17 19:49:13 +00:00
alu 6de16ec656 user and group add icons 2024-07-16 19:25:25 +00:00
alu db06522d15 add admin page,
move requestTicket and deleteAllCookies to login.js,
update to new auth paths
2024-07-16 19:00:30 +00:00
alu 019a3a4455 fix duplicate styles 2024-06-24 18:19:02 +00:00
alu f58abd4d4d move local settings to utils 2024-06-17 21:02:15 +00:00
alu d3281fc27f add color theme option 2024-06-14 06:17:15 +00:00
alu d2cfbdb2a2 add missing tabindex to instance buttons 2024-06-12 21:48:29 +00:00
alu 53d820b685 remove width/height from svg icons 2024-06-10 20:04:05 +00:00
alu bbfc77dc04 Merge pull request 'Add fuzzy search using WFA' (#2) from wfa-fuzzy-search into main
Reviewed-on: #2
2024-06-05 22:19:10 +00:00
alu 80ac09e040 add fuzzy search using wfa 2024-06-05 22:11:53 +00:00
alu 199f1b1e99 fix lint 2024-04-27 03:50:29 +00:00
alu 29c36cdbf5 update for new pool and node format 2024-04-16 21:38:25 +00:00
alu a6a76930f9 fix styling 2024-01-29 21:49:19 +00:00
alu e5a0e92ebb fix instance power button prompt 2023-12-23 02:07:25 +00:00
alu 230fe2c9e4 update account page, instance creation form, and config page,
fix issue with instance delete confirm popup,
fix issues with dialog exit handling without form element
2023-11-15 20:18:01 +00:00
alu 7f4bdacef0 fix linting 2023-11-14 22:24:26 +00:00
alu e97f8d5bbc fix bugs in network config change,
generalize dialog layout,
add form validation callback,
add password change form
2023-11-14 00:09:41 +00:00
alu 4806d7f18f fix bug with sync and deleting instance 2023-11-07 05:09:28 +00:00
alu 89f0b14c21 add password form to account,
fix form input grid styling
2023-10-31 18:13:40 +00:00
alu 8cfafe7399 fix nav styling,
fix linting
2023-10-19 17:26:37 +00:00
alu e2f5bf9a0a add bottom padding to main,
improve error handling with request function
2023-10-18 19:33:58 +00:00
alu 09bdf686b3 improve account page loading time 2023-10-17 16:42:33 +00:00
alu c4ddc4ded3 fix handling of instances with empty name,
improve instance table headers
2023-10-16 22:32:19 +00:00
alu 72db810ef0 add local copy of w3.css fallback,
change how the dashboard and api is proxied
2023-10-12 17:27:37 +00:00
alu 9705470798 fix linting issues 2023-10-11 20:50:10 +00:00
alu 63f3aae9e6 remove hover effects for small charts 2023-10-11 00:26:25 +00:00
alu 98a00488a3 improve resource chart interface 2023-10-09 23:30:39 +00:00
alu 4c11714958 update readme 2023-10-05 21:09:31 +00:00
alu a18cc5f39c add alternate chart layout for narrow screens 2023-10-05 16:38:25 +00:00
alu c5c54b0691 fix search regexp escape,
wrap chartjs with custom element
2023-10-04 18:59:46 +00:00
alu c8a0eaf1f1 fix draggable getDragSource docstring 2023-09-29 21:17:34 +00:00
alu a3fb992159 fix nav.css style issue,
make search feature not case sensitive
2023-09-28 01:38:19 +00:00
alu 49b2405814 fix aria-current value for nav bar to "page",
improve nav css readability
2023-09-26 19:15:54 +00:00
alu f179c0f7fb fix config form icon alts 2023-09-21 22:41:16 +00:00
alu bf2509ee8f add visual highlight to matching search query substrings,
add missing aria-label for search input,
fix missing network add icon
2023-09-20 22:17:06 +00:00
alu 497bd60653 fix linting errors 2023-09-19 20:59:28 +00:00
alu 1b71c16eec implement simple search logic 2023-09-18 19:37:07 +00:00
alu 0f8a85d22a finish renaming,
improve search bar and add instance responsiveness
2023-09-15 22:13:21 +00:00
alu 691483b6b4 change name from client to dashboard 2023-09-15 07:02:42 +00:00
alu 8c6e3a6860 add search form layout to main page,
add search icon
2023-09-14 20:38:23 +00:00
alu bab1ab8830 improve draggable item style 2023-09-13 20:03:36 +00:00
alu ea8d2e5b8c update client user resource data format 2023-09-11 19:04:07 +00:00
alu 4f6dc18162 improve draggable-item custom element interface 2023-09-07 21:42:19 +00:00
alu 32106b96ee add autocomplete attribute for login form inputs,
fix config form labels
2023-09-07 02:44:11 +00:00
alu 820fe376d5 improve responsiveness of account resources 2023-09-05 21:47:39 +00:00
alu a3abc49075 fix missing label for rate sync in settings.html 2023-09-02 00:21:03 +00:00
alu 53c8da841d display account resource usage in charts 2023-08-31 19:01:58 +00:00
alu d02a585665 improve draggable item generalization 2023-08-29 21:26:57 +00:00
alu 98e2445979 fix linting 2023-08-23 22:55:48 +00:00
alu e6f622d643 fix bugs with draggable container value,
fix bugs with draggable item value after drop,
fix bugs with boot entry details,
improve addBootLine deleteBootLine updateBootLine
2023-08-22 21:23:14 +00:00
alu 9b42fd236c add drag icon to draggable items 2023-08-19 03:32:52 +00:00
alu 8c328dd22c fix draggable list responsiveness,
add event handlers for adding/removing boot options
2023-08-16 23:14:42 +00:00
alu 2b75732e92 use custom element for instances 2023-08-15 20:27:45 +00:00
alu 820db1058b add draggable list custom element,
improve boot order list
2023-08-14 23:42:02 +00:00
alu 4440330c88 add boot order draggable list mockup 2023-08-11 22:06:26 +00:00
alu 690c8b2938 fix add network and device icon 2023-08-11 03:28:12 +00:00
alu 0fe9200429 add boot order card in config 2023-08-09 21:52:55 +00:00
alu 6cbac6b4fc set title and header in settings page 2023-08-09 00:33:51 +00:00
alu 0ec045a666 use /cluster/* api routes 2023-08-03 00:35:56 +00:00
alu 482d2a512a fix linting errors 2023-07-28 18:32:04 +00:00
alu ddfca3b5ef improve settings page 2023-07-20 22:00:54 +00:00
alu c8919307af change vars.js.template to tempalte.vars.js,
implement interrupt based client sync
2023-07-13 22:53:57 +00:00
alu 995513c9b8 add default sync sheme 2023-07-12 22:46:59 +00:00
alu d25ce49ede implement always and hash sync 2023-07-12 22:37:08 +00:00
alu e83cde0db7 add settings page 2023-07-12 00:24:16 +00:00
alu 55881bdcc0 update sync endpoint 2023-07-11 22:07:08 +00:00
alu 1dd32eda81 implement better HTTP request types handling,
add live updating of instances using statushash endpoint to reduce data transfer
2023-07-11 21:14:54 +00:00
alu defc667041 fix add new instance handler 2023-07-11 00:22:49 +00:00
alu 84d3512417 fix input grid positioning 2023-07-06 05:02:04 +00:00
alu 893f4aad54 use float left on legend and input-grid to remove legend span nesting,
improve linting tool
2023-07-06 04:53:47 +00:00
alu 8a37615721 add pci devices to account 2023-07-05 23:29:14 +00:00
alu 3b23a9043c update client with new user config endpoint paths 2023-07-05 23:16:20 +00:00
alu 4d86104e43 use new endpoint params 2023-07-04 04:41:39 +00:00
alu bbaeac32ea add html validation 2023-06-30 01:21:47 +00:00
alu 240a8d2a73 fix utils.js 2023-06-30 00:34:22 +00:00
alu 8496d9d565 add css linting, fix css linting issues 2023-06-29 22:39:21 +00:00
alu f7dd99e933 remove todo 2023-06-29 22:21:17 +00:00
alu e66d3ceb33 add js linting, fix js linting issues 2023-06-29 22:20:15 +00:00
alu 6ce1cc332a fix nav bar width 2023-06-29 21:38:14 +00:00
alu a679e7a2d9 remove user vlan tag info,
update client with new api route
2023-06-29 02:08:52 +00:00
alu 1359ebcc87 fix pci device pcie form get 2023-06-23 03:34:44 +00:00
alu 59e75659bd fix issues in pci device config and add 2023-06-23 03:03:28 +00:00
alu 8365102ce9 fix bugs in pcie device config dialogs 2023-06-22 00:33:53 +00:00
alu 499d7eee02 add device config/add/delete handlers,
fix some styling with forms
2023-06-19 01:43:52 +00:00
alu 3e50e4cbfe shorten cpu config labels
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-14 22:54:53 +00:00
alu c8d4b466fa populate processor types from config values
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-14 22:34:48 +00:00
alu 0bb82dc81a fix proc type selector issue in config,
update account resource table with new API format
2023-06-14 04:56:49 +00:00
alu 8fdd970a44 add processor type to config 2023-06-14 01:13:06 +00:00
alu 7c1c3fbb8b remove clickable from inactive action buttons 2023-06-13 03:13:22 +00:00
alu 7c3ba9b1ed improve small screen instance list 2023-06-09 07:00:21 +00:00
alu 8eee9fabf1 fix config page styling 2023-06-09 06:55:14 +00:00
alu 55a3574da2 implement network add and delete 2023-06-09 04:25:53 +00:00
alu 1532937f00 add vlan tag to account info 2023-06-09 00:28:26 +00:00
alu 0c05140bfe update config.js to api route changes
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-08 23:33:55 +00:00
alu 7409398e11 add buttons to add and delete network interfaces and pcie devices
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-03 00:25:57 +00:00
alu 2d0b8aada6 fix config pci device population
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-02 23:15:26 +00:00
alu f6d51e742c display device_names in pcie devices config
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-01 17:21:35 +00:00
alu e32c96c41a add pcie device list to instance config
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-30 15:50:32 +00:00
alu ec96b9ec35 fix network name label in config
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-19 20:07:34 +00:00
alu 928d1d1516 fix bugs in api changes
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-18 20:19:57 +00:00
alu bff5ca3929 update account nodes to new route format
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-18 20:08:39 +00:00
alu 3ebd43bfb0 update disk attach for new api change 2023-05-18 08:09:23 +00:00
alu c2670b9f31 fix formatting 2023-05-17 21:43:03 +00:00
alu 5bbf928b2b fix formatting 2023-05-17 21:40:37 +00:00
alu 6a00a22dc2 fix account network usage
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-16 16:51:39 +00:00
alu d10aeedcf2 remove org specific values,
add dynamic org handling

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-16 15:18:36 +00:00
alu d6a7785ab0 improve response error handling,
add handler for network config,
fix import errors

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-15 22:41:54 +00:00
alu b94758c175 fix bug in instance delete
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-15 21:24:43 +00:00
alu 6530240677 add network interface to config,
improve w3-card styling

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-15 21:22:12 +00:00
alu bbc67fa6fb reduce div nesting in instances
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-15 20:41:31 +00:00
alu 9eb8aaa4be consolidate instance.js into index.js
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-15 20:36:28 +00:00
alu cd3027c35e code cleanup
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-15 20:16:38 +00:00
alu d057d98924 fix some styling 2023-05-15 02:56:48 +00:00
alu a3e1d2ed03 use safer pve ticket endpoint 2023-05-13 07:28:09 +00:00
alu 06c6e926ca display errors as modal dialogs
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-12 21:26:26 +00:00
alu be57da94db add margin to table
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-12 21:17:29 +00:00
alu 759571535e add node vmid cores and memory limits to inputs in instance create form
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-12 21:14:49 +00:00
alu c459dad024 fix requestPVE call params 2023-05-11 07:13:41 +00:00
alu a8ad9986d7 remove extra divs for hamburger nav 2023-05-11 06:13:00 +00:00
alu f9e0903671 improve nav bar styling 2023-05-11 06:03:37 +00:00
alu 891b5c3af0 fix add-cd and add-disk
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-10 22:10:23 +00:00
alu 362ba28833 add color scheme styling for svg icons
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-10 22:01:41 +00:00
alu 80bcdc340b add colors for light prefered color scheme
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-10 21:32:41 +00:00
alu c3000f8a3a use vars in more color styles 2023-05-08 02:27:14 +00:00
alu a601dde72b use css vars for colors 2023-05-08 00:50:14 +00:00
alu 512a78bd75 consolidate h1 styles 2023-05-08 00:02:47 +00:00
alu 32f0625770 fix dialog styling 2023-05-07 04:35:44 +00:00
alu e2a6083e22 fix icons and some styling 2023-05-07 04:31:04 +00:00
alu 45d82b1cf3 dark mode for client 2023-05-07 04:15:22 +00:00
alu 92267a731a fix heigh issue with main
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-05 21:44:08 +00:00
alu 822c89adda code cleanup on custom dialogs
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-05 21:43:15 +00:00
alu a0fe199ccb fix responsiveness of account resource table on large screens 2023-05-05 05:03:12 +00:00
alu c677d08637 improve responsiveness for very narrow screens 2023-05-05 05:01:05 +00:00
alu 08fc8d127b add transition to hamburger menu
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-03 22:08:00 +00:00
alu 2f459f7c5b improve hanburger menu style
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-03 21:39:26 +00:00
alu 1fe8491c38 fix login for styling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-03 21:25:55 +00:00
alu 86dd966691 improve account style,
add allowed nodes to account page,
reduce use of innerHTML

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-03 21:06:27 +00:00
alu d351fdfb67 small style improvements 2023-05-02 03:42:58 +00:00
alu 4d4edaa025 improve styling for account page
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-28 23:57:42 +00:00
alu b959d3b872 improve styling for login box and alert box,
delete modal dialog alert box on close

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-28 23:53:38 +00:00
alu fd26f81e30 change some styling for index and config
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-28 23:43:09 +00:00
alu d738b10b2b Update README.md 2023-04-26 16:36:06 -07:00
alu 92493670b7 move nav bar css to nav.css,
simplify some css rules for nav responsiveness

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-25 21:35:50 +00:00
alu 98fe0b2b3f change nav styling and responsiveness
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-25 21:25:03 +00:00
alu 4b83cdaa77 update client with new instance resource api path
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-25 15:32:34 +00:00
alu 938741d429 handle negative resources in account
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-25 15:20:59 +00:00
alu bcc7e0ea57 update client for api changes
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-24 21:47:03 +00:00
alu 4c75398cac adjust some styling 2023-04-22 04:49:17 +00:00
alu 2827f48ce0 improve mobile styling of instance actions 2023-04-22 04:34:48 +00:00
alu 01878b1fe7 fix mobile styling for instance obj 2023-04-22 04:32:09 +00:00
alu 9903f75959 add instance info to account 2023-04-21 22:58:15 +00:00
alu 90d77af0d9 shorten table headers 2023-04-21 22:24:58 +00:00
alu c9b10efed6 fix table responsiveness 2023-04-21 22:22:19 +00:00
alu 1b2b95a1a9 add w3-responsive to table 2023-04-21 22:16:21 +00:00
alu 549e0ae1ab Merge commit 'a9e7a26a9f999624ba9b5410b4b4271f196ca340' 2023-04-20 21:39:09 +00:00
alu 397cb16f48 minor change to user data
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-20 21:38:47 +00:00
alu a9e7a26a9f Update README.md 2023-04-20 14:26:51 -07:00
alu aacc264314 fix login page error handling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-19 21:24:34 +00:00
alu fcdfe020c6 improve error handling for future error display,
fix styling with config disk actions

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-19 21:11:35 +00:00
alu 6d946f08f9 Merge commit '13991f5178c1db90c2a050cf670eba3f1d40b532' 2023-04-19 05:59:01 +00:00
alu 289ca85e81 fix cookie check logic 2023-04-19 05:58:30 +00:00
alu 13991f5178 Update README.md 2023-04-18 22:54:20 -07:00
alu 8f37806280 add units to non compact values 2023-04-19 03:12:50 +00:00
alu 472952d298 add units to resource allocation 2023-04-19 03:10:51 +00:00
alu 94ea16d4f5 fix bug in resource table 2023-04-19 01:42:18 +00:00
alu 115cc51b16 update user db keys 2023-04-19 01:40:55 +00:00
alu a23aad9e99 use new resource return format
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-18 23:31:54 +00:00
alu 9be45c952f improve login style and logic
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-18 20:08:59 +00:00
alu 16be63e98f improve responsive scaling with medium screens
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-12 22:59:57 +00:00
alu ce18b94ea1 improve styling in config form
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-12 22:47:05 +00:00
alu 4ef1cd7480 fix input and select styling for new instance dialog
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-12 22:35:17 +00:00
alu e8908310b6 improve styling for dialogs
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-12 22:23:40 +00:00
alu ad43fa7f73 update dialogs to use w3 styling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-11 22:54:37 +00:00
alu a42c8a8d23 Update README.md 2023-04-11 15:34:32 -07:00
alu 02c744ca61 Update README.md 2023-04-11 15:31:43 -07:00
alu bf1ca1aa70 code cleanup
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-11 21:46:18 +00:00
alu b36cd67e86 fix some mobile responsiveness issues
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-11 21:42:59 +00:00
alu de36f2261a use w3.css for primary styling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-11 21:38:51 +00:00
alu 729f8c0faa add proper account resource usage functionality
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-11 18:01:06 +00:00
alu cabea750db improve login form responsiveness
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 22:31:39 +00:00
alu 15822fdacb better responsive handling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 22:08:11 +00:00
alu 389a548d00 bad fix for responsive instance container
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 21:55:31 +00:00
alu c1ca182d10 fix code style
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 21:26:32 +00:00
alu f89e5ceec6 add icons to instance and node status
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 21:21:11 +00:00
alu 0d4d2d4092 fix image height
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 20:46:40 +00:00
alu 22e8544a85 minor style improvement
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 20:03:15 +00:00
alu c9ce701696 improve some styling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 19:47:40 +00:00
alu e98661cb4a improve font size scaling 2023-04-06 04:43:36 +00:00
alu d3fca0f67c center config form 2023-04-06 04:31:12 +00:00
alu f9137a4f44 fix form input-grid rule 2023-04-06 04:27:34 +00:00
alu a76168d1ab add more info to insatnce container 2023-04-06 04:21:03 +00:00
alu 09bf0ebbe9 simplify styling 2023-04-06 03:46:39 +00:00
alu 9469de72fe working but hacky 2023-04-06 02:56:37 +00:00
alu 4e11110c95 reset padding and margins to be fixed later
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 02:05:57 +00:00
alu 575e7c9739 improve nav bar
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-05 23:39:28 +00:00
alu b552026d81 fix stlye sheet scoping,
imrpove some styling

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-05 23:35:11 +00:00
alu 31042464a4 change resource page to account page,
improve table styling

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-05 22:48:36 +00:00
alu f1dc99dedd rename buttons.css to button.css,
add table styling and prepare for better table generation

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-05 22:29:15 +00:00
alu 8b9cbec5fb use css vars for coloring
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-03 22:59:36 +00:00
alu c742ed7085 make instance article span entire width
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-03 22:49:24 +00:00
alu a2c027eec0 fix site grid layout
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-03 22:09:04 +00:00
alu 8e60ece366 add basic resource viewing
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-03 21:57:03 +00:00
alu 33fe93089e fix remaining icons
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-03 21:17:18 +00:00
alu 3768b2438e fix add instance button color
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-03 21:10:00 +00:00
alu b455f3238f minor style changes,
fix icon colors

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-03 13:40:37 -07:00
alu e4a2bdf205 add nav bar to config page
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-25 23:41:56 +00:00
alu d41162ebae major changes to styling:
move nav bar to left side and implement hover animations,
generalize color scheme using css variables,
move button and btn-group related styles to buttons.css,
consolidate form input and select styles in form.css

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-25 23:38:15 +00:00
alu 42e8d729d6 set default login realm in login page
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-24 22:25:14 +00:00
alu fa66459da7 add realm selector to login
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-24 21:13:33 +00:00
alu b6685696fc Update README.md 2023-02-28 21:09:12 -08:00
alu 1f03f78cc4 Update README.md 2023-02-28 21:03:52 -08:00
alu 6cdb249f8d Update README.md 2023-02-28 20:48:44 -08:00
alu 216f8bede9 fix request functionr return values
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-01 00:01:56 +00:00
alu e72ca340c9 gracefully recover from config error
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-28 23:56:39 +00:00
alu 0caaded1a5 fix request function return value to include status value
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-28 22:50:12 +00:00
alu 2df5284d90 implement instance add,
fix multiple onclick listeners being added

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-27 23:03:19 +00:00
alu fc1972a344 use new endpoint arg format 2023-02-27 02:29:55 +00:00
alu b84186c9d6 remove console.log,
add node logic to new instance form

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-24 23:27:03 +00:00
alu 8e0fa94daa fix removed import
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-24 22:38:31 +00:00
alu 6866b4e8a0 use client api endpont for proxmox requests
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-24 22:25:36 +00:00
alu a7a00043ae fix login form centering
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-23 22:19:32 +00:00
alu a55c782a43 fix login form size
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-23 22:18:11 +00:00
alu 00b1bb310d add node field for add new instance
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-23 22:14:06 +00:00
alu 3116a47416 add extra input fields for container specific options
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-23 21:58:42 +00:00
alu da9dab4443 add create instance form dialog
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-23 21:11:43 +00:00
alu 26dc801fe4 add delete instance logic
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-21 22:50:01 +00:00
alu 01074e87f8 add create instance button
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-21 21:52:57 +00:00
alu b6642c9f0c group action icons by type
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-21 21:50:54 +00:00
alu 00683eddb8 add delete button to insances,
add confirm message on start/stop,
change power action from shutdown to stop,
improve action button alt texts

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-21 19:57:47 +00:00
alu 8dbc9d5552 add saving cpu and memory resource values on exit
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-17 22:17:20 +00:00
alu 17f243a006 use proxmox builtin consoles 2023-02-17 05:12:28 +00:00
alu 604035d3a8 add handle cd add 2023-02-16 05:19:22 +00:00
alu aad2c4df6c comment out console handler
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-16 01:25:17 +00:00
alu 1533b26467 add cd-add button to vm config
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-16 00:57:11 +00:00
alu 725a7baf5e remove xternjs for now
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-16 00:36:13 +00:00
alu ce5eea1dde add disk add for both vm and container
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-15 00:17:35 +00:00
alu 95b7cade56 remove unecessary line
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 22:57:42 +00:00
alu 1149ff5c7a remove appendFormBody
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 22:55:09 +00:00
alu 424f33c840 add create disk dialog form creation
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 21:56:27 +00:00
alu 3e910f8e5d differentiate cancel and confirm buttons in dialog
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 21:13:38 +00:00
alu 3d020fefb9 capitalize all button text
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 21:11:10 +00:00
alu 6b1ed9399c separate dialog and instance custom elements
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 21:08:48 +00:00
alu 1a6e31621e remove unecessary hr id
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 21:04:11 +00:00
alu 5c83cacd45 improve disk dialog creation
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 21:03:53 +00:00
alu cccf01258d remove handler for inactive buttons,
change delete icon

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 20:05:56 +00:00
alu afa0b4838f change disk icon to loading after action triggers 2023-02-14 09:23:37 +00:00
alu bbc88bb5f2 open console in new window 2023-02-14 05:36:21 +00:00
alu 139bc324e8 fix user name by adding username cookie 2023-02-14 05:21:45 +00:00
alu eb26054c74 add basic xternjs functionality 2023-02-14 05:14:33 +00:00
alu d902277635 improve some styling to adapt to different screen sizes
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 01:36:25 +00:00
alu 73f89fa48d add blank.svg,
fix usage of conflicting id values to use dataset
2023-02-13 22:51:22 +00:00
alu f989f7fc13 remove commented code 2023-02-13 04:15:46 +00:00
alu 6c2d6e9ca2 improve power management status update
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-10 22:48:52 +00:00
alu 5d42dbe637 fix lxc handling in disk attach
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-10 22:17:23 +00:00
alu 4496c5f27c implement disk delete (all disk actions finished)
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-10 21:36:47 +00:00
alu efa2585de2 add disk attach
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-10 21:15:00 +00:00
alu b03454402c fix README code formatting 2023-02-06 16:28:58 -08:00
alu 8ed298f571 fix form input labels in login 2023-02-06 21:59:00 +00:00
alu ae49938303 implement disk move delete source
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-02 19:35:14 +00:00
alu 426313a2ae populate handler for disk move
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-02 18:26:44 +00:00
alu 4d66972aae add diskMove handler creation, improve form css
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-02 00:12:36 +00:00
alu 2d061a6650 improve dialog box custom element
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-01 23:42:30 +00:00
alu 1769babac1 add example entries in vars.js.template
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-01 22:26:56 +00:00
alu 7e87c870d3 add button titles to instance
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-01 00:02:10 +00:00
alu f3c46f6f07 add temp fix for long disk size change issue
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-30 23:43:00 +00:00
alu e5601f9f3a fix disk detach and disk resize issues
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-30 23:35:26 +00:00
alu 821d4622f7 add dialog confirm,
add resize handler

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-30 22:21:05 +00:00
alu 9069aba221 add icon to all pages 2023-01-28 03:18:20 +00:00
alu bd8d8a82cf add favicon
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-28 00:30:46 +00:00
alu 04b959bd16 create custom dialog form element
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-27 22:33:05 +00:00
alu f752baa46a change vars export naming
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-27 21:49:50 +00:00
alu b75f390619 simplify power button logic,
add icons for loading instances

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-26 20:50:15 +00:00
alu 8134925c03 avoid refreshing page on disk change
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-26 19:55:17 +00:00
alu 7b712ad80b separate disk and resource population
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-26 19:48:41 +00:00
alu a8f9c67711 remove template in index,
add article content in js

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-26 19:33:39 +00:00
alu 96569d3f88 Update README.md 2023-01-26 01:10:23 -08:00
alu f9691f0857 add detach disk handler 2023-01-25 02:06:31 +00:00
alu 85b816dccd Update README.md 2023-01-23 17:45:03 -08:00
alu 988ed94e13 update README with new instructions 2023-01-20 05:02:50 +00:00
alu 2325dd4611 add template file for vars.js 2023-01-20 04:58:33 +00:00
alu eebc9cfd86 remove vars.js 2023-01-20 04:56:31 +00:00
alu 44f5b2243a update README.md with new instructions
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 20:40:22 +00:00
alu c5d14708d3 untrack vars.js
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 20:33:34 +00:00
alu 2d1d64a765 move api path info to vars.js
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 20:33:05 +00:00
alu c99f6cef2d add config for tronnet, will remove later
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 19:59:12 +00:00
alu 0f9a28ae83 use extra js file to store config data
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 19:58:44 +00:00
alu 8676eb6dc3 change attach and detach icons
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 18:08:00 +00:00
alu 33bdb0b19e change attach and detach icons,
imrpove action bar layout

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 02:23:28 +00:00
alu 63b2bebf2e add action button icons
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 02:13:07 +00:00
alu 62b4fa40e8 add unused disk to disk populate
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 00:54:14 +00:00
alu 727a5dcd81 revert to show disk text config
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 00:35:52 +00:00
alu 96ab6791b2 improve acessibility
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-18 23:43:57 +00:00
alu e3bf24c885 remove padding from first form fieldset
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-18 22:58:00 +00:00
alu dd165d58a5 hide instance actions if the node is offline
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:45:16 -08:00
alu 019ee1b140 set instance to original if request returns an error
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:41:52 -08:00
alu dccb5ec582 fix unknown status check order
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:36:34 -08:00
alu 93490a7e65 fix instance icon assignment
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:34:22 -08:00
alu 72e0407be2 show blank name if instance is unknown,
show stopped icon if instance is unknown

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:33:21 -08:00
alu 6a20785b7c ensure node search returns a node
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:29:10 -08:00
alu 80be88c6b9 fix variable item typo
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:27:15 -08:00
alu 5077dca134 fix finding nodes in resources
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:26:49 -08:00
alu 18b70c4506 fix instance population
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:26:19 -08:00
alu 89d5fcff16 use resource path rather than querying each node
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:24:51 -08:00
alu 7f29719532 remove unrelated comments
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:02:02 -08:00
alu 3b79a997be remove unused function getDiskImageOptions
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:56:20 -08:00
alu 0127b60bf3 keep disk filename prefixes to assign value correctly
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:50:59 -08:00
alu b29b1dd08f remove iso/ prefix from disk filename
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:48:50 -08:00
alu faea8bf08d fix varaible naming for storage in handleStorageChange
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:45:29 -08:00
alu 940ea453d1 fix varaible instantiation
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:44:34 -08:00
alu cd2d93ad34 fix disk image select id assignment
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:43:50 -08:00
alu 977f363934 add storage change handler
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:39:28 -08:00
alu 3784a4511a fix config disk grid layout
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:26:42 -08:00
alu 253342826c use grid-column to span some inputs
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:15:04 -08:00
alu 70d1995453 remove storage and iso from disk image options
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:04:28 -08:00
alu d9ccaddd3c populate diskImageSelect with options
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 10:59:06 -08:00
alu 5ccf122ca6 add options to disk image selection
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 10:48:09 -08:00
alu c0cd6a546e add ids to storage and size input
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 10:31:26 -08:00
alu 453dc9051b set non resiable disk size value to 0
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 10:26:11 -08:00
alu 6cd66db083 fix handleDeviceChange not implemented
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 10:19:07 -08:00
alu 5e03f48ba6 min input content,
disable disk device reassignment for all disks

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 10:18:22 -08:00
alu f3b9a5d81e hide device input if device input is not required
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 10:01:06 -08:00
alu ca59f11bdb add hidden class
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 09:56:48 -08:00
alu 44f837b33d remove unit selector for unit text,
hide size input and unit text if disk is not resizable

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 09:52:20 -08:00
alu ecf593f9ef change volume contents includes to be specific to bus type
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 16:14:33 -08:00
alu b928f87a79 fix min checking
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 16:10:56 -08:00
alu b8fb562c41 fix invalid query selector ids
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 16:08:20 -08:00
alu c21838d760 fix query selector in handleUnitChange
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 16:07:06 -08:00
alu 680bb92242 set minimum size on unit change,
check min  size value on unit change

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 16:06:19 -08:00
alu a4ea5a2a60 disable size unit select if resizable is false
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:50:32 -08:00
alu ef44a7bfbc fix disk config layout
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:48:54 -08:00
alu 28a4dc94cb add required params to addDiskLine
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:48:20 -08:00
alu 7fe8dd2b7e fix variable naming conflict
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:47:13 -08:00
alu 474a9c9222 add selector for size unit
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:45:51 -08:00
alu dfba5cdd5a remove unit from size
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:27:41 -08:00
alu 359bb8a7f3 fix config disk layout
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:25:03 -08:00
alu 75e77b1362 fix delete button alt text
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:24:24 -08:00
alu ba7a96f0eb add delete disk button
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:22:39 -08:00
alu 4512242336 fix storageSelect and sizeInput populating
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:16:02 -08:00
alu f7a11046e0 fix storage options populating
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:14:34 -08:00
alu 32a3c50f97 fix icon alt again
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:13:17 -08:00
alu 5bc908eaae fix bus prefix issue
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:12:46 -08:00
alu 5041f7ea1c fix icon alt text
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:10:24 -08:00
alu fd8025ed32 fix icon reference
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:09:21 -08:00
alu 2ea2865d8d make each disk configurable
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:05:58 -08:00
alu bee34d4569 remove name field
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 14:25:31 -08:00
alu b0b3698258 right align all disk config buttons 2023-01-12 15:26:45 -08:00
alu 24351ded1e add grey backgroudn to disabled inputs 2023-01-12 15:21:08 -08:00
alu 6598ae3133 disable bus when lxc,
disable path when qemu
2023-01-12 15:17:35 -08:00
alu cb9c3a051f remove console.log s 2023-01-12 15:08:45 -08:00
alu 5b111b1114 removed redundant params in populateResources 2023-01-12 15:08:02 -08:00
alu 976b62fce8 add storage target option adding 2023-01-12 15:05:22 -08:00
alu ad84fc9772 add fetch storage options 2023-01-12 14:43:17 -08:00
alu c6f8b7542d fix event listener callback function 2023-01-11 03:02:17 -08:00
alu ba97c056d8 fix disk populating bugs 2023-01-11 03:00:01 -08:00
alu d43363c066 add query select for addDiskDevice 2023-01-11 02:58:22 -08:00
alu 4f8f4ae863 try global vars 2023-01-11 02:57:37 -08:00
alu 7e71d098c4 fix parameters 2023-01-11 02:52:52 -08:00
alu 4556abc8ea consolidate handler params 2023-01-11 02:51:06 -08:00
alu 3993b23d22 add missing type param in add disk handlers 2023-01-11 02:47:36 -08:00
alu da1b0661ca fix missing param in add disk handlers 2023-01-11 02:46:25 -08:00
alu 7799954b0f streamline add disk code 2023-01-11 02:43:56 -08:00
alu 1575fa33fd remoe focus outline 2023-01-11 02:27:10 -08:00
alu 1ce205c17b add correct event listener 2023-01-11 02:16:30 -08:00
alu 847f2d4d64 add red border on add disk device input focus 2023-01-11 02:15:58 -08:00
alu 801bd1e105 populate add disk device with next avaliable value,
add red border around invalid device number
2023-01-11 02:01:34 -08:00
alu f12ee81b3e flip qemu disk ordering 2023-01-11 01:33:50 -08:00
alu 49416cdd34 add pointer to disk items 2023-01-11 01:32:51 -08:00
alu 30c6deb86c generalize button img hover 2023-01-11 01:30:28 -08:00
alu 17836e432d fix form img hover 2023-01-11 01:28:09 -08:00
alu a221643ce4 add hover cursor change 2023-01-11 01:24:28 -08:00
alu 619003616a fixes to limit and bus selection 2023-01-10 17:16:05 -08:00
alu f53882a50b use capital case for disk name 2023-01-10 17:12:24 -08:00
alu dbd6213b86 fix prefix in disk population 2023-01-10 17:11:00 -08:00
alu ffbfc04511 remove all references to disks 2023-01-10 17:09:59 -08:00
alu 60737d1e77 fix populating bug 2023-01-10 17:09:10 -08:00
alu 81da4724e9 fix variable typo 2023-01-10 17:06:34 -08:00
alu d3c4f338cf fix variable name collision 2023-01-10 17:05:41 -08:00
alu 0b139f37c0 used variable to store disk configuration 2023-01-10 17:04:20 -08:00
alu 17676a8564 add logic for bus select and device limit 2023-01-10 16:49:57 -08:00
alu b222f95483 align add disk button to the right 2023-01-10 16:15:44 -08:00
alu 15b2e7ec58 add disk mini form 2023-01-10 16:01:13 -08:00
alu de7f7ed2b5 add some top margin to create disk button 2023-01-10 00:21:48 -08:00
alu 95d735979a add disk create button 2023-01-10 00:18:23 -08:00
alu 392446219d fix disk populating issue 2023-01-09 23:51:11 -08:00
alu 0dee24b337 sort disks in numerical order 2023-01-09 23:50:11 -08:00
alu 39594672bf decrease stop.svg thickness 2023-01-09 23:02:57 -08:00
alu 3de452435c increase stop.svg slightly 2023-01-09 23:00:11 -08:00
alu 5c2a0e53d6 increase stop.svg thickness 2023-01-09 22:59:09 -08:00
alu 7bbff982fe increase start.svg size 2023-01-09 22:54:41 -08:00
alu 56bbfa01f8 use better icon for drive 2023-01-09 22:43:54 -08:00
alu ee19c32f04 use a cleaner drive icon 2023-01-09 22:31:45 -08:00
alu 4c23ac5c5e add logic for disk and drive icon 2023-01-09 22:24:04 -08:00
alu 5b8ab3859d add drive.svg,
add width and height to some svg
2023-01-09 22:19:20 -08:00
alu da87996348 Fix bracket escape in README.md 2023-01-09 18:01:51 -08:00
alu 8a107df67a Create README.md 2023-01-09 17:50:05 -08:00
alu 6adf04efe8 generalize disk populating
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 16:21:45 -08:00
alu cd028002a0 change center div to fit-content
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 16:08:02 -08:00
alu 45e3fb9509 add configurable disk population
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 16:02:25 -08:00
alu e20b952a2c add some margin to nav bar
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 15:29:36 -08:00
alu cf96ffe561 add auth successful message to login
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 15:20:34 -08:00
alu 0740d65363 fix cookie deletion
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 15:16:24 -08:00
alu 9d0e0db27d add deleteAllCookies util function,
add functionality to nav buttons

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 15:08:45 -08:00
alu 78dd24d6cc add border to active button
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 15:01:21 -08:00
alu e38ab19c1d fix some nav styling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 14:54:47 -08:00
alu 58736e6992 use btn-group class for nav bar
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 14:53:29 -08:00
alu 85f3571ef6 fix style bug
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 14:50:52 -08:00
alu 941e135762 fix nav bar positioning
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 14:50:07 -08:00
alu 9fe86b0317 simplify nav heiarchy
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 14:43:48 -08:00
alu ba772cfdbb readd script to index
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 14:37:32 -08:00
alu 788f3e07a7 fix nav styling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 14:36:50 -08:00
alu c2046113a6 add top nav bar
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 14:29:27 -08:00
alu 915450b0b0 fix some styling 2023-01-08 21:46:14 -08:00
alu 594254dac3 fix memory populating bug 2023-01-08 21:32:22 -08:00
alu 80e259b4f7 add label for in name fieldset 2023-01-08 21:28:35 -08:00
alu b572b79798 fix label for assignment,
fix disk icon alt
2023-01-08 21:26:44 -08:00
alu 835b7261aa add image alts in config page,
add for to labels in config page
2023-01-08 21:23:08 -08:00
alu 72ec1c134f add config button to disk entry 2023-01-08 20:49:57 -08:00
alu b78d749df1 add config button to disk items 2023-01-08 20:48:58 -08:00
alu 0a544f87bc add row gap in input grid 2023-01-08 20:38:07 -08:00
alu b95187ee2c populate name field using addMetaLine 2023-01-08 20:32:15 -08:00
alu eb4970344f populate id parameter in addDiskLine 2023-01-08 20:30:09 -08:00
alu 4e15b71d5b fix another variable name conflict 2023-01-08 20:28:50 -08:00
alu 089e52b1a0 fix variable name conflict 2023-01-08 20:28:19 -08:00
alu e4c7f45e6c populate disks in separate fieldset 2023-01-08 20:26:20 -08:00
alu ae3d8c5cf1 improve config styling 2022-12-21 16:27:04 -08:00
alu 70f8a6bbd7 fix form icon style 2022-12-21 16:01:29 -08:00
alu b4a92a26e1 fix icon null check 2022-12-21 16:00:28 -08:00
alu e37c19de77 fix config form icon styling 2022-12-21 15:59:54 -08:00
alu 7f4c006385 add icons to config form 2022-12-21 15:56:52 -08:00
alu 0b2bb51bdd add swap in config 2022-12-21 15:39:09 -08:00
alu 6b2dbf2450 remove resource bar from instance article 2022-12-21 15:31:31 -08:00
alu afaf6c678d set step to 0.001 2022-12-21 15:26:18 -08:00
alu 06a54aa8a8 use min of 1 2022-12-21 15:22:50 -08:00
alu c18388c634 set min size for storage to 1 GB 2022-12-21 15:15:45 -08:00
alu 2cd7d9bfeb switch instance name logic 2022-12-21 15:10:13 -08:00
alu 0a392cb6a6 add better label text for rootfs 2022-12-21 15:08:21 -08:00
alu 655732fb9e fix name populating issue 2022-12-21 15:07:34 -08:00
alu 72f356d50a add lxc config form populating 2022-12-21 15:06:26 -08:00
alu 912b038136 fix login form layout 2022-12-21 11:33:51 -08:00
alu 76fea95adc fix string to int parsing 2022-12-21 01:36:24 -08:00
alu a8d66a8ae6 fix string to float conversion 2022-12-21 01:33:37 -08:00
alu 9189d4aa0e improve form populating 2022-12-21 01:31:58 -08:00
alu 461635f966 improve form layout css 2022-12-21 00:56:40 -08:00
alu cce8cf8115 add units to config form 2022-12-21 00:49:57 -08:00
alu c4d607570e remove colons from forms 2022-12-20 18:03:07 -08:00
alu 8341b88c76 add colon to sata config 2022-12-20 18:01:44 -08:00
alu 163542102d remove login status testing 2022-12-20 17:52:41 -08:00
alu fc073b6422 set legend height to 0 2022-12-20 17:51:53 -08:00
alu 322a5ea5be final form styling 2022-12-20 17:46:03 -08:00
alu c1779c9b7f remove border around form buttons 2022-12-20 17:38:14 -08:00
alu 4484f7dc8b populate vm name 2022-12-20 17:07:58 -08:00
alu 9754351e22 improve form layout and styling 2022-12-20 17:02:33 -08:00
alu 41d8b240ab parse disk to only use size 2022-12-20 16:47:49 -08:00
alu df0ff8cd7b change config form button type to button 2022-12-20 15:49:13 -08:00
alu 9854792407 try window.assign 2022-12-20 15:45:43 -08:00
alu 85f7fac100 add cancel button listener 2022-12-20 15:42:13 -08:00
alu e31c91679f fix missing increment 2022-12-20 15:38:53 -08:00
alu 589ecc1f9e add populating sata drives 2022-12-20 15:37:04 -08:00
alu f7a52a27d6 add credential check to config.js 2022-12-20 15:15:08 -08:00
alu 0c66460bf7 fix username input 2022-12-19 18:47:05 -08:00
alu b2cd65212b use grid layout for forms 2022-12-19 18:46:28 -08:00
alu 08c6126f81 undo input width 2022-12-19 18:36:41 -08:00
alu bbb7e0561e make config inputs 100% width 2022-12-19 18:35:09 -08:00
alu ce1119f14a use innerHTML directly 2022-12-19 18:21:48 -08:00
alu dc95d5092d remove unecessary line 2022-12-19 18:17:19 -08:00
alu 37508e646e use label property 2022-12-19 18:16:07 -08:00
alu 54e09d8b31 use innerHTML 2022-12-19 18:14:00 -08:00
alu 06aa9ac47e use setAttribute 2022-12-19 18:12:39 -08:00
alu ca2633fb79 fix config populating bug 2022-12-19 18:08:16 -08:00
alu bd81fdb8e6 populate cores and memory in config 2022-12-19 17:59:34 -08:00
alu 727c23cec3 remove btn-group:after 2022-12-19 17:35:55 -08:00
alu ce7466ac04 remove extra white space 2022-12-19 17:23:04 -08:00
alu 3e0ba0024b fix word break issue 2022-12-19 17:16:04 -08:00
alu ee0b534794 improve form layout handling 2022-12-19 17:05:04 -08:00
alu 534c40d7a9 add await to config form request 2022-12-19 16:21:24 -08:00
alu daf93b2b32 fix config form button ids 2022-12-19 16:17:57 -08:00
alu ef4597490a populate static elements of config.html,
add node info to config page search,
change login.css to form.css
2022-12-19 16:13:43 -08:00
alu 818748ef2b remove unecessary cookie header 2022-12-19 15:22:20 -08:00
alu ca05ecc72f remove extra end tag in config.html 2022-12-18 22:49:19 -08:00
alu e369456c57 add config.html,
add config.js,
add getURIData function to parse URI search params
2022-12-18 22:14:57 -08:00
alu 3160e0a858 readd proper imports to login.js 2022-12-18 22:00:59 -08:00
alu 368208fc61 fix login.js imports 2022-12-18 22:00:02 -08:00
alu 73ab09983c fix goToPage 2022-12-18 21:58:46 -08:00
alu 5dc35cbbf3 modularize redirect function 2022-12-18 21:52:39 -08:00
alu 42ebaff402 fix config button updating 2022-12-18 21:05:05 -08:00
alu 2721fd3e5e change config button icon on power action 2022-12-18 21:00:58 -08:00
alu aa4ea3e0de handle instance action update by process id 2022-12-18 20:32:05 -08:00
alu d03292a29c minify svgs 2022-12-18 19:58:08 -08:00
alu 0c5f20677e set proper alt for instance type/status and power action 2022-12-18 17:11:35 -08:00
alu 5e46481f72 add loading icon 2022-12-18 17:06:05 -08:00
alu a24467ee39 remove console 2022-12-18 16:56:02 -08:00
alu bb10726eef add actionLock to element power action 2022-12-18 16:53:07 -08:00
alu e6290cc058 remove periodic refresh in favor of refresh as needed 2022-12-17 17:52:20 -08:00
alu 7a5c1222ca assign alt properties 2022-12-17 17:36:45 -08:00
alu 83246d1fa1 remove debug console.log 2022-12-17 16:44:43 -08:00
alu 4cfd550a47 update power action button in element updater 2022-12-17 16:37:26 -08:00
alu 12927fd8a4 fix waitFor issue 2022-12-17 16:35:51 -08:00
alu 46da693292 handle update element in listener 2022-12-17 16:34:40 -08:00
alu 3d902a745d disable reload for now 2022-12-17 16:15:28 -08:00
alu 4153d7f76c run instance population every second 2022-12-17 16:07:18 -08:00
alu fcb28f0a9d remove csrf token from cookie 2022-12-15 23:51:51 -08:00
alu c83c706a32 put CSRF only for POST 2022-12-15 23:44:48 -08:00
alu aa8ff06b6f try setting CSRFPreventionToken header 2022-12-15 23:40:04 -08:00
alu 7618b24898 add csrf token saving 2022-12-15 23:15:52 -08:00
alu 22f334f373 fix power management request 2022-12-15 23:08:56 -08:00
alu 8997caa7be fix import 2022-12-15 23:05:32 -08:00
alu ff6c4fdd09 implement basic power management 2022-12-15 23:03:50 -08:00
alu 0ceace3c88 darken inactive config icon 2022-12-15 18:32:06 -08:00
alu ba07eff425 limit height of action buttons 2022-12-15 18:31:05 -08:00
alu f40e05fbf5 add action icons,
set action icons depending on instance status
2022-12-15 18:28:12 -08:00
alu ea596b8258 move btn-group margins to instance.css 2022-12-15 18:13:17 -08:00
alu 4e568552d7 add margin to btn-group 2022-12-15 18:09:48 -08:00
alu 10526d1a67 add power button html and css 2022-12-15 18:04:20 -08:00
alu c45c9df406 simplify instance styling 2022-12-15 17:45:40 -08:00
alu bbe9a7a93c add css for hr 2022-12-14 22:57:55 -08:00
alu 9db70290a8 add hr between summary and resources 2022-12-14 22:54:18 -08:00
alu 81b26b5f30 make instance cards more contained 2022-12-14 22:51:44 -08:00
alu 8d41997c76 center instance node info 2022-12-14 17:42:20 -08:00
alu 9a3b51a140 remove column gap 2022-12-14 17:36:28 -08:00
alu e9b236f5f0 evenly space resource elements 2022-12-14 17:34:13 -08:00
alu bc9c9b87d8 set qemu instance maxswap to 0 2022-12-14 17:31:05 -08:00
alu 3f33684edf fix resource disk text assignment 2022-12-14 17:28:48 -08:00
alu 9239b41e65 recolor resource icons to white 2022-12-14 17:27:53 -08:00
alu f72106ae83 upload resource icons,
add some column gap to resource-div
2022-12-14 17:23:05 -08:00
alu 2af76405ef add resource details to instance article 2022-12-14 17:20:31 -08:00
alu 0235760565 add implicit instance typing 2022-12-14 17:01:38 -08:00
alu 52cb2a9a10 fix instance article element selection 2022-12-14 16:10:28 -08:00
alu 3670730f2b fix instance article content 2022-12-14 16:07:25 -08:00
alu 9d29ae078a use templates to streamline instance creation 2022-12-14 16:05:15 -08:00
alu ea2011b734 improve instance article layout 2022-12-14 15:47:29 -08:00
alu 3e87f5ba29 arrange instance elements inline 2022-12-14 15:38:13 -08:00
alu 02ed33c674 add top div to instance article 2022-12-14 15:32:22 -08:00
alu a8664a4e04 fix node icon ref 2022-12-14 15:29:08 -08:00
alu a4c7a36012 add icons 2022-12-14 15:28:22 -08:00
alu bdec04aaf7 separate instance card data elements 2022-12-14 14:42:40 -08:00
alu ffa468fba2 add instance styling 2022-12-14 14:30:46 -08:00
alu b5901cb002 simplify instacne node assignment 2022-12-14 14:11:20 -08:00
alu 0a72233baa fix instance node data assignment 2022-12-14 13:48:29 -08:00
alu a10a8a4ddc add node data to instances 2022-12-14 13:47:28 -08:00
alu 5121060671 fix concat logic 2022-12-14 13:40:19 -08:00
alu 3247edbbbb simplify instance display 2022-12-14 13:37:30 -08:00
alu 35dd1402b0 fix instanceDiv 2022-12-13 23:28:07 -08:00
alu cb2cc97387 fix instance type setting,
add min-content to center-div
2022-12-13 23:26:43 -08:00
alu 63943782c6 fix node and instance element stylesheet reference 2022-12-12 21:43:19 -08:00
alu ccf7efb9b9 add useful login statuses 2022-12-12 17:12:11 -08:00
alu ad9969c393 make body optional parameter for request function 2022-12-12 16:30:55 -08:00
alu 5cee249285 move js to scripts folder,
move css to css folder
2022-12-12 16:27:45 -08:00
alu 40aea760e4 fix requestTicket return value 2022-12-12 16:21:33 -08:00
alu 4aba318c9b fix optional param 2022-12-12 16:19:47 -08:00
alu 4992f9e4ae remove repeated code 2022-12-12 16:18:40 -08:00
alu f4a74b9ba5 fix error naming again 2022-12-12 16:11:00 -08:00
alu e33e5ed67c fix error naming 2022-12-12 16:09:57 -08:00
alu a72967216d create custom ResponseError and NetworkError 2022-12-12 16:09:06 -08:00
alu 739c591dda apply promise catch to request function 2022-12-12 16:00:11 -08:00
alu 0553c3056b try promises 2022-12-12 15:56:57 -08:00
alu 942b9427ea fix ticket saving 2022-12-12 15:13:43 -08:00
alu 3bf0f9d0e4 try preventDefault 2022-12-12 15:11:52 -08:00
alu fe07147d6c try event.preventDefault fix 2022-12-12 15:10:52 -08:00
alu f1e705ae5e fix login form key issue 2022-12-12 15:00:32 -08:00
alu 1473626373 add login page 2022-12-12 14:57:43 -08:00
alu 80a442f3c0 update style.css with responsive center-div 2022-12-11 21:31:28 -08:00
alu f765be68c5 update style.css 2022-12-11 20:44:21 -08:00
alu 82496039ca fix instance populating 2022-12-11 20:22:37 -08:00
alu 9dae0cb371 fix script linking 2022-12-11 20:21:18 -08:00
alu f58cfc02e0 make instance custom element 2022-12-11 20:20:32 -08:00
alu 095c6a66bc fix instance status issue 2022-12-11 20:02:39 -08:00
alu 02188e7240 set instance color 2022-12-11 20:01:32 -08:00
alu 42aa765533 sort nodes and instances 2022-12-11 15:24:19 -08:00
alu 3a3d405c6d try new instance format 2022-12-11 15:15:39 -08:00
alu 53631e8723 fix instance populating 2022-12-11 15:07:44 -08:00
alu 1d0a1ebae3 add qemu and lxc instance populating 2022-12-11 15:06:10 -08:00
alu 7892b3168a fix instance printing 2022-12-11 14:56:41 -08:00
alu 34ecfe8a39 check instance GET 2022-12-11 14:54:50 -08:00
alu 8d0880346e fix idnentation in node.js 2022-12-10 23:05:31 -08:00
alu 438e220db0 implement node-card element,
add populating node-cards on init
2022-12-10 22:49:10 -08:00
alu 82b739af8e set cookie domain 2022-12-10 21:55:34 -08:00
alu 97f789853c update style.css 2022-12-10 19:25:59 -08:00
alu 82879d2cf1 login prototype 2022-12-10 18:02:29 -08:00
148 changed files with 3942 additions and 3098 deletions
+3 -1
View File
@@ -1,3 +1,5 @@
vars.js
**/config.json
**/package-lock.json
**/node_modules
dist/*
go.sum
+15
View File
@@ -0,0 +1,15 @@
.PHONY: build test clean
build: clean
@echo "======================== Building Binary ======================="
# resolve symbolic links in web by copying it into dist/web/
cp -rL web/ dist/web/
CGO_ENABLED=0 go build -ldflags="-s -w" -v -o dist/ .
test: clean
go run .
clean:
@echo "======================== Cleaning Project ======================"
go clean
rm -rf dist/*
+60 -22
View File
@@ -9,32 +9,70 @@ ProxmoxAAS Dashboard provides users of a proxmox based compute on demand service
- Remote console
- Extended account management
## Prerequisites
- Proxmox VE Cluster (v7.0+)
- Reverse proxy server which can proxy the dashboard and API
- FQDN
- Web server to host the ProxmoxAAS Dashboard (ie Apache2)
# Installation
## Notes
The supported setup is to use a reverse proxy to serve both the original Proxmox web interface and ProxmoxAAS Dashboard. It is possible other setups can work. Rather than provide specific steps to duplicate a certain setup, the steps included are intended as a guideline of steps required for proper function in most setups. Consequently, the examples provided are only to highlight key settings and do not represent complete working configurations. The instructions also assume you have your own domain name which will substitute `<FQDN>` in some of the configs.
## ProxmoxAAS System Installation Overview
The ProxmoxAAS project is large and is split into multiple components. There are three required components, the Dashboard, API, and Fabric. There is also an optional LDAP component for organizations that want to use LDAP as their authentication backend. The instalation order should start with the Dashboard and then proceed to the other backend components. This will require some foresight into the setup process.
The supported setup is to use a reverse proxy to serve both the original Proxmox web interface and ProxmoxAAS components. It is possible other setups can work. Rather than provide specific steps to duplicate a certain setup, the steps included are intended as a guideline of steps required for proper function in most setups. Consequently, the examples provided are only to highlight key settings and do not represent complete working configurations. The instructions also assume you have your own domain name which will substitute `domain.net` in some of the configs.
We will assume different hosts for each component which are accessible by unique host names and public URL addresses. Below is a table of references in the setup instructions across all components. You will need to substitute real addresses and hostnames for these in configurations.
| component | internal address | external address |
| --- | --- | --- |
| Proxmox VE API | pve.local | pve.domain.net |
| ProxmoxAAS-Dashboard | dashboard.local | paas.domain.net |
| ProxmoxAAS-API | api.local | paas.domain.net/api/ |
| ProxmoxAAS-Fabric | fabric.local | N/A |
| ProxmoxAAS-LDAP | ldap.local | N/A|
## Prerequisites - Dashboard
- Proxmox VE Cluster (v7.0+)
- Reverse proxy server which can proxy the dashboard, API, and Fabric
- FQDN
## Installation - Dashboard
1. Install Apache2 or another HTTP server onto a container or vm, which will be `Dashboard Host`
2. Clone this repo onto `Dashboard Host`, the default location for web root is `/var/www/html/`
4. Configure Apache2 to serve the app at port 80 by adding the file `dashboard.conf` to `/etc/apache2/sites-avaliable/` with at least the following:
```
<VirtualHost *:80>
DocumentRoot /var/www/html/ProxmoxAAS-Dashboard/
</VirtualHost>
```
4. Enable the site by running `a2ensite dashboard`
1. Initialize any host, which will be the `ProxmoxAAS-Dashboard` component host
2. Download `proxmoxaas-dashboard` binary and `template.config.json` file from [releases](https://git.tronnet.net/tronnet/ProxmoxAAS-LDAP/releases)
Rename `template.config.json` to `config.json` and modify:
- listenPort: port for PAAS-Dashboard to bind and listen on
- organization: name of your org which is displayed on the top left corner
- dashurl: url for the dashboard, ie. `https://paas.domain.net`
- apiurl: url for PAAS-API, ie. `https://paas.domain.net/api`
- pveurl: url for the Proxmox endpoint, ie. `https://pve.domain.net`
3. Execute the binary or additionally download `proxmoxaas-dashboard.service` from [releases](https://git.tronnet.net/tronnet/ProxmoxAAS-LDAP/releases) to run using systemd
After this step, the Dashboard should be available on the `Dashboard Host` at port `80`
## Configuration - Dashboard
1. In `Dashboard Host`, navigate to this repo's root folder
2. Rename `template.vars.js` to `vars.js` and assign the `API` variable with the value of the API's URL. This will likely be `paas.<FQDN>/api`
After this step, the Dashboard should be available on the `ProxmoxAAS-Dashboard` host at the configured `listenPort`
## Installation - API
To install the API, go to [ProxmoxAAS API](https://git.tronnet.net/tronnet/ProxmoxAAS-API). This is required for the app to function. The API installation will also have steps for setting up the reverse proxy server.
To install the API component, go to [ProxmoxAAS-API](https://git.tronnet.net/tronnet/ProxmoxAAS-API). This is required for the app to function. The API installation will also have steps for setting up the reverse proxy server.
## Installation - Fabric
To install the Fabric component, go to [ProxmoxAAS-Fabric](https://git.tronnet.net/tronnet/ProxmoxAAS-Fabric). This is required for the app to function. The Fabric installation will also have steps for setting up the reverse proxy server.
## Installation - LDAP
To install the LDAP component, go to [ProxmoxAAS-LDAP](https://git.tronnet.net/tronnet/ProxmoxAAS-LDAP).This is an optional component which adds a lightweight REST API server ontop of a simplified LDAP environment. It is only used by the API as a potential authentication backend.
## Installation - Reverse Proxy
1. Configure nginx or preferred reverse proxy to reverse proxy the dashboard. The configuration should include at least the following, ensuring that the configured ports are adjusted appropriately:
```
server {
listen 443 ssl;
server_name paas.domain.net;
location / {
proxy_pass http://dashboard.local:8080/;
proxy_redirect default;
}
location /api/ {
proxy_pass http://api.local:8081/api/;
proxy_redirect default;
}
}
```
2. Start nginx with the new configurations
The dashboard, API, and Fabric should be avaliable and fully functional.
-79
View File
@@ -1,79 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>proxmox - dashboard</title>
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/nav.css">
<link rel="stylesheet" href="css/form.css">
<script src="scripts/account.js" type="module"></script>
<script src="scripts/chart.js" type="module"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
@media screen and (width >= 1264px){
#resource-container {
display: grid;
grid-template-columns: repeat(auto-fill, calc(100% / 6));
grid-gap: 0;
justify-content: space-between;
}
}
@media screen and (width <= 1264px) and (width >= 680px) {
#resource-container {
display: grid;
grid-template-columns: repeat(auto-fill, 200px);
grid-gap: 0;
justify-content: space-between;
}
}
@media screen and (width <= 680px) {
#resource-container {
display: flex;
flex-direction: column;
gap: 0;
flex-wrap: nowrap;
justify-content: center;
}
}
</style>
</head>
<body>
<header>
<h1>proxmox</h1>
<label for="navtoggle">&#9776;</label>
<input type="checkbox" id="navtoggle">
<nav>
<a href="index.html">Instances</a>
<a href="account.html" aria-current="page">Account</a>
<a href="settings.html">Settings</a>
<a href="login.html">Logout</a>
</nav>
</header>
<main>
<section>
<h2>Account</h2>
<div class="w3-card w3-padding">
<h3>Account Details</h3>
<p id="username">Username:</p>
<p id="pool">Pools:</p>
<p id="vmid">VMID Range:</p>
<p id="nodes">Nodes:</p>
</div>
<div class="w3-card w3-padding">
<div class="flex row nowrap">
<h3>Password</h3>
<button class="w3-button w3-margin" id="change-password">Change Password</button>
</div>
</div>
<div class="w3-card w3-padding">
<h3>Cluster Resources</h3>
<div id="resource-container"></div>
</div>
</section>
</main>
</body>
</html>
+69
View File
@@ -0,0 +1,69 @@
package app
import (
"flag"
"fmt"
"log"
"proxmoxaas-dashboard/dist/web" // go will complain here until the first build
"proxmoxaas-dashboard/app/common"
"proxmoxaas-dashboard/app/routes"
"github.com/gin-gonic/gin"
"github.com/tdewolff/minify/v2"
)
func Run() {
gin.SetMode(gin.ReleaseMode)
configPath := flag.String("config", "config.json", "path to config.json file")
flag.Parse()
common.Global = common.GetConfig(*configPath)
router := gin.Default()
m := common.InitMinify()
ServeStatic(router, m)
html := common.MinifyStatic(m, web.Templates)
common.TMPL = common.LoadHTMLToGin(router, html)
router.GET("/account", routes.HandleGETAccount)
router.GET("/", routes.HandleGETIndex)
router.GET("/index", routes.HandleGETIndex)
router.GET("/index/instances", routes.HandleGETInstancesFragment)
router.GET("/config", routes.HandleGETConfig)
router.GET("/config/volumes", routes.HandleGETConfigVolumesFragment)
router.GET("/config/nets", routes.HandleGETConfigNetsFragment)
router.GET("/config/devices", routes.HandleGETConfigDevicesFragment)
router.GET("/config/boot", routes.HandleGETConfigBootFragment)
router.GET("/login", routes.HandleGETLogin)
router.GET("/settings", routes.HandleGETSettings)
log.Fatal(router.Run(fmt.Sprintf("0.0.0.0:%d", common.Global.Port)))
}
func ServeStatic(router *gin.Engine, m *minify.M) {
css := common.MinifyStatic(m, web.CSS_fs)
router.GET("/css/*css", func(c *gin.Context) {
path, _ := c.Params.Get("css")
data := css[fmt.Sprintf("css%s", path)]
c.Data(200, data.MimeType.Type, []byte(data.Data))
})
images := common.MinifyStatic(m, web.Images_fs)
router.GET("/images/*image", func(c *gin.Context) {
path, _ := c.Params.Get("image")
data := images[fmt.Sprintf("images%s", path)]
c.Data(200, data.MimeType.Type, []byte(data.Data))
})
modules := common.MinifyStatic(m, web.Modules_fs)
router.GET("/modules/*module", func(c *gin.Context) {
path, _ := c.Params.Get("module")
data := modules[fmt.Sprintf("modules%s", path)]
c.Data(200, data.MimeType.Type, []byte(data.Data))
})
scripts := common.MinifyStatic(m, web.Scripts_fs)
router.GET("/scripts/*script", func(c *gin.Context) {
path, _ := c.Params.Get("script")
data := scripts[fmt.Sprintf("scripts%s", path)]
c.Data(200, data.MimeType.Type, []byte(data.Data))
})
}
+52
View File
@@ -0,0 +1,52 @@
package common
import (
"io"
"github.com/tdewolff/minify/v2"
"github.com/tdewolff/minify/v2/css"
"github.com/tdewolff/minify/v2/html"
"github.com/tdewolff/minify/v2/js"
)
// defines mime type and associated minifier
type MimeType struct {
Type string
Minifier func(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error
}
// map file extension to mime types
var MimeTypes = map[string]MimeType{
"css": {
Type: "text/css",
Minifier: css.Minify,
},
"html": {
Type: "text/html",
Minifier: html.Minify,
},
"tmpl": {
Type: "text/plain",
Minifier: TemplateMinifier,
},
"frag": {
Type: "text/plain",
Minifier: TemplateMinifier,
},
"svg": {
Type: "image/svg+xml",
Minifier: nil,
},
"js": {
Type: "application/javascript",
Minifier: js.Minify,
},
"wasm": {
Type: "application/wasm",
Minifier: nil,
},
"*": {
Type: "text/plain",
Minifier: nil,
},
}
+48
View File
@@ -0,0 +1,48 @@
package common
type Config struct {
Port int `json:"listenPort"`
Organization string `json:"organization"`
DASH string `json:"dashurl"`
PVE string `json:"pveurl"`
API string `json:"apiurl"`
}
type StaticFile struct {
Data string
MimeType MimeType
}
// type used for templated <select>
type Select struct {
ID string
Required bool
Options []Option
}
// type used for templated <option>
type Option struct {
Selected bool
Value string
Display string
}
type RequestType int
type RequestContext struct {
Cookies map[string]string
Body map[string]any
}
type Auth struct {
Username string
Token string
CSRF string
}
type Icon struct {
ID string
Src string
Alt string
Clickable bool
}
+207
View File
@@ -0,0 +1,207 @@
package common
import (
"bufio"
"embed"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"io/fs"
"log"
"net/http"
"os"
"reflect"
"strings"
"github.com/gin-gonic/gin"
"github.com/tdewolff/minify/v2"
)
var TMPL *template.Template
var Global Config
func GetConfig(configPath string) Config {
content, err := os.ReadFile(configPath)
if err != nil {
log.Fatal("Error when opening config file: ", err)
}
var config Config
err = json.Unmarshal(content, &config)
if err != nil {
log.Fatal("Error during parsing config file: ", err)
}
return config
}
func InitMinify() *minify.M {
m := minify.New()
for _, v := range MimeTypes {
if v.Minifier != nil {
m.AddFunc(v.Type, v.Minifier)
}
}
return m
}
func MinifyStatic(m *minify.M, files embed.FS) map[string]StaticFile {
minified := make(map[string]StaticFile)
fs.WalkDir(files, ".", func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if !entry.IsDir() {
v, err := files.ReadFile(path)
if err != nil {
log.Fatalf("error parsing template file %s: %s", path, err.Error())
}
x := strings.Split(entry.Name(), ".")
if len(x) >= 2 { // file has extension
mimetype, ok := MimeTypes[x[len(x)-1]]
if ok && mimetype.Minifier != nil { // if the extension is mapped in MimeTypes and has a minifier
min, err := m.String(mimetype.Type, string(v)) // try to minify
if err != nil {
log.Fatalf("error minifying file %s: %s", path, err.Error())
}
minified[path] = StaticFile{
Data: min,
MimeType: mimetype,
}
} else { // if extension is not in MimeTypes and does not have minifier, skip minify
minified[path] = StaticFile{
Data: string(v),
MimeType: mimetype,
}
}
} else { // if the file has no extension, skip minify
mimetype := MimeTypes["*"]
minified[path] = StaticFile{
Data: string(v),
MimeType: mimetype,
}
}
}
return nil
})
return minified
}
func LoadHTMLToGin(engine *gin.Engine, html map[string]StaticFile) *template.Template {
root := template.New("")
engine.FuncMap = template.FuncMap{
"MapKeys": func(x any, sep string) string {
v := reflect.ValueOf(x)
keys := v.MapKeys()
s := ""
for i := 0; i < len(keys); i++ {
if i != 0 {
s += sep
}
s += keys[i].String()
}
return s
},
"Map": func(values ...any) (map[string]any, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]interface{}, len(values)/2)
for i := 0; i < len(values); i += 2 {
key, ok := values[i].(string)
if !ok {
return nil, errors.New("dict keys must be strings")
}
dict[key] = values[i+1]
}
return dict, nil
},
}
tmpl := template.Must(root, LoadAndAddToRoot(engine.FuncMap, root, html))
engine.SetHTMLTemplate(tmpl)
return tmpl
}
func LoadAndAddToRoot(FuncMap template.FuncMap, root *template.Template, html map[string]StaticFile) error {
for name, file := range html {
t := root.New(name).Funcs(FuncMap)
_, err := t.Parse(file.Data)
if err != nil {
return err
}
}
return nil
}
func TemplateMinifier(m *minify.M, w io.Writer, r io.Reader, _ map[string]string) error {
// remove newlines and tabs
rb := bufio.NewReader(r)
for {
line, err := rb.ReadString('\n')
if err != nil && err != io.EOF {
return err
}
line = strings.Replace(line, "\n", "", -1)
line = strings.Replace(line, "\t", "", -1)
line = strings.Replace(line, " ", "", -1)
if _, errws := io.WriteString(w, line); errws != nil {
return errws
}
if err == io.EOF {
break
}
}
return nil
}
func HandleNonFatalError(c *gin.Context, err error) {
log.Printf("[Error] encountered an error: %s", err.Error())
c.Status(http.StatusInternalServerError)
}
func RequestGetAPI(path string, context RequestContext) (*http.Response, int, error) {
req, err := http.NewRequest("GET", Global.API+path, nil)
if err != nil {
return nil, 0, err
}
for k, v := range context.Cookies {
req.AddCookie(&http.Cookie{Name: k, Value: v})
}
client := &http.Client{}
response, err := client.Do(req)
if err != nil {
return nil, response.StatusCode, err
} else if response.StatusCode != 200 {
return response, response.StatusCode, nil
}
data, err := io.ReadAll(response.Body)
if err != nil {
return nil, response.StatusCode, err
}
err = response.Body.Close()
if err != nil {
return nil, response.StatusCode, err
}
err = json.Unmarshal(data, &context.Body)
if err != nil {
return nil, response.StatusCode, err
}
return response, response.StatusCode, nil
}
func GetAuth(c *gin.Context) (Auth, error) {
_, errAuth := c.Cookie("auth")
username, errUsername := c.Cookie("username")
token, errToken := c.Cookie("PVEAuthCookie")
csrf, errCSRF := c.Cookie("CSRFPreventionToken")
if errUsername != nil || errAuth != nil || errToken != nil || errCSRF != nil {
return Auth{}, fmt.Errorf("error occured getting user cookies: (auth: %s, token: %s, csrf: %s)", errAuth, errToken, errCSRF)
} else {
return Auth{username, token, csrf}, nil
}
}
+263
View File
@@ -0,0 +1,263 @@
package routes
import (
"fmt"
"math"
"net/http"
"proxmoxaas-dashboard/app/common"
"github.com/gin-gonic/gin"
"github.com/go-viper/mapstructure/v2"
)
func HandleGETAccount(c *gin.Context) {
auth, err := common.GetAuth(c)
if err == nil {
account, err := GetUserAccount(auth)
if err != nil {
common.HandleNonFatalError(c, err)
return
}
for k, v := range account.Resources {
switch t := v.(type) {
case NumericResource:
avail, prefix := FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
account.Resources[k] = ResourceChart{
Type: t.Type,
Display: t.Display,
Name: t.Name,
Used: t.Total.Used,
Max: t.Total.Max,
Avail: avail,
Prefix: prefix,
Unit: t.Unit,
}
case StorageResource:
avail, prefix := FormatNumber(t.Total.Avail*t.Multiplier, t.Base)
account.Resources[k] = ResourceChart{
Type: t.Type,
Display: t.Display,
Name: t.Name,
Used: t.Total.Used,
Max: t.Total.Max,
Avail: avail,
Prefix: prefix,
Unit: t.Unit,
}
case ListResource:
l := struct {
Type string
Display bool
Resources []ResourceChart
}{
Type: t.Type,
Display: t.Display,
Resources: []ResourceChart{},
}
for _, r := range t.Total {
l.Resources = append(l.Resources, ResourceChart{
Type: t.Type,
Display: t.Display,
Name: r.Name,
Used: r.Used,
Max: r.Max,
Avail: float64(r.Avail), // usually an int
Unit: "",
})
}
account.Resources[k] = l
}
}
c.HTML(http.StatusOK, "html/account.html", gin.H{
"global": common.Global,
"page": "account",
"account": account,
})
} else {
c.Redirect(http.StatusFound, "/login") // if user is not authed, redirect user to login page
}
}
type Account struct {
Username string
Pools map[string]bool
Nodes map[string]bool
VMID struct {
Min int
Max int
}
Resources map[string]any
}
type Constraint struct {
Max int64
Used int64
Avail int64
}
type Match struct {
Name string
Match string
Max int64
Used int64
Avail int64
}
type NumericResource struct {
Type string
Name string
Multiplier int64
Base int64
Compact bool
Unit string
Display bool
Global Constraint
Nodes map[string]Constraint
Total Constraint
}
type StorageResource struct {
Type string
Name string
Multiplier int64
Base int64
Compact bool
Unit string
Display bool
Disks []string
Global Constraint
Nodes map[string]Constraint
Total Constraint
}
type ListResource struct {
Type string
Whitelist bool
Display bool
Global []Match
Nodes map[string][]Match
Total []Match
}
type ResourceChart struct {
Type string
Display bool
Name string
Used int64
Max int64
Avail float64
Prefix string
Unit string
}
func GetUserAccount(auth common.Auth) (Account, error) {
account := Account{
Resources: map[string]any{},
}
ctx := common.RequestContext{
Cookies: map[string]string{
"username": auth.Username,
"PVEAuthCookie": auth.Token,
"CSRFPreventionToken": auth.CSRF,
},
Body: map[string]any{},
}
// get user account basic data
res, code, err := common.RequestGetAPI("/user/config/cluster", ctx)
if err != nil {
return account, err
}
if code != 200 {
return account, fmt.Errorf("request to /user/config/cluster resulted in %+v", res)
}
err = mapstructure.Decode(ctx.Body, &account)
if err != nil {
return account, err
} else {
account.Username = auth.Username
}
ctx.Body = map[string]any{}
// get user resources
res, code, err = common.RequestGetAPI("/user/dynamic/resources", ctx)
if err != nil {
return account, err
}
if code != 200 {
return account, fmt.Errorf("request to /user/dynamic/resources resulted in %+v", res)
}
resources := ctx.Body
ctx.Body = map[string]any{}
// get resource meta data
res, code, err = common.RequestGetAPI("/global/config/resources", ctx)
if err != nil {
return account, err
}
if code != 200 {
return account, fmt.Errorf("request to /global/config/resources resulted in %+v", res)
}
meta := ctx.Body["resources"].(map[string]any)
// build each resource by its meta type
for k, v := range meta {
m := v.(map[string]any)
t := m["type"].(string)
r := resources[k].(map[string]any)
if t == "numeric" {
n := NumericResource{}
n.Type = t
err_m := mapstructure.Decode(m, &n)
err_r := mapstructure.Decode(r, &n)
if err_m != nil || err_r != nil {
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
}
account.Resources[k] = n
} else if t == "storage" {
n := StorageResource{}
n.Type = t
err_m := mapstructure.Decode(m, &n)
err_r := mapstructure.Decode(r, &n)
if err_m != nil || err_r != nil {
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
}
account.Resources[k] = n
} else if t == "list" {
n := ListResource{}
n.Type = t
err_m := mapstructure.Decode(m, &n)
err_r := mapstructure.Decode(r, &n)
if err_m != nil || err_r != nil {
return account, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
}
account.Resources[k] = n
}
}
return account, nil
}
func FormatNumber(val int64, base int64) (float64, string) {
valf := float64(val)
basef := float64(base)
steps := 0
for math.Abs(valf) > basef && steps < 4 {
valf /= basef
steps++
}
if base == 1000 {
prefixes := []string{"", "K", "M", "G", "T"}
return valf, prefixes[steps]
} else if base == 1024 {
prefixes := []string{"", "Ki", "Mi", "Gi", "Ti"}
return valf, prefixes[steps]
} else {
return 0, ""
}
}
+327
View File
@@ -0,0 +1,327 @@
package routes
import (
"fmt"
"net/http"
"proxmoxaas-dashboard/app/common"
"slices"
"sort"
fabric "proxmoxaas-fabric/app"
"github.com/gin-gonic/gin"
"github.com/go-viper/mapstructure/v2"
)
func HandleGETConfig(c *gin.Context) {
auth, err := common.GetAuth(c)
if err == nil {
vm_path, err := ExtractVMPath(c)
if err != nil {
common.HandleNonFatalError(c, err)
}
config, err := GetInstanceConfig(vm_path, auth)
if err != nil {
common.HandleNonFatalError(c, fmt.Errorf("error encountered getting instance config: %s", err.Error()))
}
if config.Type == "VM" { // if VM, fetch CPU types from node
config.ProctypeSelect, err = GetCPUTypes(vm_path, auth)
if err != nil {
common.HandleNonFatalError(c, fmt.Errorf("error encountered getting proctypes: %s", err.Error()))
}
}
for i, cpu := range config.ProctypeSelect.Options {
if cpu.Value == config.Proctype {
config.ProctypeSelect.Options[i].Selected = true
}
}
c.HTML(http.StatusOK, "html/config.html", gin.H{
"global": common.Global,
"page": "config",
"config": config,
})
} else {
c.Redirect(http.StatusFound, "/login")
}
}
func HandleGETConfigVolumesFragment(c *gin.Context) {
auth, err := common.GetAuth(c)
if err == nil {
vm_path, err := ExtractVMPath(c)
if err != nil {
common.HandleNonFatalError(c, err)
}
config, err := GetInstanceConfig(vm_path, auth)
if err != nil {
common.HandleNonFatalError(c, fmt.Errorf("error encountered getting instance config: %s", err.Error()))
}
c.Header("Content-Type", "text/plain")
common.TMPL.ExecuteTemplate(c.Writer, "html/config-volumes.frag", gin.H{
"config": config,
})
c.Status(http.StatusOK)
} else {
c.Status(http.StatusUnauthorized)
}
}
func HandleGETConfigNetsFragment(c *gin.Context) {
auth, err := common.GetAuth(c)
if err == nil {
vm_path, err := ExtractVMPath(c)
if err != nil {
common.HandleNonFatalError(c, err)
}
config, err := GetInstanceConfig(vm_path, auth)
if err != nil {
common.HandleNonFatalError(c, fmt.Errorf("error encountered getting instance config: %s", err.Error()))
}
c.Header("Content-Type", "text/plain")
common.TMPL.ExecuteTemplate(c.Writer, "html/config-nets.frag", gin.H{
"config": config,
})
c.Status(http.StatusOK)
} else {
c.Status(http.StatusUnauthorized)
}
}
func HandleGETConfigDevicesFragment(c *gin.Context) {
auth, err := common.GetAuth(c)
if err == nil {
vm_path, err := ExtractVMPath(c)
if err != nil {
common.HandleNonFatalError(c, err)
}
config, err := GetInstanceConfig(vm_path, auth)
if err != nil {
common.HandleNonFatalError(c, fmt.Errorf("error encountered getting instance config: %s", err.Error()))
}
c.Header("Content-Type", "text/plain")
common.TMPL.ExecuteTemplate(c.Writer, "html/config-devices.frag", gin.H{
"config": config,
})
c.Status(http.StatusOK)
} else {
c.Status(http.StatusUnauthorized)
}
}
func HandleGETConfigBootFragment(c *gin.Context) {
auth, err := common.GetAuth(c)
if err == nil {
vm_path, err := ExtractVMPath(c)
if err != nil {
common.HandleNonFatalError(c, err)
}
config, err := GetInstanceConfig(vm_path, auth)
if err != nil {
common.HandleNonFatalError(c, fmt.Errorf("error encountered getting instance config: %s", err.Error()))
}
c.Header("Content-Type", "text/plain")
common.TMPL.ExecuteTemplate(c.Writer, "html/config-boot.frag", gin.H{
"config": config,
})
c.Status(http.StatusOK)
} else {
c.Status(http.StatusUnauthorized)
}
}
func ExtractVMPath(c *gin.Context) (VMPath, error) {
req_node := c.Query("node")
req_type := c.Query("type")
req_vmid := c.Query("vmid")
if req_node == "" || req_type == "" || req_vmid == "" {
return VMPath{}, fmt.Errorf("request missing required values: (node: %s, type: %s, vmid: %s)", req_node, req_type, req_vmid)
}
vm_path := VMPath{
Node: req_node,
Type: req_type,
VMID: req_vmid,
}
return vm_path, nil
}
type VMPath struct {
Node string
Type string
VMID string
}
// imported types from fabric
type InstanceConfig struct {
Type fabric.InstanceType `json:"type"`
Name string `json:"name"`
Proctype string `json:"cpu"`
Cores uint64 `json:"cores"`
Memory uint64 `json:"memory"`
Swap uint64 `json:"swap"`
Volumes map[string]*fabric.Volume `json:"volumes"`
Nets map[string]*fabric.Net `json:"nets"`
Devices map[string]*fabric.Device `json:"devices"`
Boot fabric.BootOrder `json:"boot"`
// overrides
ProctypeSelect common.Select
}
func GetInstanceConfig(vm VMPath, auth common.Auth) (InstanceConfig, error) {
config := InstanceConfig{}
path := fmt.Sprintf("/cluster/%s/%s/%s", vm.Node, vm.Type, vm.VMID)
ctx := common.RequestContext{
Cookies: map[string]string{
"username": auth.Username,
"PVEAuthCookie": auth.Token,
"CSRFPreventionToken": auth.CSRF,
},
Body: map[string]any{},
}
res, code, err := common.RequestGetAPI(path, ctx)
if err != nil {
return config, err
}
if code != 200 {
return config, fmt.Errorf("request to %s resulted in %+v", path, res)
}
err = mapstructure.Decode(ctx.Body, &config)
if err != nil {
return config, err
}
config.Memory = config.Memory / (1024 * 1024) // memory in MiB
config.Swap = config.Swap / (1024 * 1024) // swap in MiB
return config, nil
}
type GlobalConfig struct {
CPU struct {
Whitelist bool
}
}
type UserConfig struct {
CPU struct {
Global []CPUConfig
Nodes map[string][]CPUConfig
}
}
type CPUConfig struct {
Name string
}
func GetCPUTypes(vm VMPath, auth common.Auth) (common.Select, error) {
cputypes := common.Select{
ID: "proctype",
Required: true,
}
// get global resource config
ctx := common.RequestContext{
Cookies: map[string]string{
"username": auth.Username,
"PVEAuthCookie": auth.Token,
"CSRFPreventionToken": auth.CSRF,
},
Body: map[string]any{},
}
path := "/global/config/resources"
res, code, err := common.RequestGetAPI(path, ctx)
if err != nil {
return cputypes, err
}
if code != 200 {
return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res)
}
global := GlobalConfig{}
err = mapstructure.Decode(ctx.Body["resources"], &global)
if err != nil {
return cputypes, err
}
// get user resource config
ctx.Body = map[string]any{}
path = "/user/config/resources"
res, code, err = common.RequestGetAPI(path, ctx)
if err != nil {
return cputypes, err
}
if code != 200 {
return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res)
}
user := UserConfig{}
err = mapstructure.Decode(ctx.Body, &user)
if err != nil {
return cputypes, err
}
// use node specific rules if present, otherwise use global rules
var userCPU []CPUConfig
if _, ok := user.CPU.Nodes[vm.Node]; ok {
userCPU = user.CPU.Nodes[vm.Node]
} else {
userCPU = user.CPU.Global
}
if global.CPU.Whitelist { // cpu is a whitelist
for _, cpu := range userCPU { // for each cpu type in user config add it to the options
cputypes.Options = append(cputypes.Options, common.Option{
Display: cpu.Name,
Value: cpu.Name,
})
}
} else { // cpu is a blacklist
// get the supported cpu types from the node
ctx.Body = map[string]any{}
path = fmt.Sprintf("/proxmox/nodes/%s/capabilities/qemu/cpu", vm.Node)
res, code, err = common.RequestGetAPI(path, ctx)
if err != nil {
return cputypes, err
}
if code != 200 {
return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res)
}
supported := struct {
data []CPUConfig
}{}
err = mapstructure.Decode(ctx.Body, supported)
if err != nil {
return cputypes, err
}
// for each node supported cpu type, if it is NOT in the user's config (aka is not blacklisted) then add it to the options
for _, cpu := range supported.data {
contains := slices.ContainsFunc(userCPU, func(c CPUConfig) bool {
return c.Name == cpu.Name
})
if !contains {
cputypes.Options = append(cputypes.Options, common.Option{
Display: cpu.Name,
Value: cpu.Name,
})
}
}
}
// sort the options by lexicographical order
sort.Slice(cputypes.Options, func(i, j int) bool {
return cputypes.Options[i].Display < cputypes.Options[j].Display
})
return cputypes, nil
}
+175
View File
@@ -0,0 +1,175 @@
package routes
import (
"fmt"
"net/http"
"proxmoxaas-dashboard/app/common"
"strconv"
"github.com/gin-gonic/gin"
"github.com/go-viper/mapstructure/v2"
)
func HandleGETIndex(c *gin.Context) {
auth, err := common.GetAuth(c)
if err == nil { // user should be authed, try to return index with population
instances, _, err := GetClusterResources(auth)
if err != nil {
common.HandleNonFatalError(c, err)
}
c.HTML(http.StatusOK, "html/index.html", gin.H{
"global": common.Global,
"page": "index",
"instances": instances,
})
} else { // return index without populating
c.Redirect(http.StatusFound, "/login") // if user is not authed, redirect user to login page
}
}
func HandleGETInstancesFragment(c *gin.Context) {
Auth, err := common.GetAuth(c)
if err == nil { // user should be authed, try to return index with population
instances, _, err := GetClusterResources(Auth)
if err != nil {
common.HandleNonFatalError(c, err)
}
c.Header("Content-Type", "text/plain")
common.TMPL.ExecuteTemplate(c.Writer, "html/index-instances.frag", gin.H{
"instances": instances,
})
c.Status(http.StatusOK)
} else { // return 401
c.Status(http.StatusUnauthorized)
}
}
// used in constructing instance cards in index
type Node struct {
Node string `json:"node"`
Status string `json:"status"`
}
// used in constructing instance cards in index
type InstanceCard struct {
VMID uint
Name string
Type string
Status string
Node string
NodeStatus string
}
// used in retriving cluster tasks
type Task struct {
Type string
Node string
User string
ID string
VMID uint
Status string
}
type InstanceStatus struct {
Status string
}
func GetClusterResources(auth common.Auth) (map[uint]InstanceCard, map[string]Node, error) {
ctx := common.RequestContext{
Cookies: map[string]string{
"PVEAuthCookie": auth.Token,
"CSRFPreventionToken": auth.CSRF,
},
Body: map[string]any{},
}
res, code, err := common.RequestGetAPI("/proxmox/cluster/resources", ctx)
if err != nil {
return nil, nil, err
}
if code != 200 { // if we did not successfully retrieve resources, then return 500 because auth was 1 but was invalid somehow
return nil, nil, fmt.Errorf("request to /cluster/resources resulted in %+v", res)
}
instances := map[uint]InstanceCard{}
nodes := map[string]Node{}
// if we successfully retrieved the resources, then process it and return index
for _, v := range ctx.Body["data"].([]any) {
m := v.(map[string]any)
if m["type"] == "node" {
node := Node{}
err := mapstructure.Decode(v, &node)
if err != nil {
return nil, nil, err
}
nodes[node.Node] = node
} else if m["type"] == "lxc" || m["type"] == "qemu" {
instance := InstanceCard{}
err := mapstructure.Decode(v, &instance)
if err != nil {
return nil, nil, err
}
instances[instance.VMID] = instance
}
}
for vmid, instance := range instances {
nodestatus := nodes[instance.Node].Status
instance.NodeStatus = nodestatus
instances[vmid] = instance
}
ctx.Body = map[string]any{}
res, code, err = common.RequestGetAPI("/proxmox/cluster/tasks", ctx)
if err != nil {
return nil, nil, err
}
if code != 200 { // if we did not successfully retrieve tasks, then return 500 because auth was 1 but was invalid somehow
return nil, nil, fmt.Errorf("request to /cluster/tasks resulted in %+v", res)
}
for _, v := range ctx.Body["data"].([]any) {
task := Task{}
err := mapstructure.Decode(v, &task)
if err != nil {
return nil, nil, err
}
x, err := strconv.Atoi(task.ID)
task.VMID = uint(x)
if err != nil {
return nil, nil, err
}
if task.User != auth.Username { // task was not made by user (ie was not a power on/off task)
continue
} else if _, ok := instances[task.VMID]; !ok { // task does not refer to an instance in user's instances
continue
} else if instances[task.VMID].Node != task.Node { // task does not have the correct node reference (should not happen)
continue
} else if !(task.Type == "qmstart" || task.Type == "qmstop" || task.Type == "vzstart" || task.Type == "vzstop") { // task is not start/stop for qemu or lxc
continue
} else if !(task.Status == "running" || task.Status == "OK") { // task is not running or finished with status OK
continue
} else { // recent task is a start or stop task for user instance which is running or "OK"
// get /status/current which is updated faster than /cluster/resources
instance := instances[task.VMID]
path := fmt.Sprintf("/proxmox/nodes/%s/%s/%d/status/current", instance.Node, instance.Type, instance.VMID)
ctx.Body = map[string]any{}
res, code, err := common.RequestGetAPI(path, ctx)
if err != nil {
return nil, nil, err
}
if code != 200 { // if we did not successfully retrieve tasks, then return 500 because auth was 1 but was invalid somehow
return nil, nil, fmt.Errorf("request to %s resulted in %+v", path, res)
}
status := InstanceStatus{}
mapstructure.Decode(ctx.Body["data"], &status)
instance.Status = status.Status
instances[task.VMID] = instance
}
}
return instances, nodes, nil
}
+76
View File
@@ -0,0 +1,76 @@
package routes
import (
"fmt"
"net/http"
"proxmoxaas-dashboard/app/common"
"github.com/gin-gonic/gin"
"github.com/go-viper/mapstructure/v2"
)
func GetLoginRealms() ([]Realm, error) {
realms := []Realm{}
ctx := common.RequestContext{
Cookies: nil,
Body: map[string]any{},
}
res, code, err := common.RequestGetAPI("/proxmox/access/domains", ctx)
if err != nil {
return realms, err
}
if code != 200 { // we expect /access/domains to always be avaliable
return realms, fmt.Errorf("request to /proxmox/access/domains resulted in %+v", res)
}
for _, v := range ctx.Body["data"].([]any) {
v = v.(map[string]any)
realm := Realm{}
err := mapstructure.Decode(v, &realm)
if err != nil {
return realms, err
}
realms = append(realms, realm)
}
return realms, nil
}
// used when requesting GET /access/domains
type GetRealmsBody struct {
Data []Realm `json:"data"`
}
// stores each realm's data
type Realm struct {
Default int `json:"default"`
Realm string `json:"realm"`
Comment string `json:"comment"`
}
func HandleGETLogin(c *gin.Context) {
realms, err := GetLoginRealms()
if err != nil {
common.HandleNonFatalError(c, err)
}
sel := common.Select{
ID: "realm",
Required: true,
}
for _, realm := range realms {
sel.Options = append(sel.Options, common.Option{
Selected: realm.Default != 0,
Value: realm.Realm,
Display: realm.Comment,
})
}
c.HTML(http.StatusOK, "html/login.html", gin.H{
"global": common.Global,
"page": "login",
"realms": sel,
})
}
+20
View File
@@ -0,0 +1,20 @@
package routes
import (
"net/http"
"proxmoxaas-dashboard/app/common"
"github.com/gin-gonic/gin"
)
func HandleGETSettings(c *gin.Context) {
_, err := common.GetAuth(c)
if err == nil {
c.HTML(http.StatusOK, "html/settings.html", gin.H{
"global": common.Global,
"page": "settings",
})
} else {
c.Redirect(http.StatusFound, "/login")
}
}
-71
View File
@@ -1,71 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>proxmox - dashboard</title>
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/nav.css">
<link rel="stylesheet" href="css/form.css">
<script src="scripts/config.js" type="module"></script>
<script src="scripts/draggable.js" type="module"></script>
</head>
<body>
<header>
<h1>proxmox</h1>
<label for="navtoggle">&#9776;</label>
<input type="checkbox" id="navtoggle">
<nav>
<a href="index.html" aria-current="page">Instances</a>
<a href="account.html">Account</a>
<a href="settings.html">Settings</a>
<a href="login.html">Logout</a>
</nav>
</header>
<main>
<section>
<h2 id="name"><a href="index.html">Instances</a> / %{vmname}</h2>
<form>
<fieldset class="w3-card w3-padding">
<legend>Resources</legend>
<div class="input-grid" id="resources" style="grid-template-columns: auto auto auto 1fr;"></div>
</fieldset>
<fieldset class="w3-card w3-padding">
<legend>Disks</legend>
<div class="input-grid" id="disks" style="grid-template-columns: auto auto 1fr auto;"></div>
<div class="w3-container w3-center">
<img id="disk-add" src="images/actions/disk/add-disk.svg" class="clickable" alt="Add New Disk" title="Add New Disk">
<img id="cd-add" src="images/actions/disk/add-cd.svg" class="clickable none" alt="Add New CDROM" title="Add New CDROM">
</div>
</fieldset>
<fieldset class="w3-card w3-padding">
<legend>Network Interfaces</legend>
<div class="input-grid" id="networks" style="grid-template-columns: auto auto 1fr auto;"></div>
<div class="w3-container w3-center">
<img id="network-add" src="images/actions/network/add.svg" class="clickable" alt="Add New Network Interface" title="Add New Network Interface">
</div>
</fieldset>
<fieldset class="w3-card w3-padding none" id="devices-card">
<legend>PCIe Devices</legend>
<div class="input-grid" id="devices" style="grid-template-columns: auto 1fr auto;"></div>
<div class="w3-container w3-center">
<img id="device-add" src="images/actions/device/add.svg" class="clickable" alt="Add New PCIe Device" title="Add New PCIe Device">
</div>
</fieldset>
<fieldset class="w3-card w3-padding none" id="boot-card">
<legend>Boot Order</legend>
<draggable-container id="enabled"></draggable-container>
<hr style="padding: 0; margin: 0;">
<draggable-container id="disabled"></draggable-container>
</fieldset>
<div class="w3-container w3-center" id="form-actions">
<button class="w3-button w3-margin" id="exit" type="button">EXIT</button>
</div>
</form>
</section>
</main>
</body>
</html>
+11 -11
View File
@@ -1,14 +1,14 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": "standard",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"env": {
"browser": true,
"es2021": true
},
"extends": "standard",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"no-tabs": [
"error",
{
@@ -38,5 +38,5 @@
"allowSingleLine": false
}
]
}
}
}
+16
View File
@@ -0,0 +1,16 @@
{
"extends": [
"html-validate:recommended"
],
"rules": {
"no-inline-style": "off"
},
"elements": [
"html5",
{
"head": {
"requiredContent": []
}
}
]
}
+7
View File
@@ -0,0 +1,7 @@
{
"listenPort": 8080,
"organization": "myorg",
"dashurl": "https://paas.mydomain.example",
"apiurl": "https://paas.mydomain.example/api",
"pveurl": "https://pve.mydomain.example"
}
-126
View File
@@ -1,126 +0,0 @@
:root {
--negative-color: #f00;
--positive-color: #0f0;
--highlight-color: yellow;
--lightbg-text-color: black;
--main-bg-color: #404040;
--main-text-color: white;
--main-card-bg-color: #202020;
--main-card-box-shadow: 0 2px 5px 0 rgb(0 0 0 / 80%), 0 2px 10px 0 rgb(0 0 0 / 80%);
--main-table-header-bg-color: black;
--main-input-bg-color: #404040;
}
@media screen and (prefers-color-scheme: light) {
:root {
--main-bg-color: white;
--main-text-color: black;
--main-card-bg-color: white;
--main-card-box-shadow: 0 2px 5px 0 rgb(0 0 0 / 20%), 0 2px 10px 0 rgb(0 0 0 / 20%);
--main-table-header-bg-color: #808080;
--main-input-bg-color: white;
}
}
html {
box-sizing: border-box;
}
h1, h2, h3, h4, h5, h6, p, a, label, button, input, select, td {
font-family: monospace;
}
body {
min-height: 100vh;
max-width: 100vw;
display: grid;
grid-template-rows: auto 1fr;
}
main, dialog {
max-width: 100vw;
background-color: var(--main-bg-color);
color: var(--main-text-color);
}
main {
padding: 0 16px 16px;
}
.w3-card {
background-color: var(--main-card-bg-color);
box-shadow: var(--main-card-box-shadow);
}
.w3-card + .w3-card {
margin-top: 16px;
}
th {
background-color: var(--main-table-header-bg-color);
}
td {
background-color: var(--main-card-bg-color);
}
input, select, textarea {
background-color: var(--main-input-bg-color);
color: var(--main-text-color);
}
img.clickable {
cursor: pointer;
}
img {
height: 1em;
width: 1em;
}
hr, * {
border-color: var(--main-text-color);
}
.flex {
display: flex;
}
.row {
flex-direction: row;
column-gap: 10px;
align-items: center;
}
.wrap {
flex-wrap: wrap;
row-gap: 10px;
}
.nowrap {
flex-wrap: nowrap;
}
.hidden {
visibility: hidden;
}
.none {
display: none;
}
.spacer {
min-height: 1em;
}
.w3-select, select {
padding: 8px;
-moz-appearance: none;
-webkit-appearance: none;
appearance: none;
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cstyle%3E:root%7Bcolor:%23fff%7D@media (prefers-color-scheme:light)%7B:root%7Bcolor:%23000%7D%7D%3C/style%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M12.707 14.707a1 1 0 01-1.414 0l-5-5a1 1 0 011.414-1.414L12 12.586l4.293-4.293a1 1 0 111.414 1.414l-5 5z' fill='currentColor'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 8px top 50%;
background-size: 1em auto;
}
+51
View File
@@ -0,0 +1,51 @@
module proxmoxaas-dashboard
go 1.24
require (
github.com/gin-gonic/gin v1.10.0
github.com/go-viper/mapstructure/v2 v2.2.1
github.com/tdewolff/minify v2.3.6+incompatible
proxmoxaas-fabric v0.0.0
)
replace proxmoxaas-fabric => ../ProxmoxAAS-Fabric
require (
github.com/buger/goterm v1.0.4 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/diskfs/go-diskfs v1.5.2 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/luthermonson/go-proxmox v0.2.2 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/tdewolff/minify/v2 v2.23.1 // indirect
github.com/tdewolff/parse v2.3.4+incompatible // indirect
github.com/tdewolff/parse/v2 v2.7.23 // indirect
github.com/tdewolff/test v1.0.11 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.16.0 // indirect
golang.org/x/crypto v0.37.0 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/text v0.24.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
-1
View File
@@ -1 +0,0 @@
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 11v6M14 11v6M4 7h16M6 7h12v11a3 3 0 01-3 3H9a3 3 0 01-3-3V7zM9 5a2 2 0 012-2h2a2 2 0 012 2v2H9V5z" stroke="#f00" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 292 B

-1
View File
@@ -1 +0,0 @@
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 11v6M14 11v6M4 7h16M6 7h12v11a3 3 0 01-3 3H9a3 3 0 01-3-3V7zM9 5a2 2 0 012-2h2a2 2 0 012 2v2H9V5z" stroke="#ffbfbf" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 295 B

-1
View File
@@ -1 +0,0 @@
<svg width="800" height="800" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M12 20a1 1 0 01-1-1v-6H5a1 1 0 010-2h6V5a1 1 0 012 0v6h6a1 1 0 010 2h-6v6a1 1 0 01-1 1z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 296 B

-16
View File
@@ -1,16 +0,0 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.034 229.034">
<style>
:root {
color: #fff
}
@media (prefers-color-scheme:light) {
:root {
color: #000
}
}
</style>
<path
d="M218.411 167.068l-70.305-70.301 9.398-35.073a7.504 7.504 0 00-1.94-7.245L103.311 2.197A7.499 7.499 0 0096.066.256L56.812 10.774a7.502 7.502 0 00-3.362 12.548l39.259 39.262-6.364 23.756-23.751 6.363-39.263-39.26a7.498 7.498 0 00-7.245-1.94 7.498 7.498 0 00-5.303 5.303L.266 96.059a7.5 7.5 0 001.941 7.244l52.249 52.255a7.5 7.5 0 007.245 1.941l35.076-9.4 70.302 70.306c6.854 6.854 15.968 10.628 25.662 10.629h.001c9.695 0 18.81-3.776 25.665-10.631 14.153-14.151 14.156-37.178.004-51.335zM207.8 207.795a21.15 21.15 0 01-15.058 6.239h-.002a21.153 21.153 0 01-15.056-6.236l-73.363-73.367a7.5 7.5 0 00-7.245-1.942L62 141.889 15.875 95.758l6.035-22.523 33.139 33.137a7.499 7.499 0 007.244 1.941l32.116-8.604a7.5 7.5 0 005.304-5.304l8.606-32.121a7.5 7.5 0 00-1.941-7.244L73.242 21.901l22.524-6.036 46.128 46.129-9.398 35.073a7.5 7.5 0 001.941 7.245l73.365 73.361c8.305 8.307 8.304 21.819-.002 30.122z"
fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-1
View File
@@ -1 +0,0 @@
<svg width="16" height="16" viewBox="2.3 2.3 19.4 19.4" fill="none" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g stroke="currentColor" stroke-width="1.25"><path d="M7 12h5m0 0h5m-5 0V7m0 5v5"/><circle cx="12" cy="12" r="9"/></g></svg>

Before

Width:  |  Height:  |  Size: 313 B

-1
View File
@@ -1 +0,0 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g fill="currentColor"><path d="M25 0H7a7 7 0 00-7 7v18a7 7 0 007 7h18a7 7 0 007-7V7a7 7 0 00-7-7zm5 25a5 5 0 01-5 5H7a5 5 0 01-5-5V7a5 5 0 015-5h18a5 5 0 015 5z"/><path d="M17 6h-2v9H6v2h9v9h2v-9h9v-2h-9V6z"/></g></svg>

Before

Width:  |  Height:  |  Size: 389 B

-1
View File
@@ -1 +0,0 @@
<svg width="800" height="800" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg"><g fill="#0f0"><path d="M17 12h-2.85a6.25 6.25 0 00-6.21 5H2v2h5.93a6.22 6.22 0 006.22 5H17z" class="prefix__prefix__clr-i-solid prefix__prefix__clr-i-solid-path-1"/><path d="M28.23 17A6.25 6.25 0 0022 12h-3v12h3a6.22 6.22 0 006.22-5H34v-2z" class="prefix__prefix__clr-i-solid prefix__prefix__clr-i-solid-path-2"/><path fill="none" d="M0 0h36v36H0z"/></g></svg>

Before

Width:  |  Height:  |  Size: 446 B

-1
View File
@@ -1 +0,0 @@
<svg height="800" width="800" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="red" d="M76.987 235.517H0v40.973h76.987c9.04 33.686 39.694 58.522 76.238 58.522h57.062V176.988h-57.062c-36.543 0-67.206 24.836-76.238 58.529zm435.013 0h-76.995c-9.032-33.693-39.686-58.53-76.23-58.53h-57.062v158.024h57.062c36.537 0 67.19-24.836 76.23-58.522H512v-40.972z"/></svg>

Before

Width:  |  Height:  |  Size: 377 B

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"/>

Before

Width:  |  Height:  |  Size: 64 B

-1
View File
@@ -1 +0,0 @@
<svg width="800" height="800" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M16 7l5 5m0 0l-5 5m5-5H3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 299 B

-1
View File
@@ -1 +0,0 @@
<svg width="800" height="800" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M16 7l5 5m0 0l-5 5m5-5H3" stroke="gray" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 205 B

-1
View File
@@ -1 +0,0 @@
<svg width="800" height="800" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M12 20a1 1 0 01-1-1v-6H5a1 1 0 010-2h6V5a1 1 0 012 0v6h6a1 1 0 010 2h-6v6a1 1 0 01-1 1z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 296 B

-1
View File
@@ -1 +0,0 @@
<svg width="800" height="800" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 20a1 1 0 01-1-1v-6H5a1 1 0 010-2h6V5a1 1 0 012 0v6h6a1 1 0 010 2h-6v6a1 1 0 01-1 1z" fill="gray"/></svg>

Before

Width:  |  Height:  |  Size: 202 B

-1
View File
@@ -1 +0,0 @@
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M5 10h14m-5 9l-2 2-2-2m4-14l-2-2-2 2m-5 9h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 329 B

-1
View File
@@ -1 +0,0 @@
<svg width="800" height="800" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M12 20a1 1 0 01-1-1v-6H5a1 1 0 010-2h6V5a1 1 0 012 0v6h6a1 1 0 010 2h-6v6a1 1 0 01-1 1z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 296 B

@@ -1 +0,0 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.034 229.034"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M218.411 167.068l-70.305-70.301 9.398-35.073a7.504 7.504 0 00-1.94-7.245L103.311 2.197A7.499 7.499 0 0096.066.256L56.812 10.774a7.502 7.502 0 00-3.362 12.548l39.259 39.262-6.364 23.756-23.751 6.363-39.263-39.26a7.498 7.498 0 00-7.245-1.94 7.498 7.498 0 00-5.303 5.303L.266 96.059a7.5 7.5 0 001.941 7.244l52.249 52.255a7.5 7.5 0 007.245 1.941l35.076-9.4 70.302 70.306c6.854 6.854 15.968 10.628 25.662 10.629h.001c9.695 0 18.81-3.776 25.665-10.631 14.153-14.151 14.156-37.178.004-51.335zM207.8 207.795a21.15 21.15 0 01-15.058 6.239h-.002a21.153 21.153 0 01-15.056-6.236l-73.363-73.367a7.5 7.5 0 00-7.245-1.942L62 141.889 15.875 95.758l6.035-22.523 33.139 33.137a7.499 7.499 0 007.244 1.941l32.116-8.604a7.5 7.5 0 005.304-5.304l8.606-32.121a7.5 7.5 0 00-1.941-7.244L73.242 21.901l22.524-6.036 46.128 46.129-9.398 35.073a7.5 7.5 0 001.941 7.245l73.365 73.361c8.305 8.307 8.304 21.819-.002 30.122z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1 +0,0 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.034 229.034"><path d="M218.411 167.068l-70.305-70.301 9.398-35.073a7.504 7.504 0 00-1.94-7.245L103.311 2.197A7.499 7.499 0 0096.066.256L56.812 10.774a7.502 7.502 0 00-3.362 12.548l39.259 39.262-6.364 23.756-23.751 6.363-39.263-39.26a7.498 7.498 0 00-7.245-1.94 7.498 7.498 0 00-5.303 5.303L.266 96.059a7.5 7.5 0 001.941 7.244l52.249 52.255a7.5 7.5 0 007.245 1.941l35.076-9.4 70.302 70.306c6.854 6.854 15.968 10.628 25.662 10.629h.001c9.695 0 18.81-3.776 25.665-10.631 14.153-14.151 14.156-37.178.004-51.335zM207.8 207.795a21.15 21.15 0 01-15.058 6.239h-.002a21.153 21.153 0 01-15.056-6.236l-73.363-73.367a7.5 7.5 0 00-7.245-1.942L62 141.889 15.875 95.758l6.035-22.523 33.139 33.137a7.499 7.499 0 007.244 1.941l32.116-8.604a7.5 7.5 0 005.304-5.304l8.606-32.121a7.5 7.5 0 00-1.941-7.244L73.242 21.901l22.524-6.036 46.128 46.129-9.398 35.073a7.5 7.5 0 001.941 7.245l73.365 73.361c8.305 8.307 8.304 21.819-.002 30.122z" fill="#808080"/></svg>

Before

Width:  |  Height:  |  Size: 1018 B

@@ -1 +0,0 @@
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path fill-rule="evenodd" clip-rule="evenodd" d="M2 7a5 5 0 015-5h10a5 5 0 015 5v10a5 5 0 01-5 5H7a5 5 0 01-5-5V7zm5-3a3 3 0 00-3 3v10a3 3 0 003 3h10a3 3 0 003-3V7a3 3 0 00-3-3H7zm0 13a1 1 0 011-1h8a1 1 0 110 2H8a1 1 0 01-1-1zm1.707-9.707a1 1 0 10-1.414 1.414L9.586 11l-2.293 2.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414l-3-3z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 537 B

@@ -1 +0,0 @@
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M2 7a5 5 0 015-5h10a5 5 0 015 5v10a5 5 0 01-5 5H7a5 5 0 01-5-5V7zm5-3a3 3 0 00-3 3v10a3 3 0 003 3h10a3 3 0 003-3V7a3 3 0 00-3-3H7zm0 13a1 1 0 011-1h8a1 1 0 110 2H8a1 1 0 01-1-1zm1.707-9.707a1 1 0 10-1.414 1.414L9.586 11l-2.293 2.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414l-3-3z" fill="#808080"/></svg>

Before

Width:  |  Height:  |  Size: 446 B

-1
View File
@@ -1 +0,0 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="2.8 2.4 12 12"><path d="M4.25 3l1.166-.624 8 5.333v1.248l-8 5.334-1.166-.624V3zm1.5 1.401v7.864l5.898-3.932L5.75 4.401z" fill="#0f0"/></svg>

Before

Width:  |  Height:  |  Size: 232 B

-1
View File
@@ -1 +0,0 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="-25 -25 380 380"><path stroke-width="20" d="M315 0H15C6.716 0 0 6.716 0 15v300c0 8.284 6.716 15 15 15h300c8.284 0 15-6.716 15-15V15c0-8.284-6.716-15-15-15zm-15 300H30V30h270v270z" stroke="#f00" fill="#f00"/></svg>

Before

Width:  |  Height:  |  Size: 285 B

-1
View File
@@ -1 +0,0 @@
<svg width="800" height="800" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M12 20a1 1 0 01-1-1v-6H5a1 1 0 010-2h6V5a1 1 0 012 0v6h6a1 1 0 010 2h-6v6a1 1 0 01-1 1z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 296 B

-1
View File
@@ -1 +0,0 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 229.034 229.034"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M218.411 167.068l-70.305-70.301 9.398-35.073a7.504 7.504 0 00-1.94-7.245L103.311 2.197A7.499 7.499 0 0096.066.256L56.812 10.774a7.502 7.502 0 00-3.362 12.548l39.259 39.262-6.364 23.756-23.751 6.363-39.263-39.26a7.498 7.498 0 00-7.245-1.94 7.498 7.498 0 00-5.303 5.303L.266 96.059a7.5 7.5 0 001.941 7.244l52.249 52.255a7.5 7.5 0 007.245 1.941l35.076-9.4 70.302 70.306c6.854 6.854 15.968 10.628 25.662 10.629h.001c9.695 0 18.81-3.776 25.665-10.631 14.153-14.151 14.156-37.178.004-51.335zM207.8 207.795a21.15 21.15 0 01-15.058 6.239h-.002a21.153 21.153 0 01-15.056-6.236l-73.363-73.367a7.5 7.5 0 00-7.245-1.942L62 141.889 15.875 95.758l6.035-22.523 33.139 33.137a7.499 7.499 0 007.244 1.941l32.116-8.604a7.5 7.5 0 005.304-5.304l8.606-32.121a7.5 7.5 0 00-1.941-7.244L73.242 21.901l22.524-6.036 46.128 46.129-9.398 35.073a7.5 7.5 0 001.941 7.245l73.365 73.361c8.305 8.307 8.304 21.819-.002 30.122z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g fill="#0f0" font-family="monospace" font-weight="bold"><text y="14" font-size="16">H</text><text x="9" y="8" font-size="10">0</text></g></svg>

Before

Width:  |  Height:  |  Size: 208 B

-1
View File
@@ -1 +0,0 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M14.25 8h-4.5A1.752 1.752 0 008 9.75v4.5A1.752 1.752 0 009.75 16h4.5A1.752 1.752 0 0016 14.25v-4.5A1.752 1.752 0 0014.25 8zM14 14h-4v-4h4zm8-5a1 1 0 000-2h-2v-.25A2.752 2.752 0 0017.25 4H17V2a1 1 0 00-2 0v2h-2V2a1 1 0 00-2 0v2H9V2a1 1 0 00-2 0v2h-.25A2.752 2.752 0 004 6.75V7H2a1 1 0 000 2h2v2H2a1 1 0 000 2h2v2H2a1 1 0 000 2h2v.25A2.752 2.752 0 006.75 20H7v2a1 1 0 002 0v-2h2v2a1 1 0 002 0v-2h2v2a1 1 0 002 0v-2h.25A2.752 2.752 0 0020 17.25V17h2a1 1 0 000-2h-2v-2h2a1 1 0 000-2h-2V9zm-4 8.25a.751.751 0 01-.75.75H6.75a.751.751 0 01-.75-.75V6.75A.751.751 0 016.75 6h10.5a.751.751 0 01.75.75z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 798 B

-1
View File
@@ -1 +0,0 @@
<svg height="800" width="800" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g stroke="currentColor" fill="currentColor"><path d="M480.003 128H48c0-22.056-17.944-40-40-40a8 8 0 000 16c13.234 0 24 10.766 24 24v288a8 8 0 0016 0v-8h16.01C77.238 408 88 397.238 88 384.01V384h392.003C497.646 384 512 369.646 512 352.003V159.997C512 142.354 497.646 128 480.003 128zM496 352.003c0 8.821-7.176 15.997-15.997 15.997H80a8 8 0 00-8 8v8.01c0 4.406-3.584 7.99-7.99 7.99H48V144h432.003c8.821 0 15.997 7.176 15.997 15.997v192.006z"/><path d="M240 192c-22.922 0-43.057 12.12-54.363 30.28a8.026 8.026 0 00-1.737 2.954A63.601 63.601 0 00176 256a63.583 63.583 0 008.264 31.399c.187.398.407.778.656 1.14C196.078 307.354 216.586 320 240 320c35.29 0 64-28.71 64-64a63.583 63.583 0 00-8.264-31.399 8.057 8.057 0 00-.656-1.14C283.922 204.646 263.414 192 240 192zm-48 64c0-4.395.605-8.648 1.717-12.695 3.596 3.178 8.453 6.73 15.035 10.53 6.376 3.681 11.742 6.078 16.208 7.612-2.622 2.061-5.987 4.385-10.208 6.821-8.449 4.878-14.816 7.039-18.36 7.752A47.681 47.681 0 01192 256zm96 0c0 4.103-.52 8.087-1.493 11.891-3.617-3.227-8.542-6.848-15.259-10.726-5.96-3.441-11.036-5.758-15.321-7.298 2.483-1.885 5.564-3.966 9.321-6.135 8.447-4.876 14.816-7.039 18.36-7.752A47.681 47.681 0 01288 256zm-14.052-33.901c-4.562 1.524-10.087 3.96-16.699 7.777-6.252 3.61-10.952 6.997-14.49 10.051-.449-3.245-.759-7.21-.759-11.927 0-9.763 1.314-16.361 2.469-19.785 11.465 1.064 21.775 6.169 29.479 13.884zm-46.329-12.472C226.655 214.344 226 220.354 226 228c0 7.056.557 12.721 1.401 17.26-3.022-1.232-6.59-2.938-10.65-5.282-8.302-4.793-13.33-9.159-15.769-11.883 6.394-8.915 15.757-15.56 26.637-18.468zm-21.57 80.271c4.564-1.524 10.086-3.954 16.702-7.774 6.252-3.61 10.952-6.997 14.49-10.051.449 3.245.759 7.21.759 11.927 0 9.763-1.314 16.361-2.469 19.785-11.466-1.064-21.778-6.17-29.482-13.887zm46.332 12.475C253.345 297.656 254 291.646 254 284c0-7.633-.653-13.635-1.614-18.347 3.066 1.237 6.708 2.97 10.863 5.368 8.764 5.06 13.892 9.652 16.163 12.33-6.4 9.195-15.926 16.054-27.031 19.022z"/><path d="M440 168a8 8 0 000 16c8.822 0 16 7.178 16 16v112c0 8.822-7.178 16-16 16H240c-39.701 0-72-32.299-72-72s32.299-72 72-72h168a8 8 0 000-16H240c-48.523 0-88 39.477-88 88s39.477 88 88 88h200c17.645 0 32-14.355 32-32V200c0-17.645-14.355-32-32-32zm-328 64H88c-8.822 0-16 7.178-16 16v16c0 8.822 7.178 16 16 16h24c8.822 0 16-7.178 16-16v-16c0-8.822-7.178-16-16-16zm-24 32v-16h24l.001 16H88zm24-88H88c-8.822 0-16 7.178-16 16v16c0 8.822 7.178 16 16 16h24c8.822 0 16-7.178 16-16v-16c0-8.822-7.178-16-16-16zm-24 32v-16h24l.001 16H88zm24 80H88c-8.822 0-16 7.178-16 16v16c0 8.822 7.178 16 16 16h24c8.822 0 16-7.178 16-16v-16c0-8.822-7.178-16-16-16zm-24 32v-16h24l.001 16H88z"/><path d="M432 200h-24a8 8 0 000 16h24a8 8 0 000-16zm0 96h-24a8 8 0 000 16h24a8 8 0 000-16zm0-72h-24a8 8 0 000 16h24a8 8 0 000-16zm0 24h-24a8 8 0 000 16h24a8 8 0 000-16zm0 24h-24a8 8 0 000 16h24a8 8 0 000-16z"/></g></svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

-1
View File
@@ -1 +0,0 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g fill="currentColor"><path d="M12 0a12 12 0 1012 12A12 12 0 0012 0zm0 22a10 10 0 1110-10 10 10 0 01-10 10z"/><path d="M12 8a4 4 0 104 4 4 4 0 00-4-4zm0 6a2 2 0 111.73-3A2 2 0 0112 14z"/><path d="M12 18a6 6 0 01-6-6 1 1 0 00-2 0 8 8 0 008 8 1 1 0 000-2zm0-14a1 1 0 000 2 6 6 0 016 6 1 1 0 001 1c2.57 0 .27-9-7-9z"/></g></svg>

Before

Width:  |  Height:  |  Size: 495 B

-1
View File
@@ -1 +0,0 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M22 12H2m3.45-6.89L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11zM6 16h0m4 0h0" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 405 B

-1
View File
@@ -1 +0,0 @@
<svg width="800" height="800" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M9 7V5H7v2H5v4h6V7H9zm-9 9h16V0H0v16zm2-2V2h12v12H2z" fill="currentColor" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 281 B

-1
View File
@@ -1 +0,0 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g fill="currentColor"><path d="M23 9a1 1 0 001-1V5a1 1 0 00-1-1H1a1 1 0 00-1 1v3a1 1 0 001 1 1 1 0 010 2 1 1 0 00-1 1v7a1 1 0 001 1h13a1 1 0 001-1v-3h2v3a1 1 0 001 1h5a1 1 0 001-1v-7a1 1 0 00-1-1 1 1 0 010-2zM2 16h3v2H2zm7 0v2H7v-2zm4 2h-2v-2h2zm6 0v-2h3v2zm3-10.83a3 3 0 000 5.66V14H2v-1.17a3 3 0 000-5.66V6h20z"/><path d="M9 11V9a1 1 0 00-2 0v2a1 1 0 002 0zm4 0V9a1 1 0 00-2 0v2a1 1 0 002 0zm4 0V9a1 1 0 00-2 0v2a1 1 0 002 0z"/></g></svg>

Before

Width:  |  Height:  |  Size: 610 B

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="21.986"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M19.841 3.24A10.988 10.988 0 008.54.573l1.266 3.8a7.033 7.033 0 018.809 9.158L17 11.891v7.092h7l-2.407-2.439A11.049 11.049 0 0019.841 3.24zM1 10.942a11.05 11.05 0 0011.013 11.044 11.114 11.114 0 003.521-.575l-1.266-3.8a7.035 7.035 0 01-8.788-9.22L7 9.891V6.034c.021-.02.038-.044.06-.065L7 5.909V2.982H0l2.482 2.449A10.951 10.951 0 001 10.942z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 533 B

-1
View File
@@ -1 +0,0 @@
<svg width="800" height="800" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M15.796 15.811L21 21m-3-10.5a7.5 7.5 0 11-15 0 7.5 7.5 0 0115 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

Before

Width:  |  Height:  |  Size: 351 B

-1
View File
@@ -1 +0,0 @@
<svg width="16" height="16" fill="#0f0" stroke="#none" xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="8" r="8"/></svg>

Before

Width:  |  Height:  |  Size: 125 B

-1
View File
@@ -1 +0,0 @@
<svg width="16" height="16" fill="#f00" stroke="none" xmlns="http://www.w3.org/2000/svg"><circle cx="8" cy="8" r="8"/></svg>

Before

Width:  |  Height:  |  Size: 124 B

-1
View File
@@ -1 +0,0 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 204.481 204.481"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M162.116 38.31a7.43 7.43 0 00.454-.67c.033-.055.068-.109.1-.164a7.72 7.72 0 00.419-.857c.014-.034.024-.069.038-.104a7.492 7.492 0 00.314-1.008c.068-.288.124-.581.157-.881l.008-.052a7.48 7.48 0 00.043-.796V7.5a7.5 7.5 0 00-7.5-7.5H48.332a7.5 7.5 0 00-7.5 7.5v26.279c0 .269.016.534.043.796l.008.052c.034.3.089.593.157.881.016.069.035.138.053.207.073.273.159.541.261.801.013.034.024.069.038.104.121.296.262.581.419.857.032.056.067.109.1.164.14.232.291.455.454.67.027.035.047.074.074.109l50.255 63.821-50.255 63.821c-.028.035-.047.074-.074.109a7.43 7.43 0 00-.454.67c-.033.055-.068.109-.1.164a7.72 7.72 0 00-.419.857c-.014.034-.024.069-.038.104a7.492 7.492 0 00-.314 1.008 7.308 7.308 0 00-.157.881l-.008.052a7.48 7.48 0 00-.043.796v26.279a7.5 7.5 0 007.5 7.5h107.817a7.5 7.5 0 007.5-7.5v-26.279c0-.269-.016-.534-.043-.796l-.008-.052a7.51 7.51 0 00-.157-.881c-.016-.069-.035-.138-.053-.207a7.492 7.492 0 00-.261-.801c-.013-.034-.024-.069-.038-.104a7.383 7.383 0 00-.419-.857c-.032-.056-.067-.109-.1-.164a7.646 7.646 0 00-.454-.67c-.027-.035-.047-.074-.074-.109l-50.255-63.821 50.255-63.821c.028-.035.047-.074.074-.11zM148.649 15v11.279H55.832V15h92.817zM55.832 189.481v-11.279h92.817v11.279H55.832zm84.866-26.279H63.784l38.457-48.838 38.457 48.838zm-38.457-73.084L63.784 41.279h76.914l-38.457 48.839z" fill="currentColor"/></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

-103
View File
@@ -1,103 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>proxmox - dashboard</title>
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/nav.css">
<link rel="stylesheet" href="css/form.css">
<script src="scripts/index.js" type="module"></script>
<script src="scripts/instance.js" type="module"></script>
<script src="modules/wfa.js" type="module"></script>
<style>
#instance-container > div {
border-bottom: 1px solid white;
}
#instance-container > div:last-child {
border-bottom: none;
}
@media screen and (width >= 440px) {
#vm-search {
max-width: calc(100% - 10px - 152px);
}
button .large {
display: block;
}
button .small {
display: none;
}
}
@media screen and (width <= 440px) {
#vm-search {
max-width: calc(100% - 10px - 47px);
}
button .large {
display: none;
}
button .small {
display: block;
}
}
</style>
</head>
<body>
<header>
<h1>proxmox</h1>
<label for="navtoggle">&#9776;</label>
<input type="checkbox" id="navtoggle">
<nav>
<a href="index.html" aria-current="page">Instances</a>
<a href="account.html">Account</a>
<a href="settings.html">Settings</a>
<a href="login.html">Logout</a>
</nav>
</header>
<main>
<section>
<h2>Instances</h2>
<div class="w3-card w3-padding">
<div class="flex row nowrap" style="margin-top: 1em; justify-content: space-between;">
<form id="vm-search" role="search" class="flex row nowrap">
<img src="images/static/search.svg" alt="Search VMs">
<input type="search" id="search" class="w3-input w3-border" style="height: 1lh; max-width: fit-content;" aria-label="search instances by name">
</form>
<button type="button" id="instance-add" class="w3-button" aria-label="create new instance">
<span class="large" style="margin: 0;">Create Instance</span>
<img class="small" style="height: 1lh; width: 1lh;" src="images/actions/instance/add.svg" alt="Create New Instance">
</button>
</div>
<div>
<div class="w3-row w3-hide-small" style="border-bottom: 1px solid;">
<div class="w3-col l1 m2">
<p>ID</p>
</div>
<div class="w3-col l2 m3">
<p>Name</p>
</div>
<div class="w3-col l1 m2">
<p>Type</p>
</div>
<div class="w3-col l2 m3">
<p>Status</p>
</div>
<div class="w3-col l2 w3-hide-medium">
<p>Host Name</p>
</div>
<div class="w3-col l2 w3-hide-medium">
<p>Host Status</p>
</div>
<div class="w3-col l2 m2">
<p>Actions</p>
</div>
</div>
<div id="instance-container"></div>
</div>
</div>
</section>
</main>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=proxmoxaas-dashboard
After=network.target
[Service]
WorkingDirectory=/<path to dir>
ExecStart=/<path to dir>/promoxaas-dashboard
Restart=always
RestartSec=10
Type=simple
[Install]
WantedBy=default.target
-40
View File
@@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>proxmox - dashboard</title>
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/nav.css">
<script src="scripts/login.js" type="module"></script>
</head>
<body>
<header>
<h1>proxmox</h1>
<label for="navtoggle">&#9776;</label>
<input type="checkbox" id="navtoggle">
<nav>
<a href="login.html" aria-current="page">Login</a>
</nav>
</header>
<main class="flex" style="justify-content: center; align-items: center;">
<div class="w3-container w3-card w3-margin w3-padding" style="height: fit-content;">
<h2 class="w3-center">Proxmox VE Login</h2>
<form>
<label for="username"><b>Username</b></label>
<input class="w3-input w3-border" id="username" name="username" type="text" autocomplete="username">
<label for="password"><b>Password</b></label>
<input class="w3-input w3-border" id="password" name="password" type="password" autocomplete="current-password">
<label for="realm">Realm</label>
<select class="w3-select w3-border" id="realm" name="realm"></select>
<div class="w3-center">
<button class="w3-button w3-margin" id="submit">LOGIN</button>
</div>
</form>
</div>
</main>
</body>
</html>
-328
View File
@@ -1,328 +0,0 @@
class WavefrontComponent {
constructor () {
this.lo = [0]; // lo for each wavefront
this.hi = [0]; // hi for each wavefront
this.W = []; // wavefront diag distance for each wavefront
this.A = []; // compact CIGAR for backtrace
}
// get value for wavefront=score, diag=k
get_val (score, k) {
if (this.W[score] !== undefined && this.W[score][k] !== undefined) {
return this.W[score][k];
}
else {
return NaN;
}
}
// set value for wavefront=score, diag=k
set_val (score, k, val) {
if (this.W[score]) {
this.W[score][k] = val;
}
else {
this.W[score] = [];
this.W[score][k] = val;
}
}
// get alignment traceback
get_traceback (score, k) {
if (this.A[score] !== undefined && this.A[score][k] !== undefined) {
return this.A[score][k];
}
else {
return undefined;
}
}
// set alignment traceback
set_traceback (score, k, traceback) {
if (this.A[score]) {
this.A[score][k] = traceback;
}
else {
this.A[score] = [];
this.A[score][k] = traceback;
}
}
// get hi for wavefront=score
get_hi (score) {
const hi = this.hi[score];
return isNaN(hi) ? 0 : hi;
}
// set hi for wavefront=score
set_hi (score, hi) {
this.hi[score] = hi;
}
// get lo for wavefront=score
get_lo (score) {
const lo = this.lo[score];
return isNaN(lo) ? 0 : lo;
}
// set lo for wavefront=score
set_lo (score, lo) {
this.lo[score] = lo;
}
// string representation of all wavefronts
toString () {
const traceback_str = ["OI", "EI", "OD", "ED", "SB", "IN", "DL", "EN"];
let s = "<";
let min_lo = Infinity;
let max_hi = -Infinity;
// get the min lo and max hi values across all wavefronts
for (let i = 0; i < this.W.length; i++) {
const lo = this.lo[i];
const hi = this.hi[i];
if (lo < min_lo) {
min_lo = lo;
}
if (hi > max_hi) {
max_hi = hi;
}
}
// print out two headers, one for wavefront and one for traceback
for (let k = min_lo; k <= max_hi; k++) {
s += FormatNumberLength(k, 2);
if (k < max_hi) {
s += "|";
}
}
s += ">\t<";
for (let k = min_lo; k <= max_hi; k++) {
s += FormatNumberLength(k, 2);
if (k < max_hi) {
s += "|";
}
}
s += ">\n";
// for each wavefront
for (let i = 0; i < this.W.length; i++) {
s += "[";
const lo = this.lo[i];
const hi = this.hi[i];
// print out the wavefront matrix
for (let k = min_lo; k <= max_hi; k++) {
if (this.W[i] !== undefined && this.W[i][k] !== undefined && !isNaN(this.W[i][k])) {
s += FormatNumberLength(this.W[i][k], 2);
}
else if (k < lo || k > hi) {
s += "--";
}
else {
s += " ";
}
if (k < max_hi) {
s += "|";
}
}
s += "]\t[";
// print out the traceback matrix
for (let k = min_lo; k <= max_hi; k++) {
if (this.A[i] !== undefined && this.A[i][k] !== undefined) {
s += traceback_str[this.A[i][k].toString()];
}
else if (k < lo || k > hi) {
s += "--";
}
else {
s += " ";
}
if (k < max_hi) {
s += "|";
}
}
s += "]\n";
}
return s;
}
}
const traceback = {
OpenIns: 0,
ExtdIns: 1,
OpenDel: 2,
ExtdDel: 3,
Sub: 4,
Ins: 5,
Del: 6,
End: 7
};
function FormatNumberLength (num, length) {
let r = "" + num;
while (r.length < length) {
r = " " + r;
}
return r;
}
function min (args) {
args.forEach((el, idx, arr) => {
arr[idx] = isNaN(el) ? Infinity : el;
});
const min = Math.min.apply(Math, args);
return min === Infinity ? NaN : min;
}
function max (args) {
args.forEach((el, idx, arr) => {
arr[idx] = isNaN(el) ? -Infinity : el;
});
const max = Math.max.apply(Math, args);
return max === -Infinity ? NaN : max;
}
function argmax (args) {
const val = max(args);
return args.indexOf(val);
}
export default function wf_align (s1, s2, penalties) {
const n = s1.length;
const m = s2.length;
const A_k = m - n;
const A_offset = m;
let score = 0;
const M = new WavefrontComponent();
M.set_val(0, 0, 0);
M.set_hi(0, 0);
M.set_lo(0, 0);
M.set_traceback(0, 0, traceback.End);
const I = new WavefrontComponent();
const D = new WavefrontComponent();
while (true) {
wf_extend(M, s1, n, s2, m, score);
if (M.get_val(score, A_k) >= A_offset) {
break;
}
score++;
wf_next(M, I, D, score, penalties);
}
return wf_backtrace(M, I, D, score, penalties, A_k, A_offset);
}
function wf_extend (M, s1, n, s2, m, score) {
const lo = M.get_lo(score);
const hi = M.get_hi(score);
for (let k = lo; k <= hi; k++) {
let v = M.get_val(score, k) - k;
let h = M.get_val(score, k);
if (isNaN(v) || isNaN(h)) {
continue;
}
while (s1[v] === s2[h]) {
M.set_val(score, k, M.get_val(score, k) + 1);
v++;
h++;
if (v > n || h > m) {
break;
}
}
}
}
function wf_next (M, I, D, score, penalties) {
const x = penalties.x;
const o = penalties.o;
const e = penalties.e;
const lo = min([M.get_lo(score - x), M.get_lo(score - o - e), I.get_lo(score - e), D.get_lo(score - e)]) - 1;
const hi = max([M.get_hi(score - x), M.get_hi(score - o - e), I.get_hi(score - e), D.get_hi(score - e)]) + 1;
M.set_hi(score, hi);
I.set_hi(score, hi);
D.set_hi(score, hi);
M.set_lo(score, lo);
I.set_lo(score, lo);
D.set_lo(score, lo);
for (let k = lo; k <= hi; k++) {
I.set_val(score, k, max([
M.get_val(score - o - e, k - 1),
I.get_val(score - e, k - 1)
]) + 1);
I.set_traceback(score, k, [traceback.OpenIns, traceback.ExtdIns][argmax([
M.get_val(score - o - e, k - 1),
I.get_val(score - e, k - 1)
])]);
D.set_val(score, k, max([
M.get_val(score - o - e, k + 1),
D.get_val(score - e, k + 1)
]));
D.set_traceback(score, k, [traceback.OpenDel, traceback.ExtdDel][argmax([
M.get_val(score - o - e, k + 1),
D.get_val(score - e, k + 1)
])]);
M.set_val(score, k, max([
M.get_val(score - x, k) + 1,
I.get_val(score, k),
D.get_val(score, k)
]));
M.set_traceback(score, k, [traceback.Sub, traceback.Ins, traceback.Del][argmax([
M.get_val(score - x, k) + 1,
I.get_val(score, k),
D.get_val(score, k)
])]);
}
}
function wf_backtrace (M, I, D, score, penalties, A_k) {
const traceback_CIGAR = ["I", "I", "D", "D", "X", "", "", ""];
const x = penalties.x;
const o = penalties.o;
const e = penalties.e;
let CIGAR_rev = ""; // reversed CIGAR
let tb_s = score; // traceback score
let tb_k = A_k; // traceback diag k
let current_traceback = M.get_traceback(tb_s, tb_k);
let done = false;
while (!done) {
CIGAR_rev += traceback_CIGAR[current_traceback];
switch (current_traceback) {
case traceback.OpenIns:
tb_s = tb_s - o - e;
tb_k = tb_k - 1;
current_traceback = M.get_traceback(tb_s, tb_k);
break;
case traceback.ExtdIns:
tb_s = tb_s - e;
tb_k = tb_k - 1;
current_traceback = I.get_traceback(tb_s, tb_k);
break;
case traceback.OpenDel:
tb_s = tb_s - o - e;
tb_k = tb_k + 1;
current_traceback = M.get_traceback(tb_s, tb_k);
break;
case traceback.ExtdDel:
tb_s = tb_s - e;
tb_k = tb_k + 1;
current_traceback = D.get_traceback(tb_s, tb_k);
break;
case traceback.Sub:
tb_s = tb_s - x;
// tb_k = tb_k;
current_traceback = M.get_traceback(tb_s, tb_k);
break;
case traceback.Ins:
// tb_s = tb_s;
// tb_k = tb_k;
current_traceback = I.get_traceback(tb_s, tb_k);
break;
case traceback.Del:
// tb_s = tb_s;
// tb_k = tb_k;
current_traceback = D.get_traceback(tb_s, tb_k);
break;
case traceback.End:
done = true;
break;
}
}
const CIGAR = Array.from(CIGAR_rev).reverse().join("");
return { CIGAR, score };
}
+3 -2
View File
@@ -4,7 +4,8 @@
"description": "Front-end for ProxmoxAAS",
"type": "module",
"scripts": {
"lint": "html-validator --continue; stylelint --formatter verbose --fix **/*.css; DEBUG=eslint:cli-engine eslint --fix scripts/"
"lint": "html-validate --config configs/.htmlvalidate.json web/html/*; stylelint --config configs/.stylelintrc.json --formatter verbose --fix web/css/*.css; DEBUG=eslint:cli-engine eslint --config configs/.eslintrc.json --fix web/scripts/",
"update-modules": "rm -rf web/modules/wfa.js web/modules/wfa.wasm; curl https://git.tronnet.net/alu/WFA-JS/releases/download/latest/wfa.js -o web/modules/wfa.js; curl https://git.tronnet.net/alu/WFA-JS/releases/download/latest/wfa.wasm -o web/modules/wfa.wasm"
},
"devDependencies": {
"eslint": "^8.43.0",
@@ -14,6 +15,6 @@
"eslint-plugin-promise": "^6.1.1",
"stylelint": "^15.9.0",
"stylelint-config-standard": "^33.0.0",
"w3c-html-validator": "^1.4.0"
"html-validate": "^9.4.0"
}
}
+9
View File
@@ -0,0 +1,9 @@
package main
import (
app "proxmoxaas-dashboard/app"
)
func main() {
app.Run()
}
-149
View File
@@ -1,149 +0,0 @@
import { dialog } from "./dialog.js";
import { requestAPI, goToPage, getCookie, setTitleAndHeader } from "./utils.js";
window.addEventListener("DOMContentLoaded", init);
const prefixes = {
1024: [
"",
"Ki",
"Mi",
"Gi",
"Ti"
],
1000: [
"",
"K",
"M",
"G",
"T"
]
};
async function init () {
setTitleAndHeader();
const cookie = document.cookie;
if (cookie === "") {
goToPage("login.html");
}
let resources = requestAPI("/user/dynamic/resources");
let meta = requestAPI("/global/config/resources");
let userCluster = requestAPI("/user/config/cluster");
resources = await resources;
meta = await meta;
userCluster = await userCluster;
document.querySelector("#username").innerText = `Username: ${getCookie("username")}`;
document.querySelector("#pool").innerText = `Pools: ${Object.keys(userCluster.pools).toString()}`;
document.querySelector("#vmid").innerText = `VMID Range: ${userCluster.vmid.min} - ${userCluster.vmid.max}`;
document.querySelector("#nodes").innerText = `Nodes: ${Object.keys(userCluster.nodes).toString()}`;
populateResources("#resource-container", meta, resources);
document.querySelector("#change-password").addEventListener("click", handlePasswordChangeForm);
}
function populateResources (containerID, meta, resources) {
if (resources instanceof Object) {
const container = document.querySelector(containerID);
Object.keys(meta).forEach((resourceType) => {
if (meta[resourceType].display) {
if (meta[resourceType].type === "list") {
resources[resourceType].total.forEach((listResource) => {
createResourceUsageChart(container, listResource.name, listResource.avail, listResource.used, listResource.max, null);
});
}
else {
createResourceUsageChart(container, meta[resourceType].name, resources[resourceType].total.avail, resources[resourceType].total.used, resources[resourceType].total.max, meta[resourceType]);
}
}
});
}
}
function createResourceUsageChart (container, resourceName, resourceAvail, resourceUsed, resourceMax, resourceUnitData) {
const chart = document.createElement("resource-chart");
container.append(chart);
const maxStr = parseNumber(resourceMax, resourceUnitData);
const usedStr = parseNumber(resourceUsed, resourceUnitData);
const usedRatio = resourceUsed / resourceMax;
const R = Math.min(usedRatio * 510, 255);
const G = Math.min((1 - usedRatio) * 510, 255);
const usedColor = `rgb(${R}, ${G}, 0)`;
chart.data = {
title: [resourceName, `Used ${usedStr} of ${maxStr}`],
ariaLabel: `${resourceName} used ${usedStr} of ${maxStr}`,
data: {
labels: [
"Used",
"Available"
],
datasets: [{
label: resourceName,
data: [resourceUsed, resourceAvail],
backgroundColor: [
usedColor,
"rgb(140, 140, 140)"
],
borderWidth: 0,
hoverOffset: 4
}]
},
breakpoint: 680
};
chart.style = "margin: 10px;";
}
function parseNumber (value, unitData) {
if (!unitData) {
return `${value}`;
}
const compact = unitData.compact;
const multiplier = unitData.multiplier;
const base = unitData.base;
const unit = unitData.unit;
value = multiplier * value;
if (value <= 0) {
return `0 ${unit}`;
}
else if (compact) {
const exponent = Math.floor(Math.log(value) / Math.log(base));
value = value / base ** exponent;
const unitPrefix = prefixes[base][exponent];
return `${value} ${unitPrefix}${unit}`;
}
else {
return `${value} ${unit}`;
}
}
function handlePasswordChangeForm () {
const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="new-password">New Password</label>
<input class="w3-input w3-border" id="new-password" name="new-password" type="password"required>
<label for="confirm-password">Confirm Password</label>
<input class="w3-input w3-border" id="confirm-password" name="confirm-password" type="password" required>
</form>
`;
const d = dialog("Change Password", body, async (result, form) => {
if (result === "confirm") {
const result = await requestAPI("/auth/password", "POST", { password: form.get("new-password") });
if (result.status !== 200) {
alert(result.error);
}
}
});
const password = d.querySelector("#new-password");
const confirmPassword = d.querySelector("#confirm-password");
function validatePassword () {
confirmPassword.setCustomValidity(password.value !== confirmPassword.value ? "Passwords Don't Match" : "");
}
password.addEventListener("change", validatePassword);
confirmPassword.addEventListener("keyup", validatePassword);
}
-120
View File
@@ -1,120 +0,0 @@
class ResourceChart extends HTMLElement {
constructor () {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<style>
* {
box-sizing: border-box;
font-family: monospace;
}
figure {
margin: 0;
}
div {
max-width: 400px;
aspect-ratio: 1 / 1;
}
figcaption {
text-align: center;
margin-top: 10px;
display: flex;
flex-direction: column;
}
</style>
<style id="responsive-style" media="not all">
figure {
display: flex;
align-items: center;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
}
div {
max-height: 1lh;
}
figcaption {
margin: 0;
margin-left: 10px;
display: flex;
flex-direction: row;
gap: 1ch;
font-size: small;
}
</style>
<figure>
<div>
<canvas></canvas>
</div>
<figcaption></figcaption>
</figure>
`;
this.responsiveStyle = this.shadowRoot.querySelector("#responsive-style");
this.canvas = this.shadowRoot.querySelector("canvas");
this.caption = this.shadowRoot.querySelector("figcaption");
}
set data (data) {
for (const line of data.title) {
this.caption.innerHTML += `<span>${line}</span>`;
}
this.canvas.role = "img";
this.canvas.ariaLabel = data.ariaLabel;
const chartData = {
type: "pie",
data: data.data,
options: {
plugins: {
title: {
display: false
},
legend: {
display: false
},
tooltip: {
enabled: true
}
},
interaction: {
mode: "nearest"
},
onHover: function (e, activeElements) {
if (window.innerWidth <= data.breakpoint) {
updateTooltipShow(e.chart, false);
}
else {
updateTooltipShow(e.chart, true);
}
}
}
};
this.chart = createChart(this.canvas, chartData);
if (data.breakpoint) {
this.responsiveStyle.media = `screen and (width <= ${data.breakpoint}px)`;
}
else {
this.responsiveStyle.media = "not all";
}
}
get data () {
return null;
}
}
function createChart (ctx, data) {
return new window.Chart(ctx, data);
}
// this is a really bad way to do this, but chartjs api does not expose many ways to dynamically set hover and tooltip options
function updateTooltipShow (chart, enabled) {
chart.options.plugins.tooltip.enabled = enabled;
chart.options.interaction.mode = enabled ? "nearest" : null;
chart.update();
}
customElements.define("resource-chart", ResourceChart);
-861
View File
@@ -1,861 +0,0 @@
import { requestPVE, requestAPI, goToPage, getURIData, resourcesConfig, setTitleAndHeader, bootConfig } from "./utils.js";
import { alert, dialog } from "./dialog.js";
window.addEventListener("DOMContentLoaded", init); // do the dumb thing where the disk config refreshes every second
const diskMetaData = resourcesConfig.disk;
const networkMetaData = resourcesConfig.network;
const pcieMetaData = resourcesConfig.pcie;
const bootMetaData = bootConfig;
let node;
let type;
let vmid;
let config;
async function init () {
setTitleAndHeader();
const cookie = document.cookie;
if (cookie === "") {
goToPage("login.html");
}
const uriData = getURIData();
node = uriData.node;
type = uriData.type;
vmid = uriData.vmid;
await getConfig();
populateResources();
populateDisk();
populateNetworks();
populateDevices();
populateBoot();
document.querySelector("#exit").addEventListener("click", handleFormExit);
}
function getOrdered (keys) {
const orderedKeys = Object.keys(keys).sort((a, b) => {
return parseInt(a) - parseInt(b);
}); // ordered integer list
return orderedKeys;
}
async function getConfig () {
config = await requestPVE(`/nodes/${node}/${type}/${vmid}/config`, "GET");
}
async function populateResources () {
const name = type === "qemu" ? "name" : "hostname";
document.querySelector("#name").innerHTML = document.querySelector("#name").innerHTML.replace("%{vmname}", config.data[name]);
if (type === "qemu") {
const global = await requestAPI("/global/config/resources");
const user = await requestAPI("/user/config/resources");
let options = [];
const globalCPU = global.cpu;
const userCPU = node in user.cpu.nodes ? user.cpu.nodes[node] : user.cpu.global;
if (globalCPU.whitelist) {
userCPU.forEach((userType) => {
options.push(userType.name);
});
options = options.sort((a, b) => {
return a.localeCompare(b);
});
}
else {
const supported = await requestPVE(`/nodes/${node}/capabilities/qemu/cpu`);
supported.data.forEach((supportedType) => {
if (!userCPU.some((userType) => supportedType.name === userType.name)) {
options.push(supportedType.name);
}
});
options = options.sort((a, b) => {
return a.localeCompare(b);
});
}
addResourceLine("resources", "images/resources/cpu.svg", "select", "CPU Type", "proctype", { value: config.data.cpu, options });
}
addResourceLine("resources", "images/resources/cpu.svg", "input", "CPU Amount", "cores", { type: "number", value: config.data.cores, min: 1, max: 8192 }, "Cores");
addResourceLine("resources", "images/resources/ram.svg", "input", "Memory", "ram", { type: "number", value: config.data.memory, min: 16, step: 1 }, "MiB");
if (type === "lxc") {
addResourceLine("resources", "images/resources/swap.svg", "input", "Swap", "swap", { type: "number", value: config.data.swap, min: 0, step: 1 }, "MiB");
}
}
function addResourceLine (fieldset, iconHref, type, labelText, id, attributes, unitText = null) {
const field = document.querySelector(`#${fieldset}`);
const icon = document.createElement("img");
icon.src = iconHref;
icon.alt = labelText;
field.append(icon);
const label = document.createElement("label");
label.innerText = labelText;
label.htmlFor = id;
field.append(label);
if (type === "input") {
const input = document.createElement("input");
for (const k in attributes) {
input.setAttribute(k, attributes[k]);
}
input.id = id;
input.name = id;
input.required = true;
input.classList.add("w3-input");
input.classList.add("w3-border");
field.append(input);
}
else if (type === "select") {
const select = document.createElement("select");
for (const option of attributes.options) {
select.append(new Option(option));
}
select.value = attributes.value;
select.id = id;
select.name = id;
select.required = true;
select.classList.add("w3-select");
select.classList.add("w3-border");
field.append(select);
}
if (unitText) {
const unit = document.createElement("p");
unit.innerText = unitText;
field.append(unit);
}
else {
const unit = document.createElement("div");
unit.classList.add("hidden");
field.append(unit);
}
}
async function populateDisk () {
document.querySelector("#disks").innerHTML = "";
for (let i = 0; i < diskMetaData[type].prefixOrder.length; i++) {
const prefix = diskMetaData[type].prefixOrder[i];
const busName = diskMetaData[type][prefix].name;
const disks = {};
Object.keys(config.data).forEach((element) => {
if (element.startsWith(prefix)) {
disks[element.replace(prefix, "")] = config.data[element];
}
});
const orderedKeys = getOrdered(disks);
orderedKeys.forEach((element) => {
const disk = disks[element];
addDiskLine("disks", prefix, busName, element, disk);
});
}
document.querySelector("#disk-add").addEventListener("click", handleDiskAdd);
if (type === "qemu") {
document.querySelector("#cd-add").classList.remove("none");
document.querySelector("#cd-add").addEventListener("click", handleCDAdd);
}
}
function addDiskLine (fieldset, busPrefix, busName, device, diskDetails) {
const field = document.querySelector(`#${fieldset}`);
const diskName = `${busName} ${device}`;
const diskID = `${busPrefix}${device}`;
// Set the disk icon, either drive.svg or disk.svg
const icon = document.createElement("img");
icon.src = diskMetaData[type][busPrefix].icon;
icon.alt = diskName;
icon.dataset.disk = diskID;
field.append(icon);
// Add a label for the disk bus and device number
const diskLabel = document.createElement("p");
diskLabel.innerText = diskName;
diskLabel.dataset.disk = diskID;
field.append(diskLabel);
// Add text of the disk configuration
const diskDesc = document.createElement("p");
diskDesc.innerText = diskDetails;
diskDesc.dataset.disk = diskID;
diskDesc.style.overflowX = "hidden";
diskDesc.style.whiteSpace = "nowrap";
field.append(diskDesc);
const actionDiv = document.createElement("div");
diskMetaData.actionBarOrder.forEach((element) => {
const action = document.createElement("img");
if (element === "detach_attach" && diskMetaData[type][busPrefix].actions.includes("attach")) { // attach
action.src = "images/actions/disk/attach.svg";
action.title = "Attach Disk";
action.addEventListener("click", handleDiskAttach);
action.classList.add("clickable");
}
else if (element === "detach_attach" && diskMetaData[type][busPrefix].actions.includes("detach")) { // detach
action.src = "images/actions/disk/detach.svg";
action.title = "Detach Disk";
action.addEventListener("click", handleDiskDetach);
action.classList.add("clickable");
}
else if (element === "delete") {
const active = diskMetaData[type][busPrefix].actions.includes(element) ? "active" : "inactive"; // resize
action.src = `images/actions/delete-${active}.svg`;
action.title = "Delete Disk";
if (active === "active") {
action.addEventListener("click", handleDiskDelete);
action.classList.add("clickable");
}
}
else {
const active = diskMetaData[type][busPrefix].actions.includes(element) ? "active" : "inactive"; // resize
action.src = `images/actions/disk/${element}-${active}.svg`;
if (active === "active") {
action.title = `${element.charAt(0).toUpperCase()}${element.slice(1)} Disk`;
if (element === "move") {
action.addEventListener("click", handleDiskMove);
}
else if (element === "resize") {
action.addEventListener("click", handleDiskResize);
}
action.classList.add("clickable");
}
}
action.dataset.disk = diskID;
action.alt = action.title;
actionDiv.append(action);
});
field.append(actionDiv);
}
async function handleDiskDetach () {
const disk = this.dataset.disk;
const header = `Detach ${disk}`;
const body = `<p>Are you sure you want to detach disk ${disk}</p>`;
dialog(header, body, async (result, form) => {
if (result === "confirm") {
document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg";
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/detach`, "POST");
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDisk();
deleteBootLine(`boot-${disk}`);
}
});
}
async function handleDiskAttach () {
const header = `Attach ${this.dataset.disk}`;
const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="device">${type === "qemu" ? "SATA" : "MP"}</label>
<input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="${type === "qemu" ? "5" : "255"}" required></input>
</form>
`;
dialog(header, body, async (result, form) => {
if (result === "confirm") {
const device = form.get("device");
document.querySelector(`img[data-disk="${this.dataset.disk}"]`).src = "images/status/loading.svg";
const body = {
source: this.dataset.disk.replace("unused", "")
};
const prefix = type === "qemu" ? "sata" : "mp";
const disk = `${prefix}${device}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/attach`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDisk();
addBootLine("disabled", { id: disk, prefix, value: disk, detail: config.data[disk] });
}
});
}
async function handleDiskResize () {
const header = `Resize ${this.dataset.disk}`;
const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="size-increment">Size Increment (GiB)</label>
<input class="w3-input w3-border" name="size-increment" id="size-increment" type="number" min="0" max="131072"></input>
</form>
`;
dialog(header, body, async (result, form) => {
if (result === "confirm") {
const disk = this.dataset.disk;
document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg";
const body = {
size: form.get("size-increment")
};
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/resize`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDisk();
const prefix = bootMetaData.eligiblePrefixes.find((pref) => disk.startsWith(pref));
updateBootLine(`boot-${disk}`, { id: disk, prefix, value: disk, detail: config.data[disk] });
}
});
}
async function handleDiskMove () {
const content = type === "qemu" ? "images" : "rootdir";
const storage = await requestPVE(`/nodes/${node}/storage`, "GET");
const header = `Move ${this.dataset.disk}`;
let options = "";
storage.data.forEach((element) => {
if (element.content.includes(content)) {
options += `<option value="${element.storage}">${element.storage}</option>"`;
}
});
const select = `<label for="storage-select">Storage</label><select class="w3-select w3-border" name="storage-select" id="storage-select"><option hidden disabled selected value></option>${options}</select>`;
const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
${select}
<label for="delete-check">Delete Source</label><input class="w3-input w3-border" name="delete-check" id="delete-check" type="checkbox" checked required>
</form>
`;
dialog(header, body, async (result, form) => {
if (result === "confirm") {
const disk = this.dataset.disk;
document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg";
const body = {
storage: form.get("storage-select"),
delete: form.get("delete-check") === "on" ? "1" : "0"
};
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/move`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDisk();
const prefix = bootMetaData.eligiblePrefixes.find((pref) => disk.startsWith(pref));
updateBootLine(`boot-${disk}`, { id: disk, prefix, value: config.data[disk] });
}
});
}
async function handleDiskDelete () {
const disk = this.dataset.disk;
const header = `Delete ${disk}`;
const body = `<p>Are you sure you want to <strong>delete</strong> disk${disk}</p>`;
dialog(header, body, async (result, form) => {
if (result === "confirm") {
document.querySelector(`img[data-disk="${disk}"]`).src = "images/status/loading.svg";
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/delete`, "DELETE");
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDisk();
deleteBootLine(`boot-${disk}`);
}
});
}
async function handleDiskAdd () {
const content = type === "qemu" ? "images" : "rootdir";
const storage = await requestPVE(`/nodes/${node}/storage`, "GET");
const header = "Create New Disk";
let options = "";
storage.data.forEach((element) => {
if (element.content.includes(content)) {
options += `<option value="${element.storage}">${element.storage}</option>"`;
}
});
const select = `<label for="storage-select">Storage</label><select class="w3-select w3-border" name="storage-select" id="storage-select" required><option hidden disabled selected value></option>${options}</select>`;
const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="device">${type === "qemu" ? "SATA" : "MP"}</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="${type === "qemu" ? "5" : "255"}" value="0" required></input>
${select}
<label for="size">Size (GiB)</label><input class="w3-input w3-border" name="size" id="size" type="number" min="0" max="131072" required></input>
</form>
`;
dialog(header, body, async (result, form) => {
if (result === "confirm") {
const body = {
storage: form.get("storage-select"),
size: form.get("size")
};
const id = form.get("device");
const prefix = type === "qemu" ? "sata" : "mp";
const disk = `${prefix}${id}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/create`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDisk();
addBootLine("disabled", { id: disk, prefix, value: disk, detail: config.data[disk] });
}
});
}
async function handleCDAdd () {
const isos = await requestAPI("/user/vm-isos", "GET");
const header = "Add a CDROM";
const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="device">IDE</label><input class="w3-input w3-border" name="device" id="device" type="number" min="0" max="3" required></input>
<label for="iso-select">Image</label><select class="w3-select w3-border" name="iso-select" id="iso-select" required></select>
</form>
`;
const d = dialog(header, body, async (result, form) => {
if (result === "confirm") {
const body = {
iso: form.get("iso-select")
};
const disk = `ide${form.get("device")}`;
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/disk/${disk}/create`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDisk();
addBootLine("disabled", { id: disk, prefix: "ide", value: disk, detail: config.data[disk] });
}
});
const isoSelect = d.querySelector("#iso-select");
for (const iso of isos) {
isoSelect.append(new Option(iso.name, iso.volid));
}
isoSelect.selectedIndex = -1;
}
async function populateNetworks () {
document.querySelector("#networks").innerHTML = "";
const networks = {};
const prefix = networkMetaData.prefix;
Object.keys(config.data).forEach((element) => {
if (element.startsWith(prefix)) {
networks[element.replace(prefix, "")] = config.data[element];
}
});
const orderedKeys = getOrdered(networks);
orderedKeys.forEach((element) => {
addNetworkLine("networks", prefix, element, networks[element]);
});
document.querySelector("#network-add").addEventListener("click", handleNetworkAdd);
}
function addNetworkLine (fieldset, prefix, netID, netDetails) {
const field = document.querySelector(`#${fieldset}`);
const icon = document.createElement("img");
icon.src = "images/resources/network.svg";
icon.alt = `${prefix}${netID}`;
icon.dataset.network = netID;
icon.dataset.values = netDetails;
field.appendChild(icon);
const netLabel = document.createElement("p");
netLabel.innerText = `${prefix}${netID}`;
netLabel.dataset.network = netID;
netLabel.dataset.values = netDetails;
field.append(netLabel);
const netDesc = document.createElement("p");
netDesc.innerText = netDetails;
netDesc.dataset.network = netID;
netDesc.dataset.values = netDetails;
netDesc.style.overflowX = "hidden";
netDesc.style.whiteSpace = "nowrap";
field.append(netDesc);
const actionDiv = document.createElement("div");
const configBtn = document.createElement("img");
configBtn.classList.add("clickable");
configBtn.src = "images/actions/network/config.svg";
configBtn.title = "Config Interface";
configBtn.addEventListener("click", handleNetworkConfig);
configBtn.dataset.network = netID;
configBtn.dataset.values = netDetails;
actionDiv.appendChild(configBtn);
const deleteBtn = document.createElement("img");
deleteBtn.classList.add("clickable");
deleteBtn.src = "images/actions/delete-active.svg";
deleteBtn.title = "Delete Interface";
deleteBtn.addEventListener("click", handleNetworkDelete);
deleteBtn.dataset.network = netID;
deleteBtn.dataset.values = netDetails;
actionDiv.appendChild(deleteBtn);
field.append(actionDiv);
}
async function handleNetworkConfig () {
const netID = this.dataset.network;
const netDetails = this.dataset.values;
const header = `Edit net${netID}`;
const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="rate">Rate Limit (MB/s)</label><input type="number" id="rate" name="rate" class="w3-input w3-border">
</form>
`;
const d = dialog(header, body, async (result, form) => {
if (result === "confirm") {
document.querySelector(`img[data-network="${netID}"]`).src = "images/status/loading.svg";
const body = {
rate: form.get("rate")
};
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/net${netID}/modify`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateNetworks();
const id = `net${netID}`;
updateBootLine(`boot-net${netID}`, { id, prefix: "net", value: id, detail: config.data[`net${netID}`] });
}
});
d.querySelector("#rate").value = netDetails.split("rate=")[1].split(",")[0];
}
async function handleNetworkDelete () {
const netID = this.dataset.network;
const header = `Delete net${netID}`;
const body = "";
dialog(header, body, async (result, form) => {
if (result === "confirm") {
document.querySelector(`img[data-network="${netID}"]`).src = "images/status/loading.svg";
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/net${netID}/delete`, "DELETE");
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateNetworks();
deleteBootLine(`boot-net${netID}`);
}
});
}
async function handleNetworkAdd () {
const header = "Create Network Interface";
let body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="netid">Interface ID</label><input type="number" id="netid" name="netid" class="w3-input w3-border">
<label for="rate">Rate Limit (MB/s)</label><input type="number" id="rate" name="rate" class="w3-input w3-border">
`;
if (type === "lxc") {
body += "<label for=\"name\">Interface Name</label><input type=\"text\" id=\"name\" name=\"name\" class=\"w3-input w3-border\"></input>";
}
body += "</form>";
dialog(header, body, async (result, form) => {
if (result === "confirm") {
const body = {
rate: form.get("rate")
};
if (type === "lxc") {
body.name = form.get("name");
}
const netID = form.get("netid");
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/net/net${netID}/create`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateNetworks();
const id = `net${netID}`;
addBootLine("disabled", { id, prefix: "net", value: id, detail: config.data[`net${netID}`] });
}
});
}
async function populateDevices () {
if (type === "qemu") {
document.querySelector("#devices-card").classList.remove("none");
document.querySelector("#devices").innerHTML = "";
const devices = {};
const prefix = pcieMetaData.prefix;
Object.keys(config.data).forEach((element) => {
if (element.startsWith(prefix)) {
devices[element.replace(prefix, "")] = config.data[element];
}
});
const orderedKeys = getOrdered(devices);
orderedKeys.forEach(async (element) => {
const deviceData = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/hostpci${element}`, "GET");
addDeviceLine("devices", prefix, element, devices[element], deviceData.device_name);
});
document.querySelector("#device-add").addEventListener("click", handleDeviceAdd);
}
}
function addDeviceLine (fieldset, prefix, deviceID, deviceDetails, deviceName) {
const field = document.querySelector(`#${fieldset}`);
const icon = document.createElement("img");
icon.src = "images/resources/device.svg";
icon.alt = `${prefix}${deviceID}`;
icon.dataset.device = deviceID;
icon.dataset.values = deviceDetails;
icon.dataset.name = deviceName;
field.appendChild(icon);
const deviceLabel = document.createElement("p");
deviceLabel.innerText = deviceName;
deviceLabel.dataset.device = deviceID;
deviceLabel.dataset.values = deviceDetails;
deviceLabel.dataset.name = deviceName;
deviceLabel.style.overflowX = "hidden";
deviceLabel.style.whiteSpace = "nowrap";
field.append(deviceLabel);
const actionDiv = document.createElement("div");
const configBtn = document.createElement("img");
configBtn.classList.add("clickable");
configBtn.src = "images/actions/device/config.svg";
configBtn.title = "Config Device";
configBtn.addEventListener("click", handleDeviceConfig);
configBtn.dataset.device = deviceID;
configBtn.dataset.values = deviceDetails;
configBtn.dataset.name = deviceName;
actionDiv.appendChild(configBtn);
const deleteBtn = document.createElement("img");
deleteBtn.classList.add("clickable");
deleteBtn.src = "images/actions/delete-active.svg";
deleteBtn.title = "Delete Device";
deleteBtn.addEventListener("click", handleDeviceDelete);
deleteBtn.dataset.device = deviceID;
deleteBtn.dataset.values = deviceDetails;
deleteBtn.dataset.name = deviceName;
actionDiv.appendChild(deleteBtn);
field.append(actionDiv);
}
async function handleDeviceConfig () {
const deviceID = this.dataset.device;
const deviceDetails = this.dataset.values;
const deviceName = this.dataset.name;
const header = `Edit Expansion Card ${deviceID}`;
const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="device">Device</label><select id="device" name="device" required></select><label for="pcie">PCI-Express</label><input type="checkbox" id="pcie" name="pcie" class="w3-input w3-border">
</form>
`;
const d = dialog(header, body, async (result, form) => {
if (result === "confirm") {
document.querySelector(`img[data-device="${deviceID}"]`).src = "images/status/loading.svg";
const body = {
device: form.get("device"),
pcie: form.get("pcie") ? 1 : 0
};
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/hostpci${deviceID}/modify`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDevices();
}
});
const availDevices = await requestAPI(`/cluster/${node}/pci`, "GET");
d.querySelector("#device").append(new Option(deviceName, deviceDetails.split(",")[0]));
for (const availDevice of availDevices) {
d.querySelector("#device").append(new Option(availDevice.device_name, availDevice.id));
}
d.querySelector("#pcie").checked = deviceDetails.includes("pcie=1");
}
async function handleDeviceDelete () {
const deviceID = this.dataset.device;
const header = `Remove Expansion Card ${deviceID}`;
const body = "";
dialog(header, body, async (result, form) => {
if (result === "confirm") {
document.querySelector(`img[data-device="${deviceID}"]`).src = "images/status/loading.svg";
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/hostpci${deviceID}/delete`, "DELETE");
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDevices();
}
});
}
async function handleDeviceAdd () {
const header = "Add Expansion Card";
const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="device">Device</label><select id="device" name="device" required></select><label for="pcie">PCI-Express</label><input type="checkbox" id="pcie" name="pcie" class="w3-input w3-border">
</form>
`;
const d = dialog(header, body, async (result, form) => {
if (result === "confirm") {
const body = {
device: form.get("device"),
pcie: form.get("pcie") ? 1 : 0
};
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/pci/create`, "POST", body);
if (result.status !== 200) {
alert(result.error);
}
await getConfig();
populateDevices();
}
});
const availDevices = await requestAPI(`/cluster/${node}/pci`, "GET");
for (const availDevice of availDevices) {
d.querySelector("#device").append(new Option(availDevice.device_name, availDevice.id));
}
d.querySelector("#pcie").checked = true;
}
async function populateBoot () {
if (type === "qemu") {
document.querySelector("#boot-card").classList.remove("none");
document.querySelector("#enabled").title = "Enabled";
document.querySelector("#disabled").title = "Disabled";
let order = [];
if (config.data.boot.startsWith("order=")) {
order = config.data.boot.replace("order=", "").split(";");
}
const bootable = { disabled: [] };
const eligible = bootMetaData.eligiblePrefixes;
for (let i = 0; i < order.length; i++) {
const element = order[i];
const prefix = eligible.find((pref) => order[i].startsWith(pref));
const detail = config.data[element];
bootable[i] = { id: element, value: element, prefix, detail };
}
Object.keys(config.data).forEach((element) => {
const prefix = eligible.find((pref) => element.startsWith(pref));
const detail = config.data[element];
if (prefix && !order.includes(element)) {
bootable.disabled.push({ id: element, value: element, prefix, detail });
}
});
Object.keys(bootable).sort();
Object.keys(bootable).forEach((element) => {
if (element !== "disabled") {
addBootLine("enabled", bootable[element], document.querySelector("#enabled-spacer"));
}
else {
bootable.disabled.forEach((item) => {
addBootLine("disabled", item, document.querySelector("#disabled-spacer"));
});
}
});
}
}
function addBootLine (container, data, before = null) {
const item = document.createElement("draggable-item");
item.data = data;
item.innerHTML = `
<div style="display: grid; grid-template-columns: auto auto 8ch 1fr; column-gap: 10px; align-items: center;">
<img src="images/actions/drag.svg" id="drag" alt="drag icon">
<img src="${bootMetaData[data.prefix].icon}" alt="${bootMetaData[data.prefix].alt}">
<p style="margin: 0px;">${data.id}</p>
<p style="margin: 0px; overflow-x: hidden; white-space: nowrap;">${data.detail}</p>
</div>
`;
item.draggable = true;
item.classList.add("drop-target");
item.id = `boot-${data.id}`;
if (before) {
document.querySelector(`#${container}`).insertBefore(item, before);
}
else {
document.querySelector(`#${container}`).append(item);
}
item.container = container;
item.value = data.value;
}
function deleteBootLine (id) {
const query = `#${id}`;
const enabled = document.querySelector("#enabled");
const disabled = document.querySelector("#disabled");
const inEnabled = enabled.querySelector(query);
const inDisabled = disabled.querySelector(query);
if (inEnabled) {
enabled.removeChild(inEnabled);
}
if (inDisabled) {
disabled.removeChild(inDisabled);
}
}
function updateBootLine (id, newData) {
const enabled = document.querySelector("#enabled");
const disabled = document.querySelector("#disabled");
let element = null;
if (enabled.querySelector(`#${id}`)) {
element = enabled.querySelector(`#${id}`);
}
if (disabled.querySelector(`#${id}`)) {
element = disabled.querySelector(`#${id}`);
}
if (element) {
const container = element.container;
const before = element.nextSibling;
deleteBootLine(id);
addBootLine(container, newData, before);
return true;
}
else {
return false;
}
}
async function handleFormExit () {
const body = {
cores: document.querySelector("#cores").value,
memory: document.querySelector("#ram").value
};
if (type === "lxc") {
body.swap = document.querySelector("#swap").value;
}
else if (type === "qemu") {
body.proctype = document.querySelector("#proctype").value;
body.boot = document.querySelector("#enabled").value;
}
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/resources`, "POST", body);
if (result.status === 200) {
await getConfig();
populateDisk();
goToPage("index.html");
}
else {
alert(result.error);
}
}
-211
View File
@@ -1,211 +0,0 @@
// Map valid UUIDs used by draggable-item elements in order to better validate data transfers to ignore random data transfers.
const draggableItemUUIDs = {};
/**
* Get the data transfer source object by parsing its types. Valid draggable-item events have one type of the format `application/json/${uuid}`.
* The function takes the entire type list from event.dataTransfer.types and returns the source object if valid, or null if invalid.
* @param {*} typesList from event.dataTransfer.types
* @returns {Object} Object containing the type, uuid, and element of the dataTransfer source or null
*/
function getDragSource (typesList) {
if (typesList.length !== 1) {
return null;
}
const typeString = typesList[0];
const type = typeString.split("/");
if (type.length === 3 && type[0] === "application" && type[1] === "json" && draggableItemUUIDs[type[2]]) {
return { type: typeString, uuid: type[2], element: draggableItemUUIDs[type[2]] };
}
else {
return null;
}
}
class DraggableContainer extends HTMLElement {
constructor () {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<label id="title"></label>
<div id="wrapper">
<draggable-item id="bottom" class="drop-target"></draggable-item>
</div>
`;
this.content = this.shadowRoot.querySelector("#wrapper");
this.bottom = this.shadowRoot.querySelector("#bottom");
this.titleElem = this.shadowRoot.querySelector("#title");
}
get title () {
return this.titleElem.innerText;
}
set title (title) {
this.titleElem.innerText = title;
}
append (newNode) {
newNode.uuid = window.crypto.randomUUID();
draggableItemUUIDs[newNode.uuid] = newNode;
this.content.insertBefore(newNode, this.bottom);
}
insertBefore (newNode, referenceNode) {
newNode.uuid = window.crypto.randomUUID();
draggableItemUUIDs[newNode.uuid] = newNode;
this.content.insertBefore(newNode, referenceNode);
}
querySelector (query) {
return this.content.querySelector(query);
}
removeChild (node) {
if (node && this.content.contains(node)) {
draggableItemUUIDs[node.uuid] = null;
this.content.removeChild(node);
return true;
}
else {
return false;
}
}
set value (value) {}
get value () {
const value = [];
this.content.childNodes.forEach((element) => {
if (element.value) {
value.push(element.value);
}
});
return value;
}
}
class DraggableItem extends HTMLElement {
uuid = null;
constructor () {
super();
this.attachShadow({ mode: "open" });
// for whatever reason, only grid layout seems to respect the parent's content bounds
this.shadowRoot.innerHTML = `
<style>
#drag-over {
height: 1.5em;
border: 1px dotted var(--main-text-color);
border-radius: 5px;
background-color: rgba(0,0,0,0.25);
}
img {
height: 1em;
width: 1em;
}
</style>
<div id="drag-over" style="display: none;"></div>
<div id="wrapper">
<div style="min-height: 1.5em;"></div>
</div>
`;
this.content = this.shadowRoot.querySelector("#wrapper");
// add drag and drop listeners
this.addEventListener("dragstart", (event) => {
this.content.style.opacity = "0.5";
const data = { id: this.id, data: this.data, content: this.content.innerHTML, value: this.value };
event.dataTransfer.setData(`application/json/${this.uuid}`, JSON.stringify(data));
event.dataTransfer.effectAllowed = "move";
const blank = document.createElement("img");
event.dataTransfer.setDragImage(blank, 0, 0);
setTimeout(() => {
this.content.style.visibility = "hidden";
this.content.style.height = "0";
}, 0);
});
this.addEventListener("dragend", (event) => {
if (event.dataTransfer.dropEffect === "move") {
this.parentElement.removeChild(this);
}
else {
this.content.attributeStyleMap.clear();
}
});
this.addEventListener("dragenter", (event) => {
const sourceElement = getDragSource(event.dataTransfer.types);
if (event.target.dropTarget && sourceElement) {
event.target.dragOver = sourceElement.element.innerHTML;
}
event.preventDefault();
});
this.addEventListener("dragleave", (event) => {
if (event.target.dragOver && getDragSource(event.dataTransfer.types)) {
event.target.dragOver = false;
}
event.preventDefault();
});
this.addEventListener("dragover", (event) => {
event.preventDefault();
});
this.addEventListener("drop", (event) => {
if (event.target.dragOver) {
event.target.dragOver = false;
}
const sourceElement = getDragSource(event.dataTransfer.types);
if (event.target.dropTarget && sourceElement) {
const transfer = JSON.parse(event.dataTransfer.getData(sourceElement.type));
const item = document.createElement("draggable-item");
item.data = transfer.data;
item.innerHTML = transfer.content;
item.draggable = true;
item.dropTarget = true;
item.id = transfer.id;
item.value = transfer.data.value;
item.uuid = sourceElement.uuid;
event.target.parentElement.insertBefore(item, event.target);
}
this.content.attributeStyleMap.clear();
event.preventDefault();
});
}
get innerHTML () {
return this.content.innerHTML;
}
set innerHTML (innerHTML) {
this.content.innerHTML = innerHTML;
}
get dropTarget () {
return this.classList.contains("drop-target");
}
set dropTarget (dropTarget) {
if (dropTarget) {
this.classList.add("drop-target");
}
else {
this.classList.remove("drop-target");
}
}
get dragOver () {
return this.classList.contains("drag-over");
}
set dragOver (dragOver) {
if (dragOver) {
this.classList.add("drag-over");
this.shadowRoot.querySelector("#drag-over").style.display = "block";
this.shadowRoot.querySelector("#drag-over").innerHTML = dragOver;
}
else {
this.classList.remove("drag-over");
this.shadowRoot.querySelector("#drag-over").style.display = "none";
this.shadowRoot.querySelector("#drag-over").innerHTML = "";
}
}
}
customElements.define("draggable-container", DraggableContainer);
customElements.define("draggable-item", DraggableItem);
-248
View File
@@ -1,248 +0,0 @@
import { requestPVE, requestAPI, goToPage, setTitleAndHeader } from "./utils.js";
import { alert, dialog } from "./dialog.js";
import { setupClientSync } from "./clientsync.js";
import wf_align from "../modules/wfa.js";
window.addEventListener("DOMContentLoaded", init);
let instances = [];
async function init () {
setTitleAndHeader();
const cookie = document.cookie;
if (cookie === "") {
goToPage("login.html");
}
document.querySelector("#instance-add").addEventListener("click", handleInstanceAdd);
document.querySelector("#vm-search").addEventListener("input", populateInstances);
setupClientSync(refreshInstances);
}
async function refreshInstances () {
await getInstances();
await populateInstances();
}
async function getInstances () {
const resources = await requestPVE("/cluster/resources", "GET");
instances = [];
resources.data.forEach((element) => {
if (element.type === "lxc" || element.type === "qemu") {
const nodeName = element.node;
const nodeStatus = resources.data.find(item => item.node === nodeName && item.type === "node").status;
element.node = { name: nodeName, status: nodeStatus };
instances.push(element);
}
});
}
async function populateInstances () {
let searchCriteria = localStorage.getItem("search-criteria");
if (!searchCriteria) {
searchCriteria = "fuzzy";
localStorage.setItem("search-criteria", "fuzzy");
}
const searchQuery = document.querySelector("#search").value || null;
let criteria;
if (!searchQuery) {
criteria = (a, b) => {
return (a.vmid > b.vmid) ? 1 : -1;
};
}
else if (searchCriteria === "exact") {
criteria = (a, b) => {
const aInc = a.name.toLowerCase().includes(searchQuery.toLowerCase());
const bInc = b.name.toLowerCase().includes(searchQuery.toLowerCase());
if (aInc && bInc) {
return a.vmid > b.vmid ? 1 : -1;
}
else if (aInc && !bInc) {
return -1;
}
else if (!aInc && bInc) {
return 1;
}
else {
return a.vmid > b.vmid ? 1 : -1;
}
};
}
else if (searchCriteria === "fuzzy") {
const penalties = {
m: 0,
x: 1,
o: 1,
e: 1
};
criteria = (a, b) => {
// lower is better
const aAlign = wf_align(a.name.toLowerCase(), searchQuery.toLowerCase(), penalties);
const aScore = aAlign.score / a.name.length;
const bAlign = wf_align(b.name.toLowerCase(), searchQuery.toLowerCase(), penalties);
const bScore = bAlign.score / b.name.length;
if (aScore === bScore) {
return a.vmid > b.vmid ? 1 : -1;
}
else {
return aScore - bScore;
}
};
}
instances.sort(criteria);
const instanceContainer = document.querySelector("#instance-container");
instanceContainer.innerHTML = "";
for (let i = 0; i < instances.length; i++) {
const newInstance = document.createElement("instance-card");
instances[i].searchQuery = searchQuery;
newInstance.data = instances[i];
instanceContainer.append(newInstance);
}
}
async function handleInstanceAdd () {
const header = "Create New Instance";
const body = `
<form method="dialog" class="input-grid" style="grid-template-columns: auto 1fr;" id="form">
<label for="type">Instance Type</label>
<select class="w3-select w3-border" name="type" id="type" required>
<option value="lxc">Container</option>
<option value="qemu">Virtual Machine</option>
</select>
<label for="node">Node</label>
<select class="w3-select w3-border" name="node" id="node" required></select>
<label for="name">Name</label>
<input class="w3-input w3-border" name="name" id="name" required></input>
<label for="vmid">ID</label>
<input class="w3-input w3-border" name="vmid" id="vmid" type="number" required></input>
<label for="pool">Pool</label>
<select class="w3-select w3-border" name="pool" id="pool" required></select>
<label for="cores">Cores (Threads)</label>
<input class="w3-input w3-border" name="cores" id="cores" type="number" min="1" max="8192" required></input>
<label for="memory">Memory (MiB)</label>
<input class="w3-input w3-border" name="memory" id="memory" type="number" min="16", step="1" required></input>
<p class="container-specific none" style="grid-column: 1 / span 2; text-align: center;">Container Options</p>
<label class="container-specific none" for="swap">Swap (MiB)</label>
<input class="w3-input w3-border container-specific none" name="swap" id="swap" type="number" min="0" step="1" required disabled></input>
<label class="container-specific none" for="template-image">Template Image</label>
<select class="w3-select w3-border container-specific none" name="template-image" id="template-image" required disabled></select>
<label class="container-specific none" for="rootfs-storage">ROOTFS Storage</label>
<select class="w3-select w3-border container-specific none" name="rootfs-storage" id="rootfs-storage" required disabled></select>
<label class="container-specific none" for="rootfs-size">ROOTFS Size (GiB)</label>
<input class="w3-input w3-border container-specific none" name="rootfs-size" id="rootfs-size" type="number" min="0" max="131072" required disabled></input>
<label class="container-specific none" for="password">Password</label>
<input class="w3-input w3-border container-specific none" name="password" id="password" type="password" required disabled></input>
</form>
`;
const templates = await requestAPI("/user/ct-templates", "GET");
const d = dialog(header, body, async (result, form) => {
if (result === "confirm") {
const body = {
name: form.get("name"),
cores: form.get("cores"),
memory: form.get("memory"),
pool: form.get("pool")
};
if (form.get("type") === "lxc") {
body.swap = form.get("swap");
body.password = form.get("password");
body.ostemplate = form.get("template-image");
body.rootfslocation = form.get("rootfs-storage");
body.rootfssize = form.get("rootfs-size");
}
const node = form.get("node");
const type = form.get("type");
const vmid = form.get("vmid");
const result = await requestAPI(`/cluster/${node}/${type}/${vmid}/create`, "POST", body);
if (result.status === 200) {
populateInstances();
}
else {
alert(result.error);
populateInstances();
}
}
});
const typeSelect = d.querySelector("#type");
typeSelect.selectedIndex = -1;
typeSelect.addEventListener("change", () => {
if (typeSelect.value === "qemu") {
d.querySelectorAll(".container-specific").forEach((element) => {
element.classList.add("none");
element.disabled = true;
});
}
else {
d.querySelectorAll(".container-specific").forEach((element) => {
element.classList.remove("none");
element.disabled = false;
});
}
});
const rootfsContent = "rootdir";
const rootfsStorage = d.querySelector("#rootfs-storage");
rootfsStorage.selectedIndex = -1;
const userResources = await requestAPI("/user/dynamic/resources", "GET");
const userCluster = await requestAPI("/user/config/cluster", "GET");
const nodeSelect = d.querySelector("#node");
const clusterNodes = await requestPVE("/nodes", "GET");
const allowedNodes = Object.keys(userCluster.nodes);
clusterNodes.data.forEach((element) => {
if (element.status === "online" && allowedNodes.includes(element.node)) {
nodeSelect.add(new Option(element.node));
}
});
nodeSelect.selectedIndex = -1;
nodeSelect.addEventListener("change", async () => { // change rootfs storage based on node
const node = nodeSelect.value;
const storage = await requestPVE(`/nodes/${node}/storage`, "GET");
storage.data.forEach((element) => {
if (element.content.includes(rootfsContent)) {
rootfsStorage.add(new Option(element.storage));
}
});
rootfsStorage.selectedIndex = -1;
// set core and memory min/max depending on node selected
if (node in userResources.cores.nodes) {
d.querySelector("#cores").max = userResources.cores.nodes[node].avail;
}
else {
d.querySelector("#cores").max = userResources.cores.global.avail;
}
if (node in userResources.memory.nodes) {
d.querySelector("#memory").max = userResources.memory.nodes[node].avail;
}
else {
d.querySelector("#memory").max = userResources.memory.global.avail;
}
});
// set vmid min/max
d.querySelector("#vmid").min = userCluster.vmid.min;
d.querySelector("#vmid").max = userCluster.vmid.max;
// add user pools to selector
const poolSelect = d.querySelector("#pool");
const userPools = Object.keys(userCluster.pools);
userPools.forEach((element) => {
poolSelect.add(new Option(element));
});
poolSelect.selectedIndex = -1;
// add template images to selector
const templateImage = d.querySelector("#template-image"); // populate templateImage depending on selected image storage
for (const template of templates) {
templateImage.append(new Option(template.name, template.volid));
}
templateImage.selectedIndex = -1;
}
-252
View File
@@ -1,252 +0,0 @@
import { requestPVE, requestAPI, goToPage, goToURL, instancesConfig, nodesConfig } from "./utils.js";
import { PVE } from "../vars.js";
import { dialog } from "./dialog.js";
class InstanceCard extends HTMLElement {
constructor () {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.innerHTML = `
<link rel="stylesheet" href="w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="css/style.css">
<style>
* {
margin: 0;
}
</style>
<div class="w3-row" style="margin-top: 1em; margin-bottom: 1em;">
<hr class="w3-show-small w3-hide-medium w3-hide-large" style="margin: 0; margin-bottom: 1em;">
<div class="w3-col l1 m2 s6">
<p id="instance-id"></p>
</div>
<div class="w3-col l2 m3 s6">
<p id="instance-name"></p>
</div>
<div class="w3-col l1 m2 w3-hide-small">
<p id="instance-type"></p>
</div>
<div class="w3-col l2 m3 s6 flex row nowrap">
<img id="instance-status-icon">
<p id="instance-status"></p>
</div>
<div class="w3-col l2 w3-hide-medium w3-hide-small">
<p id="node-name"></p>
</div>
<div class="w3-col l2 w3-hide-medium w3-hide-small flex row nowrap">
<img id="node-status-icon">
<p id="node-status"></p>
</div>
<div class="w3-col l2 m2 s6 flex row nowrap" style="height: 1lh;">
<img id="power-btn">
<img id="console-btn">
<img id="configure-btn">
<img id="delete-btn">
</div>
</div>
`;
this.actionLock = false;
}
get data () {
return {
type: this.type,
status: this.status,
vmid: this.status,
name: this.name,
node: this.node,
searchQuery: this.searchQuery
};
}
set data (data) {
if (data.status === "unknown") {
data.status = "stopped";
}
this.type = data.type;
this.status = data.status;
this.vmid = data.vmid;
this.name = data.name;
this.node = data.node;
this.searchQuery = data.searchQuery;
this.update();
}
update () {
const vmidParagraph = this.shadowRoot.querySelector("#instance-id");
vmidParagraph.innerText = this.vmid;
const nameParagraph = this.shadowRoot.querySelector("#instance-name");
if (this.searchQuery) {
const regExpEscape = v => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const escapedQuery = regExpEscape(this.searchQuery);
const searchRegExp = new RegExp(`(${escapedQuery})`, "gi");
const nameParts = this.name.split(searchRegExp);
for (let i = 0; i < nameParts.length; i++) {
const part = document.createElement("span");
part.innerText = nameParts[i];
if (nameParts[i].toLowerCase() === this.searchQuery.toLowerCase()) {
part.style = "color: var(--lightbg-text-color); background-color: var(--highlight-color);";
}
nameParagraph.append(part);
}
}
else {
nameParagraph.innerHTML = this.name ? this.name : "&nbsp;";
}
const typeParagraph = this.shadowRoot.querySelector("#instance-type");
typeParagraph.innerText = this.type;
const statusParagraph = this.shadowRoot.querySelector("#instance-status");
statusParagraph.innerText = this.status;
const statusIcon = this.shadowRoot.querySelector("#instance-status-icon");
statusIcon.src = instancesConfig[this.status].status.src;
statusIcon.alt = instancesConfig[this.status].status.alt;
const nodeNameParagraph = this.shadowRoot.querySelector("#node-name");
nodeNameParagraph.innerText = this.node.name;
const nodeStatusParagraph = this.shadowRoot.querySelector("#node-status");
nodeStatusParagraph.innerText = this.node.status;
const nodeStatusIcon = this.shadowRoot.querySelector("#node-status-icon");
nodeStatusIcon.src = nodesConfig[this.node.status].status.src;
nodeStatusIcon.alt = nodesConfig[this.node.status].status.src;
const powerButton = this.shadowRoot.querySelector("#power-btn");
powerButton.src = instancesConfig[this.status].power.src;
powerButton.alt = instancesConfig[this.status].power.alt;
powerButton.title = instancesConfig[this.status].power.alt;
if (instancesConfig[this.status].power.clickable) {
powerButton.classList.add("clickable");
powerButton.onclick = this.handlePowerButton.bind(this);
}
const configButton = this.shadowRoot.querySelector("#configure-btn");
configButton.src = instancesConfig[this.status].config.src;
configButton.alt = instancesConfig[this.status].config.alt;
configButton.title = instancesConfig[this.status].config.alt;
if (instancesConfig[this.status].config.clickable) {
configButton.classList.add("clickable");
configButton.onclick = this.handleConfigButton.bind(this);
}
const consoleButton = this.shadowRoot.querySelector("#console-btn");
consoleButton.src = instancesConfig[this.status].console.src;
consoleButton.alt = instancesConfig[this.status].console.alt;
consoleButton.title = instancesConfig[this.status].console.alt;
if (instancesConfig[this.status].console.clickable) {
consoleButton.classList.add("clickable");
consoleButton.onclick = this.handleConsoleButton.bind(this);
}
const deleteButton = this.shadowRoot.querySelector("#delete-btn");
deleteButton.src = instancesConfig[this.status].delete.src;
deleteButton.alt = instancesConfig[this.status].delete.alt;
deleteButton.title = instancesConfig[this.status].delete.alt;
if (instancesConfig[this.status].delete.clickable) {
deleteButton.classList.add("clickable");
deleteButton.onclick = this.handleDeleteButton.bind(this);
}
if (this.node.status !== "online") {
powerButton.classList.add("hidden");
configButton.classList.add("hidden");
consoleButton.classList.add("hidden");
deleteButton.classList.add("hidden");
}
}
async handlePowerButton () {
if (!this.actionLock) {
const header = `${this.status === "running" ? "Stop" : "Start"} VM ${this.vmid}`;
const body = `<p>Are you sure you want to ${this.status === "running" ? "stop" : "start"} VM ${this.vmid}</p>`;
dialog(header, body, async (result, form) => {
if (result === "confirm") {
this.actionLock = true;
const targetAction = this.status === "running" ? "stop" : "start";
const targetStatus = this.status === "running" ? "stopped" : "running";
const prevStatus = this.status;
this.status = "loading";
this.update();
const result = await requestPVE(`/nodes/${this.node.name}/${this.type}/${this.vmid}/status/${targetAction}`, "POST", { node: this.node.name, vmid: this.vmid });
const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
while (true) {
const taskStatus = await requestPVE(`/nodes/${this.node.name}/tasks/${result.data}/status`, "GET");
if (taskStatus.data.status === "stopped" && taskStatus.data.exitstatus === "OK") { // task stopped and was successful
this.status = targetStatus;
this.update();
this.actionLock = false;
break;
}
else if (taskStatus.data.status === "stopped") { // task stopped but was not successful
this.status = prevStatus;
alert(`attempted to ${targetAction} ${this.vmid} but process returned stopped:${result.data.exitstatus}`);
this.update();
this.actionLock = false;
break;
}
else { // task has not stopped
await waitFor(1000);
}
}
}
});
}
}
handleConfigButton () {
if (!this.actionLock && this.status === "stopped") { // if the action lock is false, and the node is stopped, then navigate to the conig page with the node infor in the search query
goToPage("config.html", { node: this.node.name, type: this.type, vmid: this.vmid });
}
}
handleConsoleButton () {
if (!this.actionLock && this.status === "running") {
const data = { console: `${this.type === "qemu" ? "kvm" : "lxc"}`, vmid: this.vmid, vmname: this.name, node: this.node.name, resize: "off", cmd: "" };
data[`${this.type === "qemu" ? "novnc" : "xtermjs"}`] = 1;
goToURL(PVE, data, true);
}
}
handleDeleteButton () {
if (!this.actionLock && this.status === "stopped") {
const header = `Delete VM ${this.vmid}`;
const body = `<p>Are you sure you want to <strong>delete</strong> VM ${this.vmid}</p>`;
dialog(header, body, async (result, form) => {
if (result === "confirm") {
this.actionLock = true;
this.status = "loading";
this.update();
const action = {};
action.purge = 1;
action["destroy-unreferenced-disks"] = 1;
const result = await requestAPI(`/cluster/${this.node.name}/${this.type}/${this.vmid}/delete`, "DELETE");
if (result.status === 200) {
if (this.parentElement) {
this.parentElement.removeChild(this);
}
}
else {
alert(result.error);
this.status = this.prevStatus;
this.update();
this.actionLock = false;
}
}
});
}
}
}
customElements.define("instance-card", InstanceCard);
-29
View File
@@ -1,29 +0,0 @@
import { setTitleAndHeader } from "./utils.js";
window.addEventListener("DOMContentLoaded", init);
function init () {
setTitleAndHeader();
const scheme = localStorage.getItem("sync-scheme");
if (scheme) {
document.querySelector(`#sync-${scheme}`).checked = true;
}
const rate = localStorage.getItem("sync-rate");
if (rate) {
document.querySelector("#sync-rate").value = rate;
}
const search = localStorage.getItem("search-criteria");
if (search) {
document.querySelector(`#search-${search}`).checked = true;
}
document.querySelector("#settings").addEventListener("submit", handleSaveSettings, false);
}
function handleSaveSettings (event) {
event.preventDefault();
const form = new FormData(document.querySelector("#settings"));
localStorage.setItem("sync-scheme", form.get("sync-scheme"));
localStorage.setItem("sync-rate", form.get("sync-rate"));
localStorage.setItem("search-criteria", form.get("search-criteria"));
window.location.reload();
}
-269
View File
@@ -1,269 +0,0 @@
import { API, organization } from "../vars.js";
export const resourcesConfig = {
disk: {
actionBarOrder: ["move", "resize", "detach_attach", "delete"],
lxc: {
prefixOrder: ["rootfs", "mp", "unused"],
rootfs: { name: "ROOTFS", icon: "images/resources/drive.svg", actions: ["move", "resize"] },
mp: { name: "MP", icon: "images/resources/drive.svg", actions: ["detach", "move", "reassign", "resize"] },
unused: { name: "UNUSED", icon: "images/resources/drive.svg", actions: ["attach", "delete", "reassign"] }
},
qemu: {
prefixOrder: ["ide", "sata", "unused"],
ide: { name: "IDE", icon: "images/resources/disk.svg", actions: ["delete"] },
sata: { name: "SATA", icon: "images/resources/drive.svg", actions: ["detach", "move", "reassign", "resize"] },
unused: { name: "UNUSED", icon: "images/resources/drive.svg", actions: ["attach", "delete", "reassign"] }
}
},
network: {
prefix: "net"
},
pcie: {
prefix: "hostpci"
}
};
export const instancesConfig = {
running: {
status: {
src: "images/status/active.svg",
alt: "Instance is running",
clickable: false
},
power: {
src: "images/actions/instance/stop.svg",
alt: "Shutdown Instance",
clickable: true
},
config: {
src: "images/actions/instance/config-inactive.svg",
alt: "Change Configuration (Inactive)",
clickable: false
},
console: {
src: "images/actions/instance/console-active.svg",
alt: "Open Console",
clickable: true
},
delete: {
src: "images/actions/delete-inactive.svg",
alt: "Delete Instance (Inactive)",
clickable: false
}
},
stopped: {
status: {
src: "images/status/inactive.svg",
alt: "Instance is stopped",
clickable: false
},
power: {
src: "images/actions/instance/start.svg",
alt: "Start Instance",
clickable: true
},
config: {
src: "images/actions/instance/config-active.svg",
alt: "Change Configuration",
clickable: true
},
console: {
src: "images/actions/instance/console-inactive.svg",
alt: "Open Console (Inactive)",
clickable: false
},
delete: {
src: "images/actions/delete-active.svg",
alt: "Delete Instance",
clickable: true
}
},
loading: {
status: {
src: "images/status/loading.svg",
alt: "Instance is loading",
clickable: false
},
power: {
src: "images/status/loading.svg",
alt: "Loading Instance",
clickable: false
},
config: {
src: "images/actions/instance/config-inactive.svg",
alt: "Change Configuration (Inactive)",
clickable: false
},
console: {
src: "images/actions/instance/console-inactive.svg",
alt: "Open Console (Inactive)",
clickable: false
},
delete: {
src: "images/actions/delete-inactive.svg",
alt: "Delete Instance (Inactive)",
clickable: false
}
}
};
export const nodesConfig = {
online: {
status: {
src: "images/status/active.svg",
alt: "Node is online"
}
},
offline: {
status: {
src: "images/status/inactive.svg",
alt: "Node is offline"
}
},
uknown: {
status: {
src: "images/status/inactive.svg",
alt: "Node is offline"
}
}
};
export const bootConfig = {
eligiblePrefixes: ["ide", "sata", "net"],
ide: {
icon: "images/resources/disk.svg",
alt: "IDE Bootable Icon"
},
sata: {
icon: "images/resources/drive.svg",
alt: "SATA Bootable Icon"
},
net: {
icon: "images/resources/network.svg",
alt: "NET Bootable Icon"
}
};
export function getCookie (cname) {
const name = cname + "=";
const decodedCookie = decodeURIComponent(document.cookie);
const ca = decodedCookie.split(";");
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === " ") {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
export async function requestTicket (username, password, realm) {
const response = await requestAPI("/auth/ticket", "POST", { username: `${username}@${realm}`, password }, false);
return response;
}
export async function requestPVE (path, method, body = null) {
const prms = new URLSearchParams(body);
const content = {
method,
mode: "cors",
credentials: "include",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
};
if (method === "POST") {
content.body = prms.toString();
content.headers.CSRFPreventionToken = getCookie("CSRFPreventionToken");
}
const response = await request(`${API}/proxmox${path}`, content);
return response;
}
export async function requestAPI (path, method, body = null) {
const prms = new URLSearchParams(body);
const content = {
method,
mode: "cors",
credentials: "include",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
}
};
if (method === "POST" || method === "DELETE") {
content.headers.CSRFPreventionToken = getCookie("CSRFPreventionToken");
}
if (body) {
content.body = prms.toString();
}
const response = await request(`${API}${path}`, content);
return response;
}
async function request (url, content) {
try {
const response = await fetch(url, content);
const contentType = response.headers.get("Content-Type");
let data = null;
if (contentType.includes("application/json")) {
data = await response.json();
data.status = response.status;
}
else if (contentType.includes("text/html")) {
data = { data: await response.text() };
data.status = response.status;
}
else {
data = response;
}
if (!response.ok) {
return { status: response.status, error: data ? data.error : response.status };
}
else {
data.status = response.status;
return data || response;
}
}
catch (error) {
return { status: 400, error };
}
}
export function goToPage (page, data = null) {
const params = data ? (new URLSearchParams(data)).toString() : "";
window.location.href = `${page}${data ? "?" : ""}${params}`;
}
export function goToURL (href, data = {}, newwindow = false) {
const url = new URL(href);
for (const k in data) {
url.searchParams.append(k, data[k]);
}
if (newwindow) {
window.open(url, `${organization} - dashboard`, "height=480,width=848");
}
else {
window.location.assign(url.toString());
}
}
export function getURIData () {
const url = new URL(window.location.href);
return Object.fromEntries(url.searchParams);
}
export async function deleteAllCookies () {
await requestAPI("/auth/ticket", "DELETE");
}
export function setTitleAndHeader () {
document.title = `${organization} - dashboard`;
document.querySelector("h1").innerText = organization;
}
-85
View File
@@ -1,85 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>proxmox - dashboard</title>
<link rel="icon" href="images/favicon.svg" sizes="any" type="image/svg+xml">
<link rel="stylesheet" href="w3.css">
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/nav.css">
<link rel="stylesheet" href="css/form.css">
<script src="scripts/settings.js" type="module"></script>
<style>
legend {
margin-bottom: 25px;
}
label {
display: flex;
height: fit-content;
width: 100%;
align-items: center;
justify-content: left;
column-gap: 10px;
}
label + p {
margin-top: 5px;
margin-bottom: 25px;
}
p:last-child {
margin-bottom: 0px;
}
</style>
</head>
<body>
<header>
<h1>proxmox</h1>
<label for="navtoggle">&#9776;</label>
<input type="checkbox" id="navtoggle">
<nav>
<a href="index.html">Instances</a>
<a href="account.html">Account</a>
<a href="settings.html" aria-current="page">Settings</a>
<a href="login.html">Logout</a>
</nav>
</header>
<main>
<section>
<h2>Settings</h2>
<form id = "settings">
<div class="w3-card w3-padding">
<h3>Synchronization Settings</h3>
<fieldset>
<legend>App Sync Method</legend>
<label><input class="w3-radio" type="radio" id="sync-always" name="sync-scheme" value="always" required>Always Sync</label>
<p>App will always periodically synchronize with Proxmox. High resource usage.</p>
<label><input class="w3-radio" type="radio" id="sync-hash" name="sync-scheme" value="hash" required>Check For Sync</label>
<p>App will periodically check for updates and synchronize only if needed. Medium resource usage.</p>
<label><input class="w3-radio" type="radio" id="sync-interrupt" name="sync-scheme" value="interrupt" required>Sync When Needed</label>
<p>App will react to changes and synchronize when changes are made. Low resource usage.</p>
</fieldset>
<fieldset>
<legend>App Sync Frequency</legend>
<div class="input-grid" style="grid-template-columns: auto auto 1fr;">
<p>Sync every</p><input aria-label="sync rate in seconds" class="w3-input w3-border" type="number" id="sync-rate" name="sync-rate" min="1" required><p>Second(s)</p>
</div>
</fieldset>
</div>
<div class="w3-card w3-padding">
<h3>Search Settings</h3>
<fieldset>
<legend>Instance Search Criteria</legend>
<label><input class="w3-radio" type="radio" id="search-exact" name="search-criteria" value="exact" required>Exact Match</label>
<p>Sorts by exact query match in instance name.</p>
<label><input class="w3-radio" type="radio" id="search-fuzzy" name="search-criteria" value="fuzzy" required>Fuzzy Match</label>
<p>Sorts by best matching to worst matching.</p>
</fieldset>
</div>
<div class="w3-container w3-center" id="form-actions">
<button class="w3-button w3-margin" id="save" type="submit">SAVE</button>
</div>
</form>
</section>
</main>
</body>
</html>
-3
View File
@@ -1,3 +0,0 @@
export const API = "https://paas.mydomain.example/api"; // the proxmox-aas api
export const PVE = "https://pve.mydomain.example"; // the proxmox api
export const organization = "mydomain"; // org name used in page title and nav bar
+29 -2
View File
@@ -8,6 +8,16 @@
.input-grid * {
margin-top: 0;
margin-bottom: 0;
padding-top: 8px;
padding-bottom: 8px;
}
.input-grid input {
padding: 8px;
}
.input-grid svg {
padding: 0;
}
.input-grid .last-item {
@@ -50,6 +60,23 @@ input[type="radio"] {
position: inherit;
}
div[draggable="true"] {
cursor: grab;
.w3-select, select {
padding: 8px;
}
.w3-check {
top: 0;
}
/* sibling of input-grid that is not inside another input grid */
:not(.input-grid) .input-grid + * {
display: inline-block;
width: 100%;
margin-top: 5px;
}
dialog {
max-width: calc(min(50%, 80ch));
background-color: var(--main-bg-color);
color: var(--main-text-color);
}
+33 -8
View File
@@ -1,15 +1,19 @@
:root {
--nav-bg-color: black;
--nav-text-color: white;
--nav-header-bg-color: #0f0;
--nav-header-text-color: black;
--nav-link-active-text-color: white;
--nav-link-active-bg-color: var(--main-bg-color, #404040);
--nav-transition-speed: 250ms;
}
@media screen and (prefers-color-scheme: light) {
:root {
@media screen and (prefers-color-scheme: dark) {
:root, :root.dark-theme {
--nav-bg-color: black;
--nav-text-color: white;
--nav-header-bg-color: #0f0;
--nav-header-text-color: black;
--nav-link-active-text-color: white;
--nav-link-active-bg-color: var(--main-bg-color, #404040);
}
:root.light-theme {
--nav-bg-color: black;
--nav-text-color: white;
--nav-header-bg-color: #0f0;
@@ -19,6 +23,27 @@
}
}
@media screen and (prefers-color-scheme: light) {
:root, :root.light-theme {
--nav-bg-color: black;
--nav-text-color: white;
--nav-header-bg-color: #0f0;
--nav-header-text-color: black;
--nav-link-active-text-color: black;
--nav-link-active-bg-color: var(--main-bg-color, white);
}
:root.dark-theme {
--nav-bg-color: black;
--nav-text-color: white;
--nav-header-bg-color: #0f0;
--nav-header-text-color: black;
--nav-link-active-text-color: white;
--nav-link-active-bg-color: var(--main-bg-color, #404040);
}
}
header {
display: grid;
align-items: center;
+173
View File
@@ -0,0 +1,173 @@
:root {
--negative-color: #f00;
--positive-color: #0f0;
--highlight-color: yellow;
--lightbg-text-color: black;
}
@media screen and (prefers-color-scheme: dark) {
:root, :root.dark-theme {
--main-bg-color: #404040;
--main-text-color: white;
--main-card-bg-color: #202020;
--main-card-box-shadow: 0 2px 5px 0 rgb(0 0 0 / 80%), 0 2px 10px 0 rgb(0 0 0 / 80%);
--main-table-header-bg-color: black;
--main-input-bg-color: #404040;
}
:root.light-theme {
--main-bg-color: white;
--main-text-color: black;
--main-card-bg-color: white;
--main-card-box-shadow: 0 2px 5px 0 rgb(0 0 0 / 20%), 0 2px 10px 0 rgb(0 0 0 / 20%);
--main-table-header-bg-color: #808080;
--main-input-bg-color: white;
}
}
@media screen and (prefers-color-scheme: light) {
:root, :root.light-theme {
--main-bg-color: white;
--main-text-color: black;
--main-card-bg-color: white;
--main-card-box-shadow: 0 2px 5px 0 rgb(0 0 0 / 20%), 0 2px 10px 0 rgb(0 0 0 / 20%);
--main-table-header-bg-color: #808080;
--main-input-bg-color: white;
}
:root.dark-theme {
--main-bg-color: #404040;
--main-text-color: white;
--main-card-bg-color: #202020;
--main-card-box-shadow: 0 2px 5px 0 rgb(0 0 0 / 80%), 0 2px 10px 0 rgb(0 0 0 / 80%);
--main-table-header-bg-color: black;
--main-input-bg-color: #404040;
}
}
html {
box-sizing: border-box;
background-color: var(--main-bg-color);
}
* {
font-family: monospace;
}
body {
min-height: 100vh;
max-width: 100vw;
display: grid;
grid-template-rows: auto 1fr;
}
main {
max-width: 100vw;
background-color: var(--main-bg-color);
color: var(--main-text-color);
padding: 0 16px 16px;
}
.w3-card {
background-color: var(--main-card-bg-color);
box-shadow: var(--main-card-box-shadow);
}
.w3-card + .w3-card {
margin-top: 16px;
}
th {
background-color: var(--main-table-header-bg-color);
}
td {
background-color: var(--main-card-bg-color);
}
input, select, textarea {
background-color: var(--main-input-bg-color);
color: var(--main-text-color);
}
img.clickable, svg.clickable {
cursor: pointer;
}
img, svg {
height: 1em;
width: 1em;
color: var(--main-text-color)
}
hr, * {
border-color: var(--main-text-color);
}
.flex {
display: flex;
}
.row {
flex-direction: row;
column-gap: 10px;
align-items: center;
}
.wrap {
flex-wrap: wrap;
row-gap: 10px;
}
.nowrap {
flex-wrap: nowrap;
}
.hidden {
visibility: hidden;
}
.none {
display: none;
}
.spacer {
min-height: 1em;
}
@media screen and (width >= 440px) {
button .large {
display: block;
}
button .small {
display: none;
}
}
@media screen and (width <= 440px) {
button .large {
display: none;
}
button .small {
display: block;
}
}
/* add hide large class similar to w3-hide-medium and w3-hide-small */
@media (width >=993px) {
.w3-hide-large {
display: none !important;
}
}
/* fix edge case in w3-hide-medium where width between 992 and 993 */
@media (width <=993px) and (width >=601px){
.w3-hide-medium{display:none!important}
}
/* fix edge case in w3-hide-small when width between 600 and 601 */
@media (width <=601px) {
.w3-hide-small{display:none!important}
}
+21
View File
@@ -0,0 +1,21 @@
package web
import (
"embed"
)
//go:embed css/*
var CSS_fs embed.FS
//go:embed images/*
var Images_fs embed.FS
//go:embed modules/*
var Modules_fs embed.FS
//go:embed scripts/*
var Scripts_fs embed.FS
//go:embed html/*
//go:embed templates/*
var Templates embed.FS
+77
View File
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{template "head" .}}
<script src="scripts/account.js" type="module"></script>
<link rel="modulepreload" href="scripts/utils.js">
<link rel="modulepreload" href="scripts/dialog.js">
<style>
@media screen and (width >= 1264px){
#resource-container {
display: grid;
grid-template-columns: repeat(auto-fill, calc(100% / 6));
grid-gap: 0;
justify-content: space-between;
}
}
@media screen and (width <= 1264px) and (width >= 680px) {
#resource-container {
display: grid;
grid-template-columns: repeat(auto-fill, 200px);
grid-gap: 0;
justify-content: space-between;
}
}
@media screen and (width <= 680px) {
#resource-container {
display: flex;
flex-direction: column;
gap: 0;
flex-wrap: nowrap;
justify-content: center;
}
}
</style>
</head>
<body>
<header>
{{template "header" .}}
</header>
<main>
<h2>Account</h2>
<section class="w3-card w3-padding">
<h3>Account Details</h3>
<p id="username">Username: {{.account.Username}}</p>
<p id="pool">Pools: {{MapKeys .account.Pools ", "}}</p>
<p id="vmid">VMID Range: {{.account.VMID.Min}} - {{.account.VMID.Max}}</p>
<p id="nodes">Nodes: {{MapKeys .account.Nodes ", "}}</p>
</section>
<section class="w3-card w3-padding">
<div class="flex row nowrap">
<h3>Password</h3>
<button class="w3-button w3-margin" id="change-password" type="button">Change Password</button>
</div>
</section>
<section class="w3-card w3-padding">
<h3>Cluster Resources</h3>
<div id="resource-container">
{{range .account.Resources}}
{{if .Display}}
{{if eq .Type "numeric"}}
{{template "resource-chart" .}}
{{end}}
{{if eq .Type "storage"}}
{{template "resource-chart" .}}
{{end}}
{{if eq .Type "list"}}
{{range .Resources}}
{{template "resource-chart" .}}
{{end}}
{{end}}
{{end}}
{{end}}
</div>
</section>
</main>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
{{template "boot" .config.Boot}}
+1
View File
@@ -0,0 +1 @@
{{template "devices" .config.Devices}}
+1
View File
@@ -0,0 +1 @@
{{template "nets" .config.Nets}}
+1
View File
@@ -0,0 +1 @@
{{template "volumes" .config.Volumes}}
+100
View File
@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{template "head" .}}
<script src="scripts/config.js" type="module"></script>
<script src="scripts/draggable.js" type="module"></script>
<script src="modules/Sortable.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@latest/Sortable.min.js"></script>
<link rel="modulepreload" href="scripts/utils.js">
<link rel="modulepreload" href="scripts/dialog.js">
<style>
.input-grid p, .input-grid div {
margin-top: 0;
margin-bottom: 0;
padding-top: 0;
padding-bottom: 0;
overflow: hidden;
white-space: nowrap;
}
</style>
</head>
<body>
<header>
{{template "header" .}}
</header>
<main>
<section>
<h2><a href="index">Instances</a> / {{.config.Name}}</h2>
<form>
<fieldset class="w3-card w3-padding">
<legend>Resources</legend>
<div class="input-grid" id="resources" style="grid-template-columns: auto auto auto 1fr;">
{{if eq .config.Type "VM"}}
{{template "proctype-input" .config.ProctypeSelect}}
{{end}}
{{template "cores-input" .config.Cores}}
{{template "memory-input" .config.Memory}}
{{if eq .config.Type "CT"}}
{{template "swap-input" .config.Swap}}
{{end}}
</div>
</fieldset>
<fieldset class="w3-card w3-padding">
<legend>Volumes</legend>
<div class="input-grid" id="volumes" style="grid-template-columns: auto auto 1fr auto;">
{{template "volumes" .config.Volumes}}
</div>
<div class="w3-container w3-center">
<button type="button" id="disk-add" class="w3-button" aria-label="Add New Disk">
<span class="large" style="margin: 0;">Add Disk</span>
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New Disk"><use href="images/actions/disk/add-disk.svg#symb"></use></svg>
</button>
{{if eq .config.Type "VM"}}
<button type="button" id="cd-add" class="w3-button" aria-label="Add New CD">
<span class="large" style="margin: 0;">Mount CD</span>
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New CDROM"><use href="images/actions/disk/add-cd.svg#symb"></use></svg>
</button>
{{end}}
</div>
</fieldset>
<fieldset class="w3-card w3-padding">
<legend>Network Interfaces</legend>
<div class="input-grid" id="networks" style="grid-template-columns: auto auto 1fr auto;">
{{template "nets" .config.Nets}}
</div>
<div class="w3-container w3-center">
<button type="button" id="network-add" class="w3-button" aria-label="Add New Network Interface">
<span class="large" style="margin: 0;">Add Network</span>
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New Network Interface"><use href="images/actions/network/add.svg#symb"></use></svg>
</button>
</div>
</fieldset>
{{if eq .config.Type "VM"}}
<fieldset class="w3-card w3-padding">
<legend>PCIe Devices</legend>
<div class="input-grid" id="devices" style="grid-template-columns: auto auto 1fr auto;">
{{template "devices" .config.Devices}}
</div>
<div class="w3-container w3-center">
<button type="button" id="device-add" class="w3-button" aria-label="Add New PCIe Device">
<span class="large" style="margin: 0;">Add Device</span>
<svg class="small" role="img" style="height: 1lh; width: 1lh;" aria-label="Add New PCIe Device"><use href="images/actions/device/add.svg#symb"></use></svg>
</button>
</div>
</fieldset>
<fieldset class="w3-card w3-padding">
<legend>Boot Order</legend>
<div id="boot-order">
{{template "boot" .config.Boot}}
</div>
</fieldset>
{{end}}
<div class="w3-container w3-center" id="form-actions">
<button class="w3-button w3-margin" id="exit" type="button">EXIT</button>
</div>
</form>
</section>
</main>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
{{range .instances}}
{{template "instance-card" .}}
{{end}}
+67
View File
@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{template "head" .}}
<script src="scripts/index.js" type="module"></script>
<script src="modules/wfa.js" type="module"></script>
<link rel="modulepreload" href="scripts/utils.js">
<link rel="modulepreload" href="scripts/dialog.js">
<link rel="modulepreload" href="scripts/clientsync.js">
<style>
#instance-container > div {
border-bottom: 1px solid white;
}
#instance-container > div:last-child {
border-bottom: none;
}
@media screen and (width >= 440px) {
#vm-search {
max-width: calc(100% - 10px - 152px);
}
}
@media screen and (width <= 440px) {
#vm-search {
max-width: calc(100% - 10px - 47px);
}
}
</style>
</head>
<body>
<header>
{{template "header" .}}
</header>
<main>
<section>
<h2>Instances</h2>
<div class="w3-card w3-padding">
<div class="flex row nowrap" style="margin-top: 1em; justify-content: space-between;">
<form id="vm-search" role="search" class="flex row nowrap">
<svg role="img" aria-label="Search Instances"><use href="images/common/search.svg#symb"></use></svg>
<input type="search" id="search" class="w3-input w3-border" style="height: 1lh; max-width: fit-content;" aria-label="search instances by name">
</form>
<button type="button" id="instance-add" class="w3-button" aria-label="create new instance">
<span class="large" style="margin: 0;">Create Instance</span>
<svg class="small" style="height: 1lh; width: 1lh;" role="img" aria-label="Create New Instance"><use href="images/actions/instance/add.svg#symb"></use></svg>
</button>
</div>
<div>
<div class="w3-row w3-hide-small" style="border-bottom: 1px solid;">
<p class="w3-col l1 m2 w3-hide-small">ID</p>
<p class="w3-col l2 m3 w3-hide-small">Name</p>
<p class="w3-col l1 m2 w3-hide-small">Type</p>
<p class="w3-col l2 m3 w3-hide-small">Status</p>
<p class="w3-col l2 w3-hide-medium w3-hide-small">Host Name</p>
<p class="w3-col l2 w3-hide-medium w3-hide-small">Host Status</p>
<p class="w3-col l2 m2 w3-hide-small">Actions</p>
</div>
<div id="instance-container">
{{range .instances}}
{{template "instance-card" .}}
{{end}}
</div>
</div>
</div>
</section>
</main>
</body>
</html>
+30
View File
@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{template "head" .}}
<script src="scripts/login.js" type="module"></script>
<link rel="modulepreload" href="scripts/utils.js">
<link rel="modulepreload" href="scripts/dialog.js">
</head>
<body>
<header>
{{template "header" .}}
</header>
<main class="flex" style="justify-content: center; align-items: center;">
<div class="w3-container w3-card w3-margin w3-padding" style="height: fit-content;">
<h2 class="w3-center">{{.global.Organization}} Login</h2>
<form>
<label for="username"><b>Username</b></label>
<input class="w3-input w3-border" id="username" name="username" type="text" autocomplete="username">
<label for="password"><b>Password</b></label>
<input class="w3-input w3-border" id="password" name="password" type="password" autocomplete="current-password">
<label for="realm">Realm</label>
{{template "select" .realms}}
<div class="w3-center">
<button class="w3-button w3-margin" id="submit" type="submit">LOGIN</button>
</div>
</form>
</div>
</main>
</body>
</html>
+81
View File
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{template "head" .}}
<script src="scripts/settings.js" type="module"></script>
<link rel="modulepreload" href="scripts/utils.js">
<style>
legend {
margin-bottom: 25px;
}
label {
display: flex;
height: fit-content;
width: 100%;
align-items: center;
justify-content: left;
column-gap: 10px;
}
label + p {
margin-top: 5px;
margin-bottom: 25px;
}
p:last-child {
margin-bottom: 0px;
}
</style>
</head>
<body>
<header>
{{template "header" .}}
</header>
<main>
<h2>Settings</h2>
<form id="settings">
<section class="w3-card w3-padding">
<h3>Synchronization Settings</h3>
<fieldset>
<legend>App Sync Method</legend>
<label><input class="w3-radio" type="radio" id="sync-always" name="sync-scheme" value="always" required>Always Sync</label>
<p>App will always periodically synchronize with Proxmox. High resource usage.</p>
<label><input class="w3-radio" type="radio" id="sync-hash" name="sync-scheme" value="hash" required>Check For Sync</label>
<p>App will periodically check for updates and synchronize only if needed. Medium resource usage.</p>
<label><input class="w3-radio" type="radio" id="sync-interrupt" name="sync-scheme" value="interrupt" required>Sync When Needed</label>
<p>App will react to changes and synchronize when changes are made. Low resource usage.</p>
</fieldset>
<fieldset>
<legend>App Sync Frequency</legend>
<div class="input-grid" style="grid-template-columns: auto auto 1fr;">
<p>Sync every</p><input aria-label="sync rate in seconds" class="w3-input w3-border" type="number" id="sync-rate" name="sync-rate" min="1" required><p>Second(s)</p>
</div>
</fieldset>
</section>
<section class="w3-card w3-padding">
<h3>Search Settings</h3>
<fieldset>
<legend>Instance Search Criteria</legend>
<label><input class="w3-radio" type="radio" id="search-exact" name="search-criteria" value="exact" required>Exact Match</label>
<p>Sorts by exact query match in instance name.</p>
<label><input class="w3-radio" type="radio" id="search-fuzzy" name="search-criteria" value="fuzzy" required>Fuzzy Match</label>
<p>Sorts by best matching to worst matching.</p>
</fieldset>
</section>
<section class="w3-card w3-padding">
<h3>Appearance</h3>
<fieldset>
<legend>Default Theme</legend>
<label>Theme<select class="w3-select w3-border" id="appearance-theme" name="appearance-theme" style="width: fit-content; padding-right: 24px;">
<option value="auto">Auto</option>
<option value="dark">Dark</option>
<option value="light">Light</option>
</select>
</label>
</fieldset>
</section>
<div class="w3-container w3-center" id="form-actions">
<button class="w3-button w3-margin" id="save" type="submit">SAVE</button>
</div>
</form>
</main>
</body>
</html>
+1
View File
@@ -0,0 +1 @@
../../common/add.svg
+1
View File
@@ -0,0 +1 @@
../../common/config.svg
+1
View File
@@ -0,0 +1 @@
../../common/delete-active.svg
+1
View File
@@ -0,0 +1 @@
../../common/delete-inactive.svg
+1
View File
@@ -0,0 +1 @@
<svg id="symb" role="img" aria-label="add cd" viewBox="2.3 2.3 19.4 19.4" fill="none" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g stroke="currentColor" stroke-width="1.25"><path d="M7 12h5m0 0h5m-5 0V7m0 5v5"/><circle cx="12" cy="12" r="9"/></g></svg>

After

Width:  |  Height:  |  Size: 331 B

+1
View File
@@ -0,0 +1 @@
<svg id="symb" role="img" aria-label="create disk" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><g fill="currentColor"><path d="M25 0H7a7 7 0 00-7 7v18a7 7 0 007 7h18a7 7 0 007-7V7a7 7 0 00-7-7zm5 25a5 5 0 01-5 5H7a5 5 0 01-5-5V7a5 5 0 015-5h18a5 5 0 015 5z"/><path d="M17 6h-2v9H6v2h9v9h2v-9h9v-2h-9V6z"/></g></svg>

After

Width:  |  Height:  |  Size: 412 B

+1
View File
@@ -0,0 +1 @@
<svg id="symb" role="img" aria-label="attach disk" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg"><g fill="#0f0"><path d="M17 12h-2.85a6.25 6.25 0 00-6.21 5H2v2h5.93a6.22 6.22 0 006.22 5H17z" class="prefix__prefix__clr-i-solid prefix__prefix__clr-i-solid-path-1"/><path d="M28.23 17A6.25 6.25 0 0022 12h-3v12h3a6.22 6.22 0 006.22-5H34v-2z" class="prefix__prefix__clr-i-solid prefix__prefix__clr-i-solid-path-2"/><path fill="none" d="M0 0h36v36H0z"/></g></svg>

After

Width:  |  Height:  |  Size: 467 B

+1
View File
@@ -0,0 +1 @@
../../common/delete-active.svg
+1
View File
@@ -0,0 +1 @@
../../common/delete-inactive.svg
+1
View File
@@ -0,0 +1 @@
<svg id="symb" role="img" aria-label="detach disk" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="red" d="M76.987 235.517H0v40.973h76.987c9.04 33.686 39.694 58.522 76.238 58.522h57.062V176.988h-57.062c-36.543 0-67.206 24.836-76.238 58.529zm435.013 0h-76.995c-9.032-33.693-39.686-58.53-76.23-58.53h-57.062v158.024h57.062c36.537 0 67.19-24.836 76.23-58.522H512v-40.972z"/></svg>

After

Width:  |  Height:  |  Size: 398 B

@@ -0,0 +1 @@
<svg id="symb" role="img" aria-label="" xmlns="http://www.w3.org/2000/svg"/>

After

Width:  |  Height:  |  Size: 76 B

+1
View File
@@ -0,0 +1 @@
<svg id="symb" role="img" aria-label="move disk" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M16 7l5 5m0 0l-5 5m5-5H3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 318 B

@@ -0,0 +1 @@
<svg id="symb" role="img" aria-label="" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M16 7l5 5m0 0l-5 5m5-5H3" stroke="gray" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 215 B

@@ -0,0 +1 @@
<svg id="symb" role="img" aria-label="resize disk" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>:root{color:#fff}@media (prefers-color-scheme:light){:root{color:#000}}</style><path d="M12 20a1 1 0 01-1-1v-6H5a1 1 0 010-2h6V5a1 1 0 012 0v6h6a1 1 0 010 2h-6v6a1 1 0 01-1 1z" fill="currentColor"/></svg>

After

Width:  |  Height:  |  Size: 317 B

@@ -0,0 +1 @@
<svg id="symb" role="img" aria-label="" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 20a1 1 0 01-1-1v-6H5a1 1 0 010-2h6V5a1 1 0 012 0v6h6a1 1 0 010 2h-6v6a1 1 0 01-1 1z" fill="gray"/></svg>

After

Width:  |  Height:  |  Size: 212 B

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