569 Commits
Author SHA1 Message Date
Arthur Lu 424940dc6c add fuzzy search using wfa 2024-06-05 22:11:53 +00:00
Arthur Lu fcdeec94d9 fix lint 2024-04-27 03:50:29 +00:00
Arthur Lu db3437894d update for new pool and node format 2024-04-16 21:38:25 +00:00
Arthur Lu 3191927b27 fix styling 2024-01-29 21:49:19 +00:00
Arthur Lu f6e370db2c fix instance power button prompt 2023-12-23 02:07:25 +00:00
Arthur Lu 3521228366 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
Arthur Lu 6fe526be6e fix linting 2023-11-14 22:24:26 +00:00
Arthur Lu bce774d48c 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
Arthur Lu 46773d290d fix bug with sync and deleting instance 2023-11-07 05:09:28 +00:00
Arthur Lu a736c3019e add password form to account,
fix form input grid styling
2023-10-31 18:13:40 +00:00
Arthur Lu 9b3a1e8cad fix nav styling,
fix linting
2023-10-19 17:26:37 +00:00
Arthur Lu 4e4b63f63a add bottom padding to main,
improve error handling with request function
2023-10-18 19:33:58 +00:00
Arthur Lu 54ecfaf782 improve account page loading time 2023-10-17 16:42:33 +00:00
Arthur Lu 5f9908df50 fix handling of instances with empty name,
improve instance table headers
2023-10-16 22:32:19 +00:00
Arthur Lu 71ecc2840e add local copy of w3.css fallback,
change how the dashboard and api is proxied
2023-10-12 17:27:37 +00:00
Arthur Lu b0694ce8c1 fix linting issues 2023-10-11 20:50:10 +00:00
Arthur Lu 36ceb0d1fa remove hover effects for small charts 2023-10-11 00:26:25 +00:00
Arthur Lu c427fd0a72 improve resource chart interface 2023-10-09 23:30:39 +00:00
Arthur Lu 2714272ccf update readme 2023-10-05 21:09:31 +00:00
Arthur Lu 179e713411 add alternate chart layout for narrow screens 2023-10-05 16:38:25 +00:00
Arthur Lu fb84ca5616 fix search regexp escape,
wrap chartjs with custom element
2023-10-04 18:59:46 +00:00
Arthur Lu 9d832fea69 fix draggable getDragSource docstring 2023-09-29 21:17:34 +00:00
Arthur Lu e9504bdf94 fix nav.css style issue,
make search feature not case sensitive
2023-09-28 01:38:19 +00:00
Arthur Lu 7dd25a0f94 fix aria-current value for nav bar to "page",
improve nav css readability
2023-09-26 19:15:54 +00:00
Arthur Lu 440c40188b fix config form icon alts 2023-09-21 22:41:16 +00:00
Arthur Lu 5f409a5bbb 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
Arthur Lu 13c036b3f9 fix linting errors 2023-09-19 20:59:28 +00:00
Arthur Lu 0057e6b873 implement simple search logic 2023-09-18 19:37:07 +00:00
Arthur Lu 6d8ac3181b finish renaming,
improve search bar and add instance responsiveness
2023-09-15 22:13:21 +00:00
Arthur Lu 6d9b5279fb change name from client to dashboard 2023-09-15 07:02:42 +00:00
Arthur Lu bdd428215b add search form layout to main page,
add search icon
2023-09-14 20:38:23 +00:00
Arthur Lu 3e54b48321 improve draggable item style 2023-09-13 20:03:36 +00:00
Arthur Lu 53055773b1 update client user resource data format 2023-09-11 19:04:07 +00:00
Arthur Lu 9f1ebcde77 improve draggable-item custom element interface 2023-09-07 21:42:19 +00:00
Arthur Lu 17596ab1aa add autocomplete attribute for login form inputs,
fix config form labels
2023-09-07 02:44:11 +00:00
Arthur Lu 2b967250f9 improve responsiveness of account resources 2023-09-05 21:47:39 +00:00
Arthur Lu a5d6e0f30a fix missing label for rate sync in settings.html 2023-09-02 00:21:03 +00:00
Arthur Lu 06e75eb186 display account resource usage in charts 2023-08-31 19:01:58 +00:00
Arthur Lu 2ec4ac30fc improve draggable item generalization 2023-08-29 21:26:57 +00:00
Arthur Lu 948868af73 fix linting 2023-08-23 22:55:48 +00:00
Arthur Lu 42a4778de8 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
Arthur Lu 1827d5c73e add drag icon to draggable items 2023-08-19 03:32:52 +00:00
Arthur Lu b9eac10811 fix draggable list responsiveness,
add event handlers for adding/removing boot options
2023-08-16 23:14:42 +00:00
Arthur Lu 7614328555 use custom element for instances 2023-08-15 20:27:45 +00:00
Arthur Lu 888e8bbc7e add draggable list custom element,
improve boot order list
2023-08-14 23:42:02 +00:00
Arthur Lu 7e01b5d730 add boot order draggable list mockup 2023-08-11 22:06:26 +00:00
Arthur Lu 9755511fe6 fix add network and device icon 2023-08-11 03:28:12 +00:00
Arthur Lu 52519fcbd1 add boot order card in config 2023-08-09 21:52:55 +00:00
Arthur Lu 9f68da61cb set title and header in settings page 2023-08-09 00:33:51 +00:00
Arthur Lu fabc934be6 use /cluster/* api routes 2023-08-03 00:35:56 +00:00
Arthur Lu c230adeb18 fix linting errors 2023-07-28 18:32:04 +00:00
Arthur Lu 2930d02ece improve settings page 2023-07-20 22:00:54 +00:00
Arthur Lu d923ed046d change vars.js.template to tempalte.vars.js,
implement interrupt based client sync
2023-07-13 22:53:57 +00:00
Arthur Lu 93e120edb5 add default sync sheme 2023-07-12 22:46:59 +00:00
Arthur Lu 0e07557375 implement always and hash sync 2023-07-12 22:37:08 +00:00
Arthur Lu e8d3fa560d add settings page 2023-07-12 00:24:16 +00:00
Arthur Lu ab257e907a update sync endpoint 2023-07-11 22:07:08 +00:00
Arthur Lu f8d59fd9c9 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
Arthur Lu 2b161d44d8 fix add new instance handler 2023-07-11 00:22:49 +00:00
Arthur Lu 1bdb0ae5a0 fix input grid positioning 2023-07-06 05:02:04 +00:00
Arthur Lu 285d2ea57e use float left on legend and input-grid to remove legend span nesting,
improve linting tool
2023-07-06 04:53:47 +00:00
Arthur Lu 488de43d2f add pci devices to account 2023-07-05 23:29:14 +00:00
Arthur Lu b8a293837b update client with new user config endpoint paths 2023-07-05 23:16:20 +00:00
Arthur Lu 24c1ac00a5 use new endpoint params 2023-07-04 04:41:39 +00:00
Arthur Lu 7e7155926b add html validation 2023-06-30 01:21:47 +00:00
Arthur Lu d04aeaf3f4 fix utils.js 2023-06-30 00:34:22 +00:00
Arthur Lu 95f1aceb1f add css linting, fix css linting issues 2023-06-29 22:39:21 +00:00
Arthur Lu 459376ba66 remove todo 2023-06-29 22:21:17 +00:00
Arthur Lu 392148852a add js linting, fix js linting issues 2023-06-29 22:20:15 +00:00
Arthur Lu 5b1aaea2cb fix nav bar width 2023-06-29 21:38:14 +00:00
Arthur Lu 87c1c50355 remove user vlan tag info,
update client with new api route
2023-06-29 02:08:52 +00:00
Arthur Lu 426e27812f fix pci device pcie form get 2023-06-23 03:34:44 +00:00
Arthur Lu 2eb874580b fix issues in pci device config and add 2023-06-23 03:03:28 +00:00
Arthur Lu bb069f9d5a fix bugs in pcie device config dialogs 2023-06-22 00:33:53 +00:00
Arthur Lu 41e28f4c4b add device config/add/delete handlers,
fix some styling with forms
2023-06-19 01:43:52 +00:00
Arthur Lu b946fcc773 shorten cpu config labels
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-14 22:54:53 +00:00
Arthur Lu 22c97fc026 populate processor types from config values
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-14 22:34:48 +00:00
Arthur Lu 0f29d3b40e fix proc type selector issue in config,
update account resource table with new API format
2023-06-14 04:56:49 +00:00
Arthur Lu 73ea740600 add processor type to config 2023-06-14 01:13:06 +00:00
Arthur Lu d34874ca02 remove clickable from inactive action buttons 2023-06-13 03:13:22 +00:00
Arthur Lu b87b80f9dd improve small screen instance list 2023-06-09 07:00:21 +00:00
Arthur Lu c8f8737582 fix config page styling 2023-06-09 06:55:14 +00:00
Arthur Lu a0c2599f4e implement network add and delete 2023-06-09 04:25:53 +00:00
Arthur Lu 90125fc24b add vlan tag to account info 2023-06-09 00:28:26 +00:00
Arthur Lu 262a54df2b update config.js to api route changes
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-08 23:33:55 +00:00
Arthur Lu ee661b3dca 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
Arthur Lu 85dd99000d fix config pci device population
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-02 23:15:26 +00:00
Arthur Lu 8a930228c6 display device_names in pcie devices config
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-06-01 17:21:35 +00:00
Arthur Lu aebab2222c add pcie device list to instance config
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-30 15:50:32 +00:00
Arthur Lu eb61b95c79 fix network name label in config
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-19 20:07:34 +00:00
Arthur Lu c254ba302b fix bugs in api changes
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-18 20:19:57 +00:00
Arthur Lu 572386b871 update account nodes to new route format
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-18 20:08:39 +00:00
Arthur Lu 6182a87e2c update disk attach for new api change 2023-05-18 08:09:23 +00:00
Arthur Lu cf316fbee0 fix formatting 2023-05-17 21:43:03 +00:00
Arthur Lu 7141c0d23c fix formatting 2023-05-17 21:40:37 +00:00
Arthur Lu 2bb73ce828 fix account network usage
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-16 16:51:39 +00:00
Arthur Lu 0f149ba5c7 remove org specific values,
add dynamic org handling

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-16 15:18:36 +00:00
Arthur Lu d6b576abcc 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
Arthur Lu 4fbc4ee868 fix bug in instance delete
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-15 21:24:43 +00:00
Arthur Lu 2fed6e6134 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
Arthur Lu d9b56ecc48 reduce div nesting in instances
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-15 20:41:31 +00:00
Arthur Lu a645102684 consolidate instance.js into index.js
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-15 20:36:28 +00:00
Arthur Lu b5c181b818 code cleanup
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-15 20:16:38 +00:00
Arthur Lu 56bf77df96 fix some styling 2023-05-15 02:56:48 +00:00
Arthur Lu c6d5d5dbc5 use safer pve ticket endpoint 2023-05-13 07:28:09 +00:00
Arthur Lu 119efcda33 display errors as modal dialogs
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-12 21:26:26 +00:00
Arthur Lu c53fb01599 add margin to table
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-12 21:17:29 +00:00
Arthur Lu 54533e213b 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
Arthur Lu f5b0c29109 fix requestPVE call params 2023-05-11 07:13:41 +00:00
Arthur Lu 4124b88b42 remove extra divs for hamburger nav 2023-05-11 06:13:00 +00:00
Arthur Lu 6b08ca1df7 improve nav bar styling 2023-05-11 06:03:37 +00:00
Arthur Lu d9cd651bb4 fix add-cd and add-disk
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-10 22:10:23 +00:00
Arthur Lu 8a8e5e5c47 add color scheme styling for svg icons
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-10 22:01:41 +00:00
Arthur Lu b8ec177877 add colors for light prefered color scheme
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-10 21:32:41 +00:00
Arthur Lu e509c22e3a use vars in more color styles 2023-05-08 02:27:14 +00:00
Arthur Lu af3b6c1143 use css vars for colors 2023-05-08 00:50:14 +00:00
Arthur Lu e2b74b1c5d consolidate h1 styles 2023-05-08 00:02:47 +00:00
Arthur Lu 8cc6b49fbe fix dialog styling 2023-05-07 04:35:44 +00:00
Arthur Lu d74565921e fix icons and some styling 2023-05-07 04:31:04 +00:00
Arthur Lu af73b029db dark mode for client 2023-05-07 04:15:22 +00:00
Arthur Lu 6badb7d156 fix heigh issue with main
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-05 21:44:08 +00:00
Arthur Lu aff0d6aac8 code cleanup on custom dialogs
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-05 21:43:15 +00:00
Arthur Lu d9e676cbd5 fix responsiveness of account resource table on large screens 2023-05-05 05:03:12 +00:00
Arthur Lu 2eb3670bc3 improve responsiveness for very narrow screens 2023-05-05 05:01:05 +00:00
Arthur Lu 3a79b8f5c8 add transition to hamburger menu
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-03 22:08:00 +00:00
Arthur Lu 9d5d18619f improve hanburger menu style
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-03 21:39:26 +00:00
Arthur Lu 0b66410a1d fix login for styling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-05-03 21:25:55 +00:00
Arthur Lu 8edca1eed0 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
Arthur Lu 35d35af59a small style improvements 2023-05-02 03:42:58 +00:00
Arthur Lu d047bd7cca improve styling for account page
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-28 23:57:42 +00:00
Arthur Lu 9703faa1fa 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
Arthur Lu 35376e972a change some styling for index and config
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-28 23:43:09 +00:00
Arthur Lu 6e78ab352b Update README.md 2023-04-26 16:36:06 -07:00
Arthur Lu e3def16c34 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
Arthur Lu 701f254424 change nav styling and responsiveness
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-25 21:25:03 +00:00
Arthur Lu 927ae672a6 update client with new instance resource api path
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-25 15:32:34 +00:00
Arthur Lu 7a4216e255 handle negative resources in account
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-25 15:20:59 +00:00
Arthur Lu 83a310095f update client for api changes
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-24 21:47:03 +00:00
Arthur Lu 1be6566527 adjust some styling 2023-04-22 04:49:17 +00:00
Arthur Lu 5d5b805d05 improve mobile styling of instance actions 2023-04-22 04:34:48 +00:00
Arthur Lu 47cc47bee6 fix mobile styling for instance obj 2023-04-22 04:32:09 +00:00
Arthur Lu 46116e32f4 add instance info to account 2023-04-21 22:58:15 +00:00
Arthur Lu 81dc02bd98 shorten table headers 2023-04-21 22:24:58 +00:00
Arthur Lu dee465022f fix table responsiveness 2023-04-21 22:22:19 +00:00
Arthur Lu 90193b3cf4 add w3-responsive to table 2023-04-21 22:16:21 +00:00
Arthur Lu fb6395f094 Merge commit '04b6fe2b5f4d10e9ce14790d07e6e3f96f36ed34' 2023-04-20 21:39:09 +00:00
Arthur Lu 6fd6d06978 minor change to user data
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-20 21:38:47 +00:00
Arthur Lu 04b6fe2b5f Update README.md 2023-04-20 14:26:51 -07:00
Arthur Lu 41c3ba3dd3 fix login page error handling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-19 21:24:34 +00:00
Arthur Lu ce9ab6c322 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
Arthur Lu 87dbca57f6 Merge commit 'e8cbcbf4fca643e08ec4b7e36924c633903f52f3' 2023-04-19 05:59:01 +00:00
Arthur Lu e9ec759940 fix cookie check logic 2023-04-19 05:58:30 +00:00
Arthur Lu e8cbcbf4fc Update README.md 2023-04-18 22:54:20 -07:00
Arthur Lu 9f6bf6a8ad add units to non compact values 2023-04-19 03:12:50 +00:00
Arthur Lu 4bfb619c25 add units to resource allocation 2023-04-19 03:10:51 +00:00
Arthur Lu 91278ed505 fix bug in resource table 2023-04-19 01:42:18 +00:00
Arthur Lu 23e5445995 update user db keys 2023-04-19 01:40:55 +00:00
Arthur Lu a7de1b7366 use new resource return format
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-18 23:31:54 +00:00
Arthur Lu ed7c79df78 improve login style and logic
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-18 20:08:59 +00:00
Arthur Lu 069b3d197f improve responsive scaling with medium screens
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-12 22:59:57 +00:00
Arthur Lu 85a73b3a63 improve styling in config form
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-12 22:47:05 +00:00
Arthur Lu ac2f5b8a21 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
Arthur Lu 6d5488abd6 improve styling for dialogs
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-12 22:23:40 +00:00
Arthur Lu 1dc42baeb1 update dialogs to use w3 styling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-11 22:54:37 +00:00
Arthur Lu b656563d61 Update README.md 2023-04-11 15:34:32 -07:00
Arthur Lu ef1ea14f34 Update README.md 2023-04-11 15:31:43 -07:00
Arthur Lu 06b4bef6dd code cleanup
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-11 21:46:18 +00:00
Arthur Lu 1861255d23 fix some mobile responsiveness issues
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-11 21:42:59 +00:00
Arthur Lu 484ed1b92a use w3.css for primary styling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-11 21:38:51 +00:00
Arthur Lu 0b701cc0d0 add proper account resource usage functionality
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-11 18:01:06 +00:00
Arthur Lu 5f12d9465e improve login form responsiveness
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 22:31:39 +00:00
Arthur Lu 3ec4edab24 better responsive handling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 22:08:11 +00:00
Arthur Lu 15f14215d1 bad fix for responsive instance container
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 21:55:31 +00:00
Arthur Lu df4ab8a94b fix code style
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 21:26:32 +00:00
Arthur Lu 41bcedc4df add icons to instance and node status
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 21:21:11 +00:00
Arthur Lu d8a5bba412 fix image height
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 20:46:40 +00:00
Arthur Lu 18d16ebec1 minor style improvement
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 20:03:15 +00:00
Arthur Lu 08dcc8bcdf improve some styling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 19:47:40 +00:00
Arthur Lu efbfa6441b improve font size scaling 2023-04-06 04:43:36 +00:00
Arthur Lu db38f0041e center config form 2023-04-06 04:31:12 +00:00
Arthur Lu 690d1d8cbc fix form input-grid rule 2023-04-06 04:27:34 +00:00
Arthur Lu 5df3cf23f5 add more info to insatnce container 2023-04-06 04:21:03 +00:00
Arthur Lu 444f8e93d2 simplify styling 2023-04-06 03:46:39 +00:00
Arthur Lu 64af15898d working but hacky 2023-04-06 02:56:37 +00:00
Arthur Lu e244255cc1 reset padding and margins to be fixed later
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-06 02:05:57 +00:00
Arthur Lu 3f3918d0ac improve nav bar
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-05 23:39:28 +00:00
Arthur Lu 58862f32a5 fix stlye sheet scoping,
imrpove some styling

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-05 23:35:11 +00:00
Arthur Lu c6659cffd9 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
Arthur Lu 055e7ddc14 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
Arthur Lu eecf272e99 use css vars for coloring
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-03 22:59:36 +00:00
Arthur Lu 3c2fa28f0a make instance article span entire width
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-03 22:49:24 +00:00
Arthur Lu 21cae2738c fix site grid layout
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-03 22:09:04 +00:00
Arthur Lu 81e4832fa2 add basic resource viewing
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-03 21:57:03 +00:00
Arthur Lu c16046f3fa fix remaining icons
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-03 21:17:18 +00:00
Arthur Lu 4b45b598bd fix add instance button color
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-03 21:10:00 +00:00
Arthur Lu 965a0a412e minor style changes,
fix icon colors

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-04-03 13:40:37 -07:00
Arthur Lu 3a3c5f46bd add nav bar to config page
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-25 23:41:56 +00:00
Arthur Lu d1cd928d11 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
Arthur Lu fc0e878353 set default login realm in login page
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-24 22:25:14 +00:00
Arthur Lu 4f1c6a1e10 add realm selector to login
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-24 21:13:33 +00:00
Arthur Lu baef83968c Update README.md 2023-02-28 21:09:12 -08:00
Arthur Lu f3b0056f3b Update README.md 2023-02-28 21:03:52 -08:00
Arthur Lu ae3af947df Update README.md 2023-02-28 20:48:44 -08:00
Arthur Lu d7e835b3c5 fix request functionr return values
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-03-01 00:01:56 +00:00
Arthur Lu 7f3bbbbff2 gracefully recover from config error
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-28 23:56:39 +00:00
Arthur Lu 7f6d5d319e 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
Arthur Lu 35ca6ad9ca 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
Arthur Lu bf00f9adf6 use new endpoint arg format 2023-02-27 02:29:55 +00:00
Arthur Lu 74509bf78e 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
Arthur Lu 5046bfb1c5 fix removed import
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-24 22:38:31 +00:00
Arthur Lu b1a8dba7c5 use client api endpont for proxmox requests
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-24 22:25:36 +00:00
Arthur Lu 96b1916fd0 fix login form centering
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-23 22:19:32 +00:00
Arthur Lu 86edfd7fa9 fix login form size
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-23 22:18:11 +00:00
Arthur Lu 300f55c38f add node field for add new instance
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-23 22:14:06 +00:00
Arthur Lu 0639d53a5d add extra input fields for container specific options
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-23 21:58:42 +00:00
Arthur Lu aeee33d705 add create instance form dialog
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-23 21:11:43 +00:00
Arthur Lu f72bb605e5 add delete instance logic
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-21 22:50:01 +00:00
Arthur Lu d392b2e445 add create instance button
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-21 21:52:57 +00:00
Arthur Lu 108a2bdda0 group action icons by type
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-21 21:50:54 +00:00
Arthur Lu 26e46a9c6b 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
Arthur Lu 7539ff731b 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
Arthur Lu 87e50051c7 use proxmox builtin consoles 2023-02-17 05:12:28 +00:00
Arthur Lu d8d36bee96 add handle cd add 2023-02-16 05:19:22 +00:00
Arthur Lu c665d89fc9 comment out console handler
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-16 01:25:17 +00:00
Arthur Lu 03df04ac1c add cd-add button to vm config
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-16 00:57:11 +00:00
Arthur Lu 8dfbdbac78 remove xternjs for now
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-16 00:36:13 +00:00
Arthur Lu f8ea7a0a03 add disk add for both vm and container
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-15 00:17:35 +00:00
Arthur Lu e796b1a6f0 remove unecessary line
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 22:57:42 +00:00
Arthur Lu d1546f5511 remove appendFormBody
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 22:55:09 +00:00
Arthur Lu 13a3a8f425 add create disk dialog form creation
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 21:56:27 +00:00
Arthur Lu 2218fd558a differentiate cancel and confirm buttons in dialog
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 21:13:38 +00:00
Arthur Lu a410933ff2 capitalize all button text
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 21:11:10 +00:00
Arthur Lu d12ac0cf9a separate dialog and instance custom elements
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 21:08:48 +00:00
Arthur Lu f666850ccb remove unecessary hr id
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 21:04:11 +00:00
Arthur Lu a2b37f5fe9 improve disk dialog creation
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 21:03:53 +00:00
Arthur Lu 134431d88c remove handler for inactive buttons,
change delete icon

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-14 20:05:56 +00:00
Arthur Lu 7204968187 change disk icon to loading after action triggers 2023-02-14 09:23:37 +00:00
Arthur Lu cd857d7835 open console in new window 2023-02-14 05:36:21 +00:00
Arthur Lu fca1adea24 fix user name by adding username cookie 2023-02-14 05:21:45 +00:00
Arthur Lu be50bf3de9 add basic xternjs functionality 2023-02-14 05:14:33 +00:00
Arthur Lu 1e076ca3e0 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
Arthur Lu a7affa28a8 add blank.svg,
fix usage of conflicting id values to use dataset
2023-02-13 22:51:22 +00:00
Arthur Lu 7e36a8c835 remove commented code 2023-02-13 04:15:46 +00:00
Arthur Lu e7973db190 improve power management status update
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-10 22:48:52 +00:00
Arthur Lu 12ec4f1ee4 fix lxc handling in disk attach
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-10 22:17:23 +00:00
Arthur Lu 6fec7b61a7 implement disk delete (all disk actions finished)
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-10 21:36:47 +00:00
Arthur Lu bbebce3c99 add disk attach
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-10 21:15:00 +00:00
Arthur Lu a7bc917e16 fix README code formatting 2023-02-06 16:28:58 -08:00
Arthur Lu 382da648fd fix form input labels in login 2023-02-06 21:59:00 +00:00
Arthur Lu fcf1ea5b96 implement disk move delete source
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-02 19:35:14 +00:00
Arthur Lu 93b1450d8c populate handler for disk move
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-02 18:26:44 +00:00
Arthur Lu bceb816478 add diskMove handler creation, improve form css
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-02 00:12:36 +00:00
Arthur Lu ed36cb62d2 improve dialog box custom element
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-01 23:42:30 +00:00
Arthur Lu 4e6f8e8a63 add example entries in vars.js.template
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-01 22:26:56 +00:00
Arthur Lu 1ae3ffe886 add button titles to instance
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-02-01 00:02:10 +00:00
Arthur Lu 7f9130b33f 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
Arthur Lu 77baa2f30c fix disk detach and disk resize issues
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-30 23:35:26 +00:00
Arthur Lu 37839d4117 add dialog confirm,
add resize handler

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-30 22:21:05 +00:00
Arthur Lu dbbb3ab09c add icon to all pages 2023-01-28 03:18:20 +00:00
Arthur Lu 98bae790f3 add favicon
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-28 00:30:46 +00:00
Arthur Lu cc72688bf8 create custom dialog form element
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-27 22:33:05 +00:00
Arthur Lu d6a37ef59c change vars export naming
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-27 21:49:50 +00:00
Arthur Lu c6ff9c8cfa 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
Arthur Lu 0ba917706e avoid refreshing page on disk change
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-26 19:55:17 +00:00
Arthur Lu 603b20ee9c separate disk and resource population
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-26 19:48:41 +00:00
Arthur Lu 584696772a 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
Arthur Lu 120e719a28 Update README.md 2023-01-26 01:10:23 -08:00
Arthur Lu 6c3905c847 add detach disk handler 2023-01-25 02:06:31 +00:00
Arthur Lu 10c197f3df Update README.md 2023-01-23 17:45:03 -08:00
Arthur Lu c5d36c26ee update README with new instructions 2023-01-20 05:02:50 +00:00
Arthur Lu 32cdfb3334 add template file for vars.js 2023-01-20 04:58:33 +00:00
Arthur Lu d8ff6c1235 remove vars.js 2023-01-20 04:56:31 +00:00
Arthur Lu b53c16b21b update README.md with new instructions
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 20:40:22 +00:00
Arthur Lu 7ecf78f24d untrack vars.js
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 20:33:34 +00:00
Arthur Lu 860fd2c828 move api path info to vars.js
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 20:33:05 +00:00
Arthur Lu c09300e6a0 add config for tronnet, will remove later
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 19:59:12 +00:00
Arthur Lu c9f9ad590c use extra js file to store config data
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 19:58:44 +00:00
Arthur Lu 1b4e8478dc change attach and detach icons
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 18:08:00 +00:00
Arthur Lu 2298b907d9 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
Arthur Lu 1bb0aa5d3e add action button icons
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 02:13:07 +00:00
Arthur Lu 2557292a28 add unused disk to disk populate
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 00:54:14 +00:00
Arthur Lu be589fd586 revert to show disk text config
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-19 00:35:52 +00:00
Arthur Lu 5f3f6a9f53 improve acessibility
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-18 23:43:57 +00:00
Arthur Lu d52e730dd1 remove padding from first form fieldset
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-18 22:58:00 +00:00
Arthur Lu 2fe9e847c9 hide instance actions if the node is offline
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:45:16 -08:00
Arthur Lu 23f3153294 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
Arthur Lu 1b4e818b6d fix unknown status check order
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:36:34 -08:00
Arthur Lu d957fb35ef fix instance icon assignment
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:34:22 -08:00
Arthur Lu 1c0faf2a71 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
Arthur Lu f2da3882dc ensure node search returns a node
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:29:10 -08:00
Arthur Lu 3457e46a69 fix variable item typo
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:27:15 -08:00
Arthur Lu 7127439970 fix finding nodes in resources
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:26:49 -08:00
Arthur Lu 94bf131c43 fix instance population
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:26:19 -08:00
Arthur Lu 1d4981c93f use resource path rather than querying each node
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:24:51 -08:00
Arthur Lu 9dbbb5909a remove unrelated comments
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 12:02:02 -08:00
Arthur Lu 004c5279fb remove unused function getDiskImageOptions
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:56:20 -08:00
Arthur Lu 720413760a keep disk filename prefixes to assign value correctly
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:50:59 -08:00
Arthur Lu 8cd7142d97 remove iso/ prefix from disk filename
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:48:50 -08:00
Arthur Lu 9d3ab59768 fix varaible naming for storage in handleStorageChange
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:45:29 -08:00
Arthur Lu 445fc10e5f fix varaible instantiation
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:44:34 -08:00
Arthur Lu c5224b71dc fix disk image select id assignment
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:43:50 -08:00
Arthur Lu bd92558d66 add storage change handler
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:39:28 -08:00
Arthur Lu 9a69f541c7 fix config disk grid layout
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:26:42 -08:00
Arthur Lu 6c18a44225 use grid-column to span some inputs
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:15:04 -08:00
Arthur Lu fb1ed3ce4b remove storage and iso from disk image options
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 11:04:28 -08:00
Arthur Lu 0bfe716ecb populate diskImageSelect with options
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 10:59:06 -08:00
Arthur Lu a4b7d7fd3e add options to disk image selection
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 10:48:09 -08:00
Arthur Lu 4b9ed93669 add ids to storage and size input
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 10:31:26 -08:00
Arthur Lu bc89676f8a set non resiable disk size value to 0
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 10:26:11 -08:00
Arthur Lu 2d55625675 fix handleDeviceChange not implemented
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 10:19:07 -08:00
Arthur Lu 03f6493b63 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
Arthur Lu 3688b44c56 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
Arthur Lu 89c7942901 add hidden class
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-17 09:56:48 -08:00
Arthur Lu e5bf7e383a 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
Arthur Lu 1e035e878c 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
Arthur Lu c8141943f5 fix min checking
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 16:10:56 -08:00
Arthur Lu 0d2a2763c9 fix invalid query selector ids
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 16:08:20 -08:00
Arthur Lu 810d33aba9 fix query selector in handleUnitChange
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 16:07:06 -08:00
Arthur Lu 767c239c82 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
Arthur Lu bbfdfa0f98 disable size unit select if resizable is false
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:50:32 -08:00
Arthur Lu 3e03bc5ede fix disk config layout
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:48:54 -08:00
Arthur Lu e11f281a59 add required params to addDiskLine
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:48:20 -08:00
Arthur Lu 299f60a878 fix variable naming conflict
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:47:13 -08:00
Arthur Lu eaf51064ac add selector for size unit
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:45:51 -08:00
Arthur Lu abe70d0b72 remove unit from size
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:27:41 -08:00
Arthur Lu 2ae003b98c fix config disk layout
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:25:03 -08:00
Arthur Lu a2b4dcd985 fix delete button alt text
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:24:24 -08:00
Arthur Lu 14ee712d3c add delete disk button
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:22:39 -08:00
Arthur Lu 36d93017f9 fix storageSelect and sizeInput populating
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:16:02 -08:00
Arthur Lu 0aa983594f fix storage options populating
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:14:34 -08:00
Arthur Lu ea06755a1e fix icon alt again
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:13:17 -08:00
Arthur Lu 9c37a9e562 fix bus prefix issue
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:12:46 -08:00
Arthur Lu f378a1c7f9 fix icon alt text
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:10:24 -08:00
Arthur Lu 4d0a44995b fix icon reference
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:09:21 -08:00
Arthur Lu 92739d9660 make each disk configurable
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 15:05:58 -08:00
Arthur Lu 40c844a6f4 remove name field
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-16 14:25:31 -08:00
Arthur Lu 39cce60fc0 right align all disk config buttons 2023-01-12 15:26:45 -08:00
Arthur Lu 68146260ed add grey backgroudn to disabled inputs 2023-01-12 15:21:08 -08:00
Arthur Lu d0482e867f disable bus when lxc,
disable path when qemu
2023-01-12 15:17:35 -08:00
Arthur Lu a1727eebdc remove console.log s 2023-01-12 15:08:45 -08:00
Arthur Lu 79a46b1f9b removed redundant params in populateResources 2023-01-12 15:08:02 -08:00
Arthur Lu 6984f20998 add storage target option adding 2023-01-12 15:05:22 -08:00
Arthur Lu 8345dd61f2 add fetch storage options 2023-01-12 14:43:17 -08:00
Arthur Lu 0da9812b89 fix event listener callback function 2023-01-11 03:02:17 -08:00
Arthur Lu ec1426f0c1 fix disk populating bugs 2023-01-11 03:00:01 -08:00
Arthur Lu cdb06ce8cc add query select for addDiskDevice 2023-01-11 02:58:22 -08:00
Arthur Lu 79fd1f0625 try global vars 2023-01-11 02:57:37 -08:00
Arthur Lu d5d495f566 fix parameters 2023-01-11 02:52:52 -08:00
Arthur Lu 8b2fb8c1ba consolidate handler params 2023-01-11 02:51:06 -08:00
Arthur Lu 32ca9c2309 add missing type param in add disk handlers 2023-01-11 02:47:36 -08:00
Arthur Lu acc6d834c4 fix missing param in add disk handlers 2023-01-11 02:46:25 -08:00
Arthur Lu 906c292e53 streamline add disk code 2023-01-11 02:43:56 -08:00
Arthur Lu d8c44b3fb1 remoe focus outline 2023-01-11 02:27:10 -08:00
Arthur Lu 2da3d77860 add correct event listener 2023-01-11 02:16:30 -08:00
Arthur Lu fadf3f206c add red border on add disk device input focus 2023-01-11 02:15:58 -08:00
Arthur Lu e9bfea7233 populate add disk device with next avaliable value,
add red border around invalid device number
2023-01-11 02:01:34 -08:00
Arthur Lu 9b455a2616 flip qemu disk ordering 2023-01-11 01:33:50 -08:00
Arthur Lu 0ee6a217e3 add pointer to disk items 2023-01-11 01:32:51 -08:00
Arthur Lu f45647210d generalize button img hover 2023-01-11 01:30:28 -08:00
Arthur Lu 5dbdde2d9f fix form img hover 2023-01-11 01:28:09 -08:00
Arthur Lu 1aca7439a5 add hover cursor change 2023-01-11 01:24:28 -08:00
Arthur Lu d9d9a0a0e7 fixes to limit and bus selection 2023-01-10 17:16:05 -08:00
Arthur Lu d6d028ff89 use capital case for disk name 2023-01-10 17:12:24 -08:00
Arthur Lu b3f76107ec fix prefix in disk population 2023-01-10 17:11:00 -08:00
Arthur Lu 5e11295390 remove all references to disks 2023-01-10 17:09:59 -08:00
Arthur Lu 724048e7d0 fix populating bug 2023-01-10 17:09:10 -08:00
Arthur Lu 583286041e fix variable typo 2023-01-10 17:06:34 -08:00
Arthur Lu d54605edd5 fix variable name collision 2023-01-10 17:05:41 -08:00
Arthur Lu e91bcb6ad1 used variable to store disk configuration 2023-01-10 17:04:20 -08:00
Arthur Lu 6f848f9fda add logic for bus select and device limit 2023-01-10 16:49:57 -08:00
Arthur Lu 6a5b953da2 align add disk button to the right 2023-01-10 16:15:44 -08:00
Arthur Lu 0c3cdb2ca6 add disk mini form 2023-01-10 16:01:13 -08:00
Arthur Lu 8417175605 add some top margin to create disk button 2023-01-10 00:21:48 -08:00
Arthur Lu 65308c1eed add disk create button 2023-01-10 00:18:23 -08:00
Arthur Lu fff454ea0e fix disk populating issue 2023-01-09 23:51:11 -08:00
Arthur Lu ff31563f88 sort disks in numerical order 2023-01-09 23:50:11 -08:00
Arthur Lu 473c13aa08 decrease stop.svg thickness 2023-01-09 23:02:57 -08:00
Arthur Lu 6305f77718 increase stop.svg slightly 2023-01-09 23:00:11 -08:00
Arthur Lu 22d7c1a1c0 increase stop.svg thickness 2023-01-09 22:59:09 -08:00
Arthur Lu e714206325 increase start.svg size 2023-01-09 22:54:41 -08:00
Arthur Lu ea36b47b24 use better icon for drive 2023-01-09 22:43:54 -08:00
Arthur Lu 3f989cf579 use a cleaner drive icon 2023-01-09 22:31:45 -08:00
Arthur Lu 5072aef9dc add logic for disk and drive icon 2023-01-09 22:24:04 -08:00
Arthur Lu c646d0e3ce add drive.svg,
add width and height to some svg
2023-01-09 22:19:20 -08:00
Arthur Lu ad203878ac Fix bracket escape in README.md 2023-01-09 18:01:51 -08:00
Arthur Lu b26721ecf7 Create README.md 2023-01-09 17:50:05 -08:00
Arthur Lu c88f44bfdb generalize disk populating
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 16:21:45 -08:00
Arthur Lu 8d2171bc78 change center div to fit-content
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 16:08:02 -08:00
Arthur Lu d097266c3d add configurable disk population
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 16:02:25 -08:00
Arthur Lu 0755f99368 add some margin to nav bar
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 15:29:36 -08:00
Arthur Lu eed1678b90 add auth successful message to login
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 15:20:34 -08:00
Arthur Lu dace5237c8 fix cookie deletion
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 15:16:24 -08:00
Arthur Lu 78edc9e0a3 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
Arthur Lu 0a2ba085ce add border to active button
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 15:01:21 -08:00
Arthur Lu 20a63a03cb fix some nav styling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 14:54:47 -08:00
Arthur Lu 5865ecbb1b use btn-group class for nav bar
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 14:53:29 -08:00
Arthur Lu fb1a8ec32a fix style bug
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 14:50:52 -08:00
Arthur Lu 32ab93e3d4 fix nav bar positioning
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 14:50:07 -08:00
Arthur Lu 388912f8a5 simplify nav heiarchy
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 14:43:48 -08:00
Arthur Lu dd2ceffd5d readd script to index
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 14:37:32 -08:00
Arthur Lu eede39230c fix nav styling
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 14:36:50 -08:00
Arthur Lu 64b1d35c17 add top nav bar
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
2023-01-09 14:29:27 -08:00
Arthur Lu 45d6a6993b fix some styling 2023-01-08 21:46:14 -08:00
Arthur Lu a114fe2477 fix memory populating bug 2023-01-08 21:32:22 -08:00
Arthur Lu b4361b0ccf add label for in name fieldset 2023-01-08 21:28:35 -08:00
Arthur Lu 05679be08c fix label for assignment,
fix disk icon alt
2023-01-08 21:26:44 -08:00
Arthur Lu f4c830f1ca add image alts in config page,
add for to labels in config page
2023-01-08 21:23:08 -08:00
Arthur Lu 70611e0864 add config button to disk entry 2023-01-08 20:49:57 -08:00
Arthur Lu 6a92706e80 add config button to disk items 2023-01-08 20:48:58 -08:00
Arthur Lu 18d59cc0b4 add row gap in input grid 2023-01-08 20:38:07 -08:00
Arthur Lu f3e6856cd3 populate name field using addMetaLine 2023-01-08 20:32:15 -08:00
Arthur Lu 9f866647e7 populate id parameter in addDiskLine 2023-01-08 20:30:09 -08:00
Arthur Lu f0d4ca3f7a fix another variable name conflict 2023-01-08 20:28:50 -08:00
Arthur Lu bbc4286297 fix variable name conflict 2023-01-08 20:28:19 -08:00
Arthur Lu 0e066c75fc populate disks in separate fieldset 2023-01-08 20:26:20 -08:00
Arthur Lu 08735128de improve config styling 2022-12-21 16:27:04 -08:00
Arthur Lu 72da25c713 fix form icon style 2022-12-21 16:01:29 -08:00
Arthur Lu 542c22b364 fix icon null check 2022-12-21 16:00:28 -08:00
Arthur Lu 0fe83fa2b8 fix config form icon styling 2022-12-21 15:59:54 -08:00
Arthur Lu ffd48cafad add icons to config form 2022-12-21 15:56:52 -08:00
Arthur Lu 54fa98e754 add swap in config 2022-12-21 15:39:09 -08:00
Arthur Lu a98057ae39 remove resource bar from instance article 2022-12-21 15:31:31 -08:00
Arthur Lu a9c816f9c0 set step to 0.001 2022-12-21 15:26:18 -08:00
Arthur Lu 512c615351 use min of 1 2022-12-21 15:22:50 -08:00
Arthur Lu 5f40fea9ff set min size for storage to 1 GB 2022-12-21 15:15:45 -08:00
Arthur Lu ec6b3c4510 switch instance name logic 2022-12-21 15:10:13 -08:00
Arthur Lu 2375263a9a add better label text for rootfs 2022-12-21 15:08:21 -08:00
Arthur Lu 3748fcdd99 fix name populating issue 2022-12-21 15:07:34 -08:00
Arthur Lu 05b5853c8f add lxc config form populating 2022-12-21 15:06:26 -08:00
Arthur Lu fb12817140 fix login form layout 2022-12-21 11:33:51 -08:00
Arthur Lu 7ac537d00b fix string to int parsing 2022-12-21 01:36:24 -08:00
Arthur Lu d3d2670198 fix string to float conversion 2022-12-21 01:33:37 -08:00
Arthur Lu 0c4917c77a improve form populating 2022-12-21 01:31:58 -08:00
Arthur Lu 833307158c improve form layout css 2022-12-21 00:56:40 -08:00
Arthur Lu cdcae0b05e add units to config form 2022-12-21 00:49:57 -08:00
Arthur Lu da4dab35cc remove colons from forms 2022-12-20 18:03:07 -08:00
Arthur Lu 3a174c6989 add colon to sata config 2022-12-20 18:01:44 -08:00
Arthur Lu 2dc311af6a remove login status testing 2022-12-20 17:52:41 -08:00
Arthur Lu 045f8c9825 set legend height to 0 2022-12-20 17:51:53 -08:00
Arthur Lu 1796d97ce8 final form styling 2022-12-20 17:46:03 -08:00
Arthur Lu f830655d0f remove border around form buttons 2022-12-20 17:38:14 -08:00
Arthur Lu 1f3ee1c456 populate vm name 2022-12-20 17:07:58 -08:00
Arthur Lu aa4085f312 improve form layout and styling 2022-12-20 17:02:33 -08:00
Arthur Lu ab5410218d parse disk to only use size 2022-12-20 16:47:49 -08:00
Arthur Lu 34d562349b change config form button type to button 2022-12-20 15:49:13 -08:00
Arthur Lu d2bee8d432 try window.assign 2022-12-20 15:45:43 -08:00
Arthur Lu 36dadf5e9a add cancel button listener 2022-12-20 15:42:13 -08:00
Arthur Lu 99ef947b1c fix missing increment 2022-12-20 15:38:53 -08:00
Arthur Lu 2104a0e15e add populating sata drives 2022-12-20 15:37:04 -08:00
Arthur Lu f5403c7af1 add credential check to config.js 2022-12-20 15:15:08 -08:00
Arthur Lu 191936fde7 fix username input 2022-12-19 18:47:05 -08:00
Arthur Lu 6ac057bdbf use grid layout for forms 2022-12-19 18:46:28 -08:00
Arthur Lu 9825b6b951 undo input width 2022-12-19 18:36:41 -08:00
Arthur Lu 2d37f3e589 make config inputs 100% width 2022-12-19 18:35:09 -08:00
Arthur Lu 86eadf1791 use innerHTML directly 2022-12-19 18:21:48 -08:00
Arthur Lu 3368305b9b remove unecessary line 2022-12-19 18:17:19 -08:00
Arthur Lu 4b5c00f541 use label property 2022-12-19 18:16:07 -08:00
Arthur Lu 51965010e8 use innerHTML 2022-12-19 18:14:00 -08:00
Arthur Lu 35dfbc9b70 use setAttribute 2022-12-19 18:12:39 -08:00
Arthur Lu 4294fd7ca0 fix config populating bug 2022-12-19 18:08:16 -08:00
Arthur Lu cf0f06fb22 populate cores and memory in config 2022-12-19 17:59:34 -08:00
Arthur Lu 1a4d32f876 remove btn-group:after 2022-12-19 17:35:55 -08:00
Arthur Lu a584fa61b9 remove extra white space 2022-12-19 17:23:04 -08:00
Arthur Lu 39c60a654c fix word break issue 2022-12-19 17:16:04 -08:00
Arthur Lu ad6e330a74 improve form layout handling 2022-12-19 17:05:04 -08:00
Arthur Lu e0e3d5802e add await to config form request 2022-12-19 16:21:24 -08:00
Arthur Lu 1cb66da505 fix config form button ids 2022-12-19 16:17:57 -08:00
Arthur Lu 913175239c 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
Arthur Lu 529959cba5 remove unecessary cookie header 2022-12-19 15:22:20 -08:00
Arthur Lu 474e5b2e09 remove extra end tag in config.html 2022-12-18 22:49:19 -08:00
Arthur Lu ecb7e6f192 add config.html,
add config.js,
add getURIData function to parse URI search params
2022-12-18 22:14:57 -08:00
Arthur Lu 4fd15f804b readd proper imports to login.js 2022-12-18 22:00:59 -08:00
Arthur Lu f497973cc8 fix login.js imports 2022-12-18 22:00:02 -08:00
Arthur Lu 18da1b996d fix goToPage 2022-12-18 21:58:46 -08:00
Arthur Lu fe27bba986 modularize redirect function 2022-12-18 21:52:39 -08:00
Arthur Lu f5e360046d fix config button updating 2022-12-18 21:05:05 -08:00
Arthur Lu 652c409055 change config button icon on power action 2022-12-18 21:00:58 -08:00
Arthur Lu bbf94c983d handle instance action update by process id 2022-12-18 20:32:05 -08:00
Arthur Lu 8d4c5e122a minify svgs 2022-12-18 19:58:08 -08:00
Arthur Lu a31e370d4d set proper alt for instance type/status and power action 2022-12-18 17:11:35 -08:00
Arthur Lu 6c64c06687 add loading icon 2022-12-18 17:06:05 -08:00
Arthur Lu 3fd9f3bb3f remove console 2022-12-18 16:56:02 -08:00
Arthur Lu 015855d38f add actionLock to element power action 2022-12-18 16:53:07 -08:00
Arthur Lu 5971c1ebef remove periodic refresh in favor of refresh as needed 2022-12-17 17:52:20 -08:00
Arthur Lu f4374faccd assign alt properties 2022-12-17 17:36:45 -08:00
Arthur Lu c39fca591c remove debug console.log 2022-12-17 16:44:43 -08:00
Arthur Lu c5137481b7 update power action button in element updater 2022-12-17 16:37:26 -08:00
Arthur Lu aacb2cb86d fix waitFor issue 2022-12-17 16:35:51 -08:00
Arthur Lu 14e35fe670 handle update element in listener 2022-12-17 16:34:40 -08:00
Arthur Lu 8397f313c9 disable reload for now 2022-12-17 16:15:28 -08:00
Arthur Lu d37b8a5bc0 run instance population every second 2022-12-17 16:07:18 -08:00
Arthur Lu e66cf54a2e remove csrf token from cookie 2022-12-15 23:51:51 -08:00
Arthur Lu 9dc6b6d588 put CSRF only for POST 2022-12-15 23:44:48 -08:00
Arthur Lu c62f583dd4 try setting CSRFPreventionToken header 2022-12-15 23:40:04 -08:00
Arthur Lu b63a9b254f add csrf token saving 2022-12-15 23:15:52 -08:00
Arthur Lu 2c7121d84a fix power management request 2022-12-15 23:08:56 -08:00
Arthur Lu 07b17cf12c fix import 2022-12-15 23:05:32 -08:00
Arthur Lu 5dcd6a805d implement basic power management 2022-12-15 23:03:50 -08:00
Arthur Lu b6e61c310c darken inactive config icon 2022-12-15 18:32:06 -08:00
Arthur Lu 1e216a7817 limit height of action buttons 2022-12-15 18:31:05 -08:00
Arthur Lu b2c8458e5d add action icons,
set action icons depending on instance status
2022-12-15 18:28:12 -08:00
Arthur Lu b743af759f move btn-group margins to instance.css 2022-12-15 18:13:17 -08:00
Arthur Lu aaa59c530f add margin to btn-group 2022-12-15 18:09:48 -08:00
Arthur Lu b4a15c4118 add power button html and css 2022-12-15 18:04:20 -08:00
Arthur Lu 9f4642ec81 simplify instance styling 2022-12-15 17:45:40 -08:00
Arthur Lu eb46ac7a5b add css for hr 2022-12-14 22:57:55 -08:00
Arthur Lu ac89be7600 add hr between summary and resources 2022-12-14 22:54:18 -08:00
Arthur Lu 53720d4d8a make instance cards more contained 2022-12-14 22:51:44 -08:00
Arthur Lu 70fa73c2f2 center instance node info 2022-12-14 17:42:20 -08:00
Arthur Lu 3bbcf1a19b remove column gap 2022-12-14 17:36:28 -08:00
Arthur Lu 8778aa83b0 evenly space resource elements 2022-12-14 17:34:13 -08:00
Arthur Lu cff0986b29 set qemu instance maxswap to 0 2022-12-14 17:31:05 -08:00
Arthur Lu f53048edc4 fix resource disk text assignment 2022-12-14 17:28:48 -08:00
Arthur Lu 1429175d97 recolor resource icons to white 2022-12-14 17:27:53 -08:00
Arthur Lu d64d6f1875 upload resource icons,
add some column gap to resource-div
2022-12-14 17:23:05 -08:00
Arthur Lu f12454a56c add resource details to instance article 2022-12-14 17:20:31 -08:00
Arthur Lu 08dca0cb9e add implicit instance typing 2022-12-14 17:01:38 -08:00
Arthur Lu 866d3c6aca fix instance article element selection 2022-12-14 16:10:28 -08:00
Arthur Lu 1b57b587c7 fix instance article content 2022-12-14 16:07:25 -08:00
Arthur Lu 77673e80fe use templates to streamline instance creation 2022-12-14 16:05:15 -08:00
Arthur Lu 36b86e4edb improve instance article layout 2022-12-14 15:47:29 -08:00
Arthur Lu 90eba4904c arrange instance elements inline 2022-12-14 15:38:13 -08:00
Arthur Lu 223681ccc6 add top div to instance article 2022-12-14 15:32:22 -08:00
Arthur Lu 12ef3de58c fix node icon ref 2022-12-14 15:29:08 -08:00
Arthur Lu 7b276c2649 add icons 2022-12-14 15:28:22 -08:00
Arthur Lu 6400f36324 separate instance card data elements 2022-12-14 14:42:40 -08:00
Arthur Lu 8ef1cc3344 add instance styling 2022-12-14 14:30:46 -08:00
Arthur Lu 5c0176dc98 simplify instacne node assignment 2022-12-14 14:11:20 -08:00
Arthur Lu 0b4f50c85c fix instance node data assignment 2022-12-14 13:48:29 -08:00
Arthur Lu f81fea1268 add node data to instances 2022-12-14 13:47:28 -08:00
Arthur Lu f12be4b2a9 fix concat logic 2022-12-14 13:40:19 -08:00
Arthur Lu a1fd99e102 simplify instance display 2022-12-14 13:37:30 -08:00
Arthur Lu 4730ada40a fix instanceDiv 2022-12-13 23:28:07 -08:00
Arthur Lu 979307095d fix instance type setting,
add min-content to center-div
2022-12-13 23:26:43 -08:00
Arthur Lu f0e5b31bdf fix node and instance element stylesheet reference 2022-12-12 21:43:19 -08:00
Arthur Lu 184cc5ee60 add useful login statuses 2022-12-12 17:12:11 -08:00
Arthur Lu f681ad77e7 make body optional parameter for request function 2022-12-12 16:30:55 -08:00
Arthur Lu 4a97631ec9 move js to scripts folder,
move css to css folder
2022-12-12 16:27:45 -08:00
Arthur Lu d09b7b59ba fix requestTicket return value 2022-12-12 16:21:33 -08:00
Arthur Lu c0c0805aa3 fix optional param 2022-12-12 16:19:47 -08:00
Arthur Lu 12a733f556 remove repeated code 2022-12-12 16:18:40 -08:00
Arthur Lu 81e9c28a7a fix error naming again 2022-12-12 16:11:00 -08:00
Arthur Lu 9cef571576 fix error naming 2022-12-12 16:09:57 -08:00
Arthur Lu 063e7f00b6 create custom ResponseError and NetworkError 2022-12-12 16:09:06 -08:00
Arthur Lu f6fce759d8 apply promise catch to request function 2022-12-12 16:00:11 -08:00
Arthur Lu 54d27d5ccd try promises 2022-12-12 15:56:57 -08:00
Arthur Lu 90ef6bca81 fix ticket saving 2022-12-12 15:13:43 -08:00
Arthur Lu 87806a9aaa try preventDefault 2022-12-12 15:11:52 -08:00
Arthur Lu 3abd62773d try event.preventDefault fix 2022-12-12 15:10:52 -08:00
Arthur Lu ce4bf30e4b fix login form key issue 2022-12-12 15:00:32 -08:00
Arthur Lu 12089dcf9c add login page 2022-12-12 14:57:43 -08:00
Arthur Lu f1f820aeb2 update style.css with responsive center-div 2022-12-11 21:31:28 -08:00
Arthur Lu dc778fe317 update style.css 2022-12-11 20:44:21 -08:00
Arthur Lu 0111f5d7c7 fix instance populating 2022-12-11 20:22:37 -08:00
Arthur Lu 067bc41d69 fix script linking 2022-12-11 20:21:18 -08:00
Arthur Lu 21771fdb75 make instance custom element 2022-12-11 20:20:32 -08:00
Arthur Lu a7c1baa35c fix instance status issue 2022-12-11 20:02:39 -08:00
Arthur Lu 11ac75b0c5 set instance color 2022-12-11 20:01:32 -08:00
Arthur Lu 3be0c02595 sort nodes and instances 2022-12-11 15:24:19 -08:00
Arthur Lu 8349cfd31d try new instance format 2022-12-11 15:15:39 -08:00
Arthur Lu a86df14fd7 fix instance populating 2022-12-11 15:07:44 -08:00
Arthur Lu bb06579219 add qemu and lxc instance populating 2022-12-11 15:06:10 -08:00
Arthur Lu 1f4c0310ca fix instance printing 2022-12-11 14:56:41 -08:00
Arthur Lu 6f32ae83cc check instance GET 2022-12-11 14:54:50 -08:00
Arthur Lu e6c1c02358 fix idnentation in node.js 2022-12-10 23:05:31 -08:00
Arthur Lu 2e3bb6d58d implement node-card element,
add populating node-cards on init
2022-12-10 22:49:10 -08:00
Arthur Lu 8f48dc61ac set cookie domain 2022-12-10 21:55:34 -08:00
Arthur Lu 029f626913 update style.css 2022-12-10 19:25:59 -08:00
Arthur Lu bceae9d4d5 login prototype 2022-12-10 18:02:29 -08:00
171 changed files with 3270 additions and 5743 deletions
+42
View File
@@ -0,0 +1,42 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": "standard",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"no-tabs": [
"error",
{
"allowIndentationTabs": true
}
],
"indent": [
"error",
"tab"
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
],
"brace-style": [
"error",
"stroustrup",
{
"allowSingleLine": false
}
]
}
}
+3 -3
View File
@@ -1,3 +1,3 @@
**/config.json
dist/*
go.sum
vars.js
**/package-lock.json
**/node_modules
-6
View File
@@ -1,6 +0,0 @@
[submodule "proxmoxaas-common-lib"]
path = proxmoxaas-common-lib
url = https://git.tronnet.net/tronnet/proxmoxaas-common-lib
[submodule "WFA-JS"]
path = WFA-JS
url = https://git.tronnet.net/alu/WFA-JS
-26
View File
@@ -1,26 +0,0 @@
.PHONY: build build-web build-wfa clean clean-wfa clean-web
build: clean ensure-dist build-wfa build-web
@echo "======================== Building Binary ======================="
# resolve symbolic links in web by copying it into dist/web/
CGO_ENABLED=0 go build -tags release -ldflags="-s -w" -v -o dist/ .
clean: clean-wfa clean-web
@echo "======================== Cleaning Project ======================"
go clean
rm -rf dist/*
ensure-dist:
mkdir -p dist
build-web: ensure-dist
cp -rL web/ dist/web/
clean-web:
build-wfa:
$(MAKE) -C WFA-JS
cp -f WFA-JS/dist/* web/modules
clean-wfa:
$(MAKE) clean -C WFA-JS
+21 -59
View File
@@ -9,70 +9,32 @@ ProxmoxAAS Dashboard provides users of a proxmox based compute on demand service
- Remote console
- Extended account management
# Installation
## ProxmoxAAS System Installation Overview
The ProxmoxAAS project is large and is split into multiple components. There are four required components, the Dashboard, API, Fabric, and access-manager-api. The installation 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 |
| access-manager-api | access.local | N/A|
## Prerequisites - Dashboard
## Prerequisites
- Proxmox VE Cluster (v7.0+)
- Reverse proxy server which can proxy the dashboard, API, and Fabric
- Reverse proxy server which can proxy the dashboard and API
- FQDN
- Web server to host the ProxmoxAAS Dashboard (ie Apache2)
## 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.
## Installation - 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
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`
After this step, the Dashboard should be available on the `ProxmoxAAS-Dashboard` host at the configured `listenPort`
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`
## Installation - API
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 - access-manager-api
To install the access-manager-api component, go to [access-manager-api](https://git.tronnet.net/tronnet/access-manager-api). This is required for the app to function. The access-manager-api installation will also have steps for setting up the reverse proxy server.
## 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.
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.
Submodule WFA-JS deleted from b0aacd5f9c
+79
View File
@@ -0,0 +1,79 @@
<!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>
-80
View File
@@ -1,80 +0,0 @@
package app
import (
"flag"
"fmt"
"log"
"proxmoxaas-dashboard/app/common"
"proxmoxaas-dashboard/app/routes"
"web" // go will complain here until the first build
"github.com/gin-gonic/gin"
"github.com/tdewolff/minify/v2"
)
func Run() {
configPath := flag.String("config", "config.json", "path to config.json file")
flag.Parse()
common.Global = common.GetConfig(*configPath)
// setup gin
gin.SetMode(gin.ReleaseMode)
router := gin.Default()
// setup static resources
m := common.InitMinify()
ServeStatic(router, m)
html := common.MinifyStatic(m, web.Templates)
common.TMPL = common.LoadHTMLToGin(router, html)
// dynamic routes for pages and page fragments
router.GET("/", routes.HandleGETIndex)
router.GET("/index", routes.HandleGETIndex)
router.GET("/index/instances", routes.HandleGETInstancesFragment)
router.GET("/account", routes.HandleGETAccount)
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("/backups", routes.HandleGETBackups)
router.GET("/backups/backups", routes.HandleGETBackupsFragment)
router.GET("/login", routes.HandleGETLogin)
router.GET("/settings", routes.HandleGETSettings)
// run on all interfaces with port
log.Fatal("[ERR ] starting gin router: ", router.Run(fmt.Sprintf("0.0.0.0:%d", common.Global.Port)))
}
// setup static resources under web (css, images, modules, scripts)
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.Header("Content-Encoding", data.MimeType.Encoding)
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.Header("Content-Encoding", data.MimeType.Encoding)
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.Header("Content-Encoding", data.MimeType.Encoding)
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.Header("Content-Encoding", data.MimeType.Encoding)
c.Data(200, data.MimeType.Type, []byte(data.Data))
})
}
-57
View File
@@ -1,57 +0,0 @@
//go:build !release
package common
import (
"io"
"github.com/tdewolff/minify/v2"
)
// defines mime type and associated minifier
type MimeType struct {
Type string
Encoding string
Minifier func(m *minify.M, w io.Writer, r io.Reader, params map[string]string) error
}
// debug mime types
var MimeTypes = map[string]MimeType{
"css": {
Type: "text/css",
Minifier: nil,
},
"html": {
Type: "text/html",
Minifier: nil,
},
"tmpl": {
Type: "text/plain",
Minifier: nil,
},
"frag": {
Type: "text/plain",
Minifier: nil,
},
"svg": {
Type: "image/svg+xml",
Minifier: nil,
},
"js": {
Type: "application/javascript",
Minifier: nil,
},
"wasm": {
Type: "application/wasm",
Minifier: nil,
},
"gz": {
Type: "application/wasm",
Encoding: "gzip",
Minifier: nil,
},
"*": {
Type: "text/plain",
Minifier: nil,
},
}
-61
View File
@@ -1,61 +0,0 @@
//go:build release
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"
"github.com/tdewolff/minify/v2/svg"
)
// defines mime type and associated minifier
type MimeType struct {
Type string
Encoding 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: svg.Minify,
},
"js": {
Type: "application/javascript",
Minifier: js.Minify,
},
"wasm": {
Type: "application/wasm",
Minifier: nil,
},
"gz": {
Type: "application/wasm",
Encoding: "gzip",
Minifier: nil,
},
"*": {
Type: "text/plain",
Minifier: nil,
},
}
-37
View File
@@ -1,37 +0,0 @@
package common
import "html/template"
var Global Config
type Config struct {
Port int `json:"listenPort"`
Organization string `json:"organization"`
DASH string `json:"dashurl"`
PVE string `json:"pveurl"`
API string `json:"apiurl"`
}
// variable for html template root
// generated from LoadHTMLToGin
var TMPL *template.Template
// static served file type containing data and mimetype
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
}
-265
View File
@@ -1,265 +0,0 @@
package common
import (
"bufio"
"embed"
"encoding/json"
"errors"
"fmt"
"html/template"
"io"
"io/fs"
"log"
"net/http"
"os"
"reflect"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/tdewolff/minify/v2"
paas "proxmoxaas-common-lib"
)
// get config file from configPath
func GetConfig(configPath string) Config {
root, err := os.OpenRoot(".")
if err != nil {
log.Fatal("[ERR ] error opening root dir: ", err)
}
defer root.Close()
content, err := root.ReadFile(configPath)
if err != nil {
log.Fatal("[ERR ] error opening config file: ", err)
}
var config Config
err = json.Unmarshal(content, &config)
if err != nil {
log.Fatal("[ERR ] error parsing config file: ", err)
}
return config
}
// initialize minifier using the meta types specified
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)
err := 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("[ERR ] 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("[ERR ] 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
})
if err != nil {
log.Printf("[ERR ] error in MinifyStatic: %s", err)
return nil
} else {
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()
var s strings.Builder
for i := range keys {
if i != 0 {
s.WriteString(sep)
}
s.WriteString(keys[i].String())
}
return s.String()
},
"Map": func(values ...any) (map[string]any, error) {
if len(values)%2 != 0 {
return nil, errors.New("invalid dict call")
}
dict := make(map[string]any, 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(root)
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("[WARN] encountered a non-fatal error: %s", err.Error())
c.Status(http.StatusInternalServerError)
}
func RequestGetAPI(path string, auth *paas.Auth, body any) (*http.Response, int, error) {
req, err := http.NewRequest("GET", Global.API+path, nil)
if err != nil {
return nil, 0, err
}
if auth != nil {
for k, v := range GetRequestContextFromCookies(*auth) {
req.AddCookie(&http.Cookie{Name: k, Value: v, Secure: true})
}
}
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
}
switch body.(type) { // write json to body object depending on type, currently supports map[string]any (ie json) or []any (ie array of json)
case *map[string]any:
err = json.Unmarshal(data, &body)
if err != nil {
return nil, response.StatusCode, err
}
case *[]any:
err = json.Unmarshal(data, &body)
if err != nil {
return nil, response.StatusCode, err
}
default:
}
return response, response.StatusCode, nil
}
func GetAuthFromRequest(c *gin.Context) (paas.Auth, error) {
_, errAuth := c.Cookie("auth")
username, errUsername := c.Cookie("username")
token, errToken := c.Cookie("PVEAuthCookie")
csrf, errCSRF := c.Cookie("CSRFPreventionToken")
access, errAccess := c.Cookie("PAASAccessManagerTicket")
if errUsername != nil || errAuth != nil || errToken != nil || errCSRF != nil || errAccess != nil {
return paas.Auth{}, fmt.Errorf("error occured getting user cookies: (auth: %s, token: %s, csrf: %s)", errAuth, errToken, errCSRF)
} else {
return paas.Auth{Username: username, Token: token, CSRF: csrf, AccessManagerTicket: access}, nil
}
}
func GetInstancePathFromRequest(c *gin.Context) (paas.InstancePath, error) {
req_node := c.Query("node")
req_type := c.Query("type")
req_vmid := c.Query("vmid")
if req_node == "" || req_type == "" || req_vmid == "" {
return paas.InstancePath{}, fmt.Errorf("request missing required values: (node: %s, type: %s, vmid: %s)", req_node, req_type, req_vmid)
}
vmid_int, err := strconv.ParseUint(req_vmid, 10, 64)
if err != nil {
return paas.InstancePath{}, err
}
vm_path := paas.InstancePath{
NodeName: req_node,
InstanceType: paas.InstanceType(req_type),
InstanceID: paas.InstanceID(vmid_int),
}
return vm_path, nil
}
func GetRequestContextFromCookies(auth paas.Auth) map[string]string {
return map[string]string{
"username": auth.Username,
"PVEAuthCookie": auth.Token,
"CSRFPreventionToken": auth.CSRF,
"PAASAccessManagerTicket": auth.AccessManagerTicket,
}
}
-252
View File
@@ -1,252 +0,0 @@
package routes
import (
"fmt"
"net/http"
paas "proxmoxaas-common-lib"
"proxmoxaas-dashboard/app/common"
"github.com/gerow/go-color"
"github.com/gin-gonic/gin"
"github.com/go-viper/mapstructure/v2"
)
type Pool struct {
paas.Pool `mapstructure:",squash"`
ResourceCharts map[string]map[string]any
}
type ResourceChart struct {
Type string
Display bool
Name string
Used int64
Max int64
Avail string
Prefix string
Unit string
ColorHex string
}
var Red = color.RGB{
R: 1,
G: 0,
B: 0,
}
var Green = color.RGB{
R: 0,
G: 1,
B: 0,
}
func HandleGETAccount(c *gin.Context) {
auth, err := common.GetAuthFromRequest(c)
if err == nil {
user, err := GetUserBasic(auth)
if err != nil {
common.HandleNonFatalError(c, err)
return
}
pools, err := GetUserPools(auth)
if err != nil {
common.HandleNonFatalError(c, err)
return
}
c.HTML(http.StatusOK, "html/account.html", gin.H{
"global": common.Global,
"page": "account",
"user": user,
"pools": pools,
})
} else {
c.Redirect(http.StatusFound, "/login") // if user is not authed, redirect user to login page
}
}
func GetUserBasic(auth paas.Auth) (paas.User, error) {
user := paas.User{}
body := map[string]any{}
res, code, err := common.RequestGetAPI(fmt.Sprintf("/access/users/%s", auth.Username), &auth, &body)
if err != nil {
return user, err
}
if code != 200 {
return user, fmt.Errorf("request to /access/pools resulted in %+v", res)
}
err = mapstructure.Decode(body["user"], &user)
return user, err
}
func GetUserPools(auth paas.Auth) (map[string]Pool, error) {
pools := map[string]Pool{}
// get all pools
body := map[string]any{}
res, code, err := common.RequestGetAPI("/access/pools", &auth, &body)
if err != nil {
return pools, err
}
if code != 200 {
return pools, fmt.Errorf("request to /access/pools resulted in %+v", res)
}
err = mapstructure.Decode(body["pools"].(map[string]any), &pools)
if err != nil {
return pools, err
}
// get global config for resource type metadata
body = map[string]any{}
// get resource meta data
res, code, err = common.RequestGetAPI("/global/config/resources", &auth, &body)
if err != nil {
return pools, err
}
if code != 200 {
return pools, fmt.Errorf("request to /global/config/resources resulted in %+v", res)
}
meta := body["resources"].(map[string]any)
// for each pool
for poolname, pool := range pools {
// for each resource in pool data
// create pool charts map
pool.ResourceCharts = make(map[string]map[string]any)
for k, v := range pool.Resources {
m := meta[k].(map[string]any)
t := m["type"].(string)
r := v.(map[string]any)
category := m["category"].(string)
// create a category if it does not already exist
if _, ok := pool.ResourceCharts[category]; !ok {
pool.ResourceCharts[category] = map[string]any{}
}
// depending on type, decode the apool data into the corresponding resource type
switch t {
case "numeric":
n := paas.NumericResource{}
n.Type = t
err_m := mapstructure.Decode(m, &n)
err_r := mapstructure.Decode(r, &n)
if err_m != nil || err_r != nil {
return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
}
pool.ResourceCharts[category][k] = n
case "storage":
n := paas.StorageResource{}
n.Type = t
err_m := mapstructure.Decode(m, &n)
err_r := mapstructure.Decode(r, &n)
if err_m != nil || err_r != nil {
return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
}
pool.ResourceCharts[category][k] = n
case "list":
n := paas.ListResource{}
n.Type = t
err_m := mapstructure.Decode(m, &n)
err_r := mapstructure.Decode(r, &n)
if err_m != nil || err_r != nil {
return pools, fmt.Errorf("%s\n%s", err_m.Error(), err_r.Error())
}
pool.ResourceCharts[category][k] = n
}
// delete the old entry, only categories should be left at the end of the loop
//delete(pools[poolname].Resources, k)
}
pools[poolname] = pool
}
err = FormatPoolResourceCharts(&pools)
if err != nil {
return pools, err
}
return pools, nil
}
func FormatPoolResourceCharts(pools *map[string]Pool) error {
for poolname, pool := range *pools {
// for each resource category
for categoryname, category := range pool.ResourceCharts {
// for each resource in each category
for resourcename, resource := range category {
// create a resource chart for resource depending on resource type
switch t := resource.(type) {
case paas.NumericResource:
avail, prefix := paas.FormatNumber(paas.SafeUint64(t.Total.Avail*t.Multiplier), t.Base)
(*pools)[poolname].ResourceCharts[categoryname][resourcename] = 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,
ColorHex: InterpolateColorHSV(Green, Red, float64(t.Total.Used)/float64(t.Total.Max)).ToHTML(),
}
case paas.StorageResource:
avail, prefix := paas.FormatNumber(paas.SafeUint64(t.Total.Avail*t.Multiplier), t.Base)
(*pools)[poolname].ResourceCharts[categoryname][resourcename] = 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,
ColorHex: InterpolateColorHSV(Green, Red, float64(t.Total.Used)/float64(t.Total.Max)).ToHTML(),
}
case paas.ListResource:
l := struct {
Type string
Display bool
Resources []ResourceChart
}{
Type: t.Type,
Display: t.Display,
Resources: []ResourceChart{},
}
for _, r := range t.Total {
avail := fmt.Sprintf("%d", r.Avail)
l.Resources = append(l.Resources, ResourceChart{
Type: t.Type,
Display: t.Display,
Name: r.Name,
Used: r.Used,
Max: r.Max,
Avail: avail, // usually an int
Unit: "",
ColorHex: InterpolateColorHSV(Green, Red, float64(r.Used)/float64(r.Max)).ToHTML(),
})
}
(*pools)[poolname].ResourceCharts[categoryname][resourcename] = l
}
}
}
}
return nil
}
// interpolate between min and max by normalized (0 - 1) val
func InterpolateColorHSV(min color.RGB, max color.RGB, val float64) color.RGB {
minhsl := min.ToHSL()
maxhsl := max.ToHSL()
interphsl := color.HSL{
H: (1-val)*minhsl.H + (val)*maxhsl.H,
S: (1-val)*minhsl.S + (val)*maxhsl.S,
L: (1-val)*minhsl.L + (val)*maxhsl.L,
}
return interphsl.ToRGB()
}
-107
View File
@@ -1,107 +0,0 @@
package routes
import (
"fmt"
"net/http"
paas "proxmoxaas-common-lib"
"proxmoxaas-dashboard/app/common"
"time"
"github.com/gin-gonic/gin"
"github.com/go-viper/mapstructure/v2"
)
type InstanceBackup struct {
Volid string `json:"volid"`
Notes string `json:"notes"`
Size int64 `json:"size"`
CTime int64 `json:"ctime"`
SizeFormatted string
TimeFormatted string
}
func HandleGETBackups(c *gin.Context) {
auth, err := common.GetAuthFromRequest(c)
if err == nil {
vm_path, err := common.GetInstancePathFromRequest(c)
if err != nil {
common.HandleNonFatalError(c, err)
return
}
backups, err := GetInstanceBackups(vm_path, auth)
if err != nil {
common.HandleNonFatalError(c, fmt.Errorf("error encountered getting instance backups: %s", err.Error()))
}
config, err := GetInstanceConfig(vm_path, auth) // only used for the VM's name
if err != nil {
common.HandleNonFatalError(c, fmt.Errorf("error encountered getting instance config: %s", err.Error()))
}
c.HTML(http.StatusOK, "html/backups.html", gin.H{
"global": common.Global,
"page": "backups",
"backups": backups,
"config": config,
})
} else {
c.Redirect(http.StatusFound, "/login")
}
}
func HandleGETBackupsFragment(c *gin.Context) {
auth, err := common.GetAuthFromRequest(c)
if err == nil { // user should be authed, try to return index with population
vm_path, err := common.GetInstancePathFromRequest(c)
if err != nil {
common.HandleNonFatalError(c, err)
return
}
backups, err := GetInstanceBackups(vm_path, auth)
if err != nil {
common.HandleNonFatalError(c, fmt.Errorf("error encountered getting instance backups: %s", err.Error()))
}
c.Header("Content-Type", "text/plain")
err = common.TMPL.ExecuteTemplate(c.Writer, "html/backups-backups.go.tmpl", gin.H{
"backups": backups,
})
if err != nil {
c.Status(http.StatusInternalServerError)
} else {
c.Status(http.StatusOK)
}
} else { // return 401
c.Status(http.StatusUnauthorized)
}
}
func GetInstanceBackups(vm paas.InstancePath, auth paas.Auth) ([]InstanceBackup, error) {
backups := []InstanceBackup{}
path := fmt.Sprintf("/cluster/%s/%s/%d/backup", vm.NodeName, string(vm.InstanceType), uint64(vm.InstanceID))
body := []any{}
res, code, err := common.RequestGetAPI(path, &auth, &body)
if err != nil {
return backups, err
}
if code != 200 {
return backups, fmt.Errorf("request to %s resulted in %+v", path, res)
}
err = mapstructure.Decode(body, &backups)
if err != nil {
return backups, err
}
for i := range backups {
size, prefix := paas.FormatNumber(paas.SafeUint64(backups[i].Size), paas.Base1024)
backups[i].SizeFormatted = fmt.Sprintf("%s %sB", size, prefix)
t := time.Unix(backups[i].CTime, 0)
backups[i].TimeFormatted = t.Format("02-01-06 15:04:05")
}
return backups, nil
}
-297
View File
@@ -1,297 +0,0 @@
package routes
import (
"fmt"
"net/http"
paas "proxmoxaas-common-lib"
"proxmoxaas-dashboard/app/common"
"slices"
"sort"
"github.com/gin-gonic/gin"
"github.com/go-viper/mapstructure/v2"
)
type InstanceConfig struct {
paas.Instance `mapstructure:",squash"`
// overrides
ProctypeSelect common.Select
}
type GlobalConfig struct {
CPU struct {
Whitelist bool
}
}
type PoolConfig struct {
CPU struct {
Global []paas.Match
Nodes map[string][]paas.Match
}
}
func HandleGETConfig(c *gin.Context) {
auth, err := common.GetAuthFromRequest(c)
if err == nil {
vm_path, err := common.GetInstancePathFromRequest(c)
if err != nil {
common.HandleNonFatalError(c, err)
return
}
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, config.Pool, 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.GetAuthFromRequest(c)
if err == nil {
vm_path, err := common.GetInstancePathFromRequest(c)
if err != nil {
common.HandleNonFatalError(c, err)
return
}
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")
err = common.TMPL.ExecuteTemplate(c.Writer, "html/config-volumes.go.tmpl", gin.H{
"config": config,
})
if err != nil {
c.Status(http.StatusInternalServerError)
} else {
c.Status(http.StatusOK)
}
} else {
c.Status(http.StatusUnauthorized)
}
}
func HandleGETConfigNetsFragment(c *gin.Context) {
auth, err := common.GetAuthFromRequest(c)
if err == nil {
vm_path, err := common.GetInstancePathFromRequest(c)
if err != nil {
common.HandleNonFatalError(c, err)
return
}
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")
err = common.TMPL.ExecuteTemplate(c.Writer, "html/config-nets.go.tmpl", gin.H{
"config": config,
})
if err != nil {
c.Status(http.StatusInternalServerError)
} else {
c.Status(http.StatusOK)
}
} else {
c.Status(http.StatusUnauthorized)
}
}
func HandleGETConfigDevicesFragment(c *gin.Context) {
auth, err := common.GetAuthFromRequest(c)
if err == nil {
vm_path, err := common.GetInstancePathFromRequest(c)
if err != nil {
common.HandleNonFatalError(c, err)
return
}
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")
err = common.TMPL.ExecuteTemplate(c.Writer, "html/config-devices.go.tmpl", gin.H{
"config": config,
})
if err != nil {
c.Status(http.StatusInternalServerError)
} else {
c.Status(http.StatusOK)
}
} else {
c.Status(http.StatusUnauthorized)
}
}
func HandleGETConfigBootFragment(c *gin.Context) {
auth, err := common.GetAuthFromRequest(c)
if err == nil {
vm_path, err := common.GetInstancePathFromRequest(c)
if err != nil {
common.HandleNonFatalError(c, err)
return
}
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")
err = common.TMPL.ExecuteTemplate(c.Writer, "html/config-boot.go.tmpl", gin.H{
"config": config,
})
if err != nil {
c.Status(http.StatusInternalServerError)
} else {
c.Status(http.StatusOK)
}
} else {
c.Status(http.StatusUnauthorized)
}
}
func GetInstanceConfig(vm paas.InstancePath, auth paas.Auth) (InstanceConfig, error) {
config := InstanceConfig{}
path := fmt.Sprintf("/cluster/%s/%s/%d/", vm.NodeName, string(vm.InstanceType), uint64(vm.InstanceID))
body := map[string]any{}
res, code, err := common.RequestGetAPI(path, &auth, &body)
if err != nil {
return config, err
}
if code != 200 {
return config, fmt.Errorf("request to %s resulted in %+v", path, res)
}
err = mapstructure.Decode(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
}
func GetCPUTypes(vm paas.InstancePath, pool string, auth paas.Auth) (common.Select, error) {
cputypes := common.Select{
ID: "proctype",
Required: true,
}
// get global resource config
body := map[string]any{}
path := "/global/config/resources"
res, code, err := common.RequestGetAPI(path, &auth, &body)
if err != nil {
return cputypes, err
}
if code != 200 {
return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res)
}
globalConfig := GlobalConfig{}
err = mapstructure.Decode(body["resources"], &globalConfig)
if err != nil {
return cputypes, err
}
// get pool resource config
body = map[string]any{}
path = fmt.Sprintf("/access/pools/%s", pool)
res, code, err = common.RequestGetAPI(path, &auth, &body)
if err != nil {
return cputypes, err
}
if code != 200 {
return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res)
}
poolCPUConfig := PoolConfig{}
err = mapstructure.Decode(body["pool"].(map[string]any)["resources"], &poolCPUConfig)
if err != nil {
return cputypes, err
}
// use node specific rules if present, otherwise use global rules
var userCPU []paas.Match
if _, ok := poolCPUConfig.CPU.Nodes[vm.NodeName]; ok {
userCPU = poolCPUConfig.CPU.Nodes[vm.NodeName]
} else {
userCPU = poolCPUConfig.CPU.Global
}
if globalConfig.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
body = map[string]any{}
path = fmt.Sprintf("/proxmox/nodes/%s/capabilities/qemu/cpu", vm.NodeName)
res, code, err = common.RequestGetAPI(path, &auth, &body)
if err != nil {
return cputypes, err
}
if code != 200 {
return cputypes, fmt.Errorf("request to %s resulted in %+v", path, res)
}
supported := struct {
data []paas.Match
}{}
err = mapstructure.Decode(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 paas.Match) 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
}
-222
View File
@@ -1,222 +0,0 @@
package routes
import (
"fmt"
"net/http"
paas "proxmoxaas-common-lib"
"proxmoxaas-dashboard/app/common"
"strconv"
"github.com/gin-gonic/gin"
"github.com/go-viper/mapstructure/v2"
)
// primary type used in constructing instance cards in index
type InstanceCard struct {
VMID uint
Name string
Type string
Status string
Node string
NodeStatus string
ConfigPath string
ConsolePath string
BackupsPath string
}
// used in constructing instance cards in index
type Node struct {
Node string `json:"node"`
Status string `json:"status"`
}
// used in retriving cluster tasks
type Task struct {
Type string
Node string
User string
ID string
VMID uint
Status string
EndTime uint
}
type InstanceStatus struct {
Status string
}
func HandleGETIndex(c *gin.Context) {
auth, err := common.GetAuthFromRequest(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)
return
}
page := gin.H{
"global": common.Global,
"page": "index",
"instances": instances,
}
c.HTML(http.StatusOK, "html/index.html", page)
} 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.GetAuthFromRequest(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)
return
}
c.Header("Content-Type", "text/plain")
err = common.TMPL.ExecuteTemplate(c.Writer, "html/index-instances.go.tmpl", gin.H{
"instances": instances,
})
if err != nil {
c.Status(http.StatusInternalServerError)
} else {
c.Status(http.StatusOK)
}
} else { // return 401
c.Status(http.StatusUnauthorized)
}
}
func GetClusterResources(auth paas.Auth) (map[uint]InstanceCard, map[string]Node, error) {
body := []any{}
res, code, err := common.RequestGetAPI("/proxmox/cluster/resources", &auth, &body)
if err != nil {
return nil, nil, err
}
if code != 200 { // if we did not successfully retrieve resources, then return the error 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{}
// parse /proxmox/cluster/resources to separate instances and nodes
for _, v := range body {
m := v.(map[string]any)
switch m["type"] {
case "node": // if type is node -> parse as Node object
node := Node{}
err := mapstructure.Decode(v, &node)
if err != nil {
return nil, nil, err
}
nodes[node.Node] = node
case "lxc", "qemu": // if type is lxc or qemu -> parse as InstanceCard object
instance := InstanceCard{}
err := mapstructure.Decode(v, &instance)
if err != nil {
return nil, nil, err
}
instances[instance.VMID] = instance
}
}
// once all basic instance and node stuff is parsed, go back and fill in cross referenced data
for vmid, instance := range instances {
// set instance's node status
instance.NodeStatus = nodes[instance.Node].Status
// set instance's config link path
instance.ConfigPath = fmt.Sprintf("config?node=%s&type=%s&vmid=%d", instance.Node, instance.Type, instance.VMID)
// set the instance's console link path
switch instance.Type {
case "qemu":
instance.ConsolePath = fmt.Sprintf("%s/?console=kvm&vmid=%d&vmname=%s&node=%s&resize=off&cmd=&novnc=1", common.Global.PVE, instance.VMID, instance.Name, instance.Node)
case "lxc":
instance.ConsolePath = fmt.Sprintf("%s/?console=lxc&vmid=%d&vmname=%s&node=%s&resize=off&cmd=&xtermjs=1", common.Global.PVE, instance.VMID, instance.Name, instance.Node)
}
// set the instance's backups link path
instance.BackupsPath = fmt.Sprintf("backups?node=%s&type=%s&vmid=%d", instance.Node, instance.Type, instance.VMID)
// save back to instances map
instances[vmid] = instance
}
body = []any{}
res, code, err = common.RequestGetAPI("/proxmox/cluster/tasks", &auth, &body)
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)
}
most_recent_task := map[uint]uint{}
expected_states := map[uint]string{}
// iterate through recent user accessible tasks to find the task most recently made on an instance
for _, v := range body {
// parse task as Task object
task := Task{}
err := mapstructure.Decode(v, &task)
if err != nil {
return nil, nil, err
}
// try to get task vmid but continue if it would be an invalid vmid
x, err := strconv.Atoi(task.ID)
// if there was an error converting the task's vmid, skip it
if err != nil {
continue
}
task.VMID = uint(x)
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"
if task.EndTime > most_recent_task[task.VMID] { // if the task's end time is later than the most recent one encountered
most_recent_task[task.VMID] = task.EndTime // update the most recent task
switch task.Type {
case "qmstart", "vzstart": // if the task was a start task, update the expected state to running
expected_states[task.VMID] = "running"
case "qmstop", "vzstop": // if the task was a stop task, update the expected state to stopped
expected_states[task.VMID] = "stopped"
}
}
}
}
// iterate through the instances with recent tasks, refetch their state from a more reliable source
for vmid, expected_state := range expected_states { // for the expected states from recent tasks
if instances[vmid].Status != expected_state { // if the current node's state from /cluster/resources differs from expected state
// get /status/current which is updated faster than /cluster/resources
instance := instances[vmid]
path := fmt.Sprintf("/proxmox/nodes/%s/%s/%d/status/current", instance.Node, instance.Type, instance.VMID)
body := map[string]any{}
res, code, err := common.RequestGetAPI(path, &auth, &body)
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)
}
// attempt to decode task status as instance status
status := InstanceStatus{}
err = mapstructure.Decode(body, &status)
if err != nil { // did not successfully decode task status, just skip
continue
}
instance.Status = status.Status
instances[vmid] = instance
}
}
return instances, nodes, nil
}
-68
View File
@@ -1,68 +0,0 @@
package routes
import (
"fmt"
"net/http"
"proxmoxaas-dashboard/app/common"
"github.com/gin-gonic/gin"
"github.com/go-viper/mapstructure/v2"
)
// stores each realm's data
type Realm struct {
Default int `json:"default"`
Realm string `json:"realm"`
Comment string `json:"comment"`
}
func GetLoginRealms() ([]Realm, error) {
realms := []Realm{}
body := []any{}
res, code, err := common.RequestGetAPI("/proxmox/access/domains", nil, &body)
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 body {
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
}
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
@@ -1,20 +0,0 @@
package routes
import (
"net/http"
"proxmoxaas-dashboard/app/common"
"github.com/gin-gonic/gin"
)
func HandleGETSettings(c *gin.Context) {
_, err := common.GetAuthFromRequest(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
@@ -0,0 +1,71 @@
<!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>
-7
View File
@@ -1,7 +0,0 @@
{
"listenPort": 8080,
"organization": "myorg",
"dashurl": "https://paas.mydomain.example",
"apiurl": "https://paas.mydomain.example/api",
"pveurl": "https://pve.mydomain.example"
}
+55
View File
@@ -0,0 +1,55 @@
.input-grid {
display: grid;
gap: 5px 10px;
align-items: center;
width: 100%;
}
.input-grid * {
margin-top: 0;
margin-bottom: 0;
}
.input-grid .last-item {
text-align: right;
}
legend {
float: left;
width: 100%;
margin: 0;
padding: 0;
line-height: 1.5em;
margin-top: 0.25lh;
margin-bottom: 0.25lh;
}
fieldset {
border: 0;
}
fieldset > *:last-child {
margin-bottom: 8px;
}
fieldset > .input-grid {
float: left;
}
body:not(:-moz-handler-blocked) fieldset {
display: table-cell;
}
input[type="checkbox"] {
width: min-content;
margin-left: auto;
margin-right: auto;
}
input[type="radio"] {
position: inherit;
}
div[draggable="true"] {
cursor: grab;
}
+13 -38
View File
@@ -1,30 +1,15 @@
: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: 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;
--nav-header-text-color: black;
--nav-link-active-text-color: black;
--nav-link-active-bg-color: var(--main-bg-color, white);
}
}
@media screen and (prefers-color-scheme: light) {
:root, :root.light-theme {
:root {
--nav-bg-color: black;
--nav-text-color: white;
--nav-header-bg-color: #0f0;
@@ -32,16 +17,6 @@
--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 {
@@ -53,7 +28,7 @@ header {
}
header h1 {
font-size: var(--small-font-size);
font-size: 18px;
margin: 0;
background-color: var(--nav-header-bg-color);
color: var(--nav-header-text-color);
@@ -61,15 +36,15 @@ header h1 {
}
nav {
font-size: var(--small-font-size);
overflow: hidden;
font-size: larger;
width: fit-content;
}
nav a, header h1, label[for="navtoggle"] {
text-align: left;
padding-left: 0.5em;
padding-right: 0.5em;
padding-left: 8px;
padding-right: 8px;
text-decoration: none;
vertical-align: middle;
height: 2em;
@@ -80,7 +55,7 @@ label[for="navtoggle"], #navtoggle {
display: none;
}
@media screen and (width >= 601px){
@media screen and (width >= 600px){
header {
grid-template-columns: auto 1fr;
}
@@ -106,7 +81,7 @@ label[for="navtoggle"], #navtoggle {
}
}
@media screen and (width <= 601px){
@media screen and (width <= 600px){
header {
grid-template-columns: 1fr auto;
}
+126
View File
@@ -0,0 +1,126 @@
: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;
}
-16
View File
@@ -1,16 +0,0 @@
{
"extends": [
"html-validate:recommended"
],
"rules": {
"no-inline-style": "off"
},
"elements": [
"html5",
{
"head": {
"requiredContent": []
}
}
]
}
-3
View File
@@ -1,3 +0,0 @@
web/modules/*
WFA-JS/*
dist/*
-33
View File
@@ -1,33 +0,0 @@
import { defineConfig, globalIgnores } from "eslint/config";
import globals from "globals";
import js from "@eslint/js";
export default defineConfig([
js.configs.recommended,
globalIgnores(["dist/", "web/modules", "WFA-JS"]),
{
languageOptions: {
globals: {
...globals.browser,
},
ecmaVersion: "latest",
sourceType: "module",
},
rules: {
"no-tabs": ["error", {
allowIndentationTabs: true,
}],
indent: ["error", "tab"],
"linebreak-style": ["error", "unix"],
quotes: ["error", "double"],
semi: ["error", "always"],
"brace-style": ["error", "stroustrup", { allowSingleLine: false }],
"no-unused-vars": ["warn", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}],
"prefer-const": ["error"]
},
}
]);
-49
View File
@@ -1,49 +0,0 @@
module proxmoxaas-dashboard
go 1.27.0
require (
github.com/gerow/go-color v0.0.0-20140219113758-125d37f527f1
github.com/gin-gonic/gin v1.12.0
github.com/go-viper/mapstructure/v2 v2.5.0
github.com/tdewolff/minify/v2 v2.24.17
proxmoxaas-common-lib v0.0.0
web v0.0.0
)
replace proxmoxaas-common-lib => ./proxmoxaas-common-lib
replace web => ./dist/web
require (
github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.2 // indirect
github.com/bytedance/sonic/loader v0.5.2 // indirect
github.com/cloudwego/base64x v0.1.7 // indirect
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
github.com/gin-contrib/sse v1.1.1 // 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.30.3 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.4.0 // indirect
github.com/leodido/go-urn v1.5.0 // indirect
github.com/mattn/go-isatty v0.0.24 // 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.4.3 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.61.0 // indirect
github.com/tdewolff/parse/v2 v2.8.16 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.2 // indirect
go.mongodb.org/mongo-driver/v2 v2.8.1 // indirect
golang.org/x/arch v0.30.0 // indirect
golang.org/x/crypto v0.55.0 // indirect
golang.org/x/net v0.58.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.41.0 // indirect
google.golang.org/protobuf v1.36.12 // indirect
)
+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 292 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 295 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 296 B

+16
View File
@@ -0,0 +1,16 @@
<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>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 313 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 389 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 446 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 377 B

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

After

Width:  |  Height:  |  Size: 64 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 299 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 205 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 296 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 202 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 329 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 296 B

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

After

Width:  |  Height:  |  Size: 1.1 KiB

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

After

Width:  |  Height:  |  Size: 1018 B

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

After

Width:  |  Height:  |  Size: 537 B

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

After

Width:  |  Height:  |  Size: 446 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 232 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 285 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 296 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 208 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 798 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 3.0 KiB

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 495 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 405 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 281 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 610 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 533 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 351 B

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

After

Width:  |  Height:  |  Size: 125 B

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

After

Width:  |  Height:  |  Size: 124 B

+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.5 KiB

+103
View File
@@ -0,0 +1,103 @@
<!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
@@ -1,11 +0,0 @@
[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
@@ -0,0 +1,40 @@
<!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
@@ -0,0 +1,328 @@
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 };
}
+19
View File
@@ -0,0 +1,19 @@
{
"name": "proxmoxaas-dashboard",
"version": "0.0.1",
"description": "Front-end for ProxmoxAAS",
"type": "module",
"scripts": {
"lint": "html-validator --continue; stylelint --formatter verbose --fix **/*.css; DEBUG=eslint:cli-engine eslint --fix scripts/"
},
"devDependencies": {
"eslint": "^8.43.0",
"eslint-config-standard": "^17.1.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-n": "^16.0.1",
"eslint-plugin-promise": "^6.1.1",
"stylelint": "^15.9.0",
"stylelint-config-standard": "^33.0.0",
"w3c-html-validator": "^1.4.0"
}
}
-9
View File
@@ -1,9 +0,0 @@
package main
import (
app "proxmoxaas-dashboard/app"
)
func main() {
app.Run()
}
+149
View File
@@ -0,0 +1,149 @@
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
@@ -0,0 +1,120 @@
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);
@@ -1,18 +1,26 @@
import { getSetting, requestAPI } from "./utils.js";
import { error } from "./dialog.js";
import { requestAPI } from "./utils.js";
import { API } from "../vars.js";
export async function setupClientSync (callback) {
const scheme = getSetting("sync-scheme");
const rate = getSetting("sync-rate");
if (scheme === "never") {
return;
let scheme = localStorage.getItem("sync-scheme");
let rate = Number(localStorage.getItem("sync-rate"));
if (!scheme) {
scheme = "always";
localStorage.setItem("sync-scheme", "always");
}
else if (scheme === "always") {
if (!rate) {
rate = "5";
localStorage.setItem("sync-rate", "5");
}
if (scheme === "always") {
callback();
window.setInterval(callback, rate * 1000);
}
else if (scheme === "hash") {
const newHash = (await requestAPI("/sync/hash")).data;
localStorage.setItem("sync-current-hash", newHash);
callback();
window.setInterval(async () => {
const newHash = (await requestAPI("/sync/hash")).data;
if (localStorage.getItem("sync-current-hash") !== newHash) {
@@ -22,8 +30,9 @@ export async function setupClientSync (callback) {
}, rate * 1000);
}
else if (scheme === "interrupt") {
const socket = new WebSocket(`wss://${window.API.replace("https://", "")}/sync/interrupt`);
socket.addEventListener("open", (_event) => {
callback();
const socket = new WebSocket(`wss://${API.replace("https://", "")}/sync/interrupt`);
socket.addEventListener("open", (event) => {
socket.send(`rate ${rate}`);
});
socket.addEventListener("message", (event) => {
@@ -32,12 +41,12 @@ export async function setupClientSync (callback) {
callback();
}
else {
error("clientsync: recieved unexpected message from server, closing socket.");
console.error("clientsync: recieved unexpected message from server, closing socket.");
socket.close();
}
});
}
else {
error(`clientsync: unsupported scheme ${scheme} selected.`);
console.error(`clientsync: unsupported scheme ${scheme} selected.`);
}
}
+861
View File
@@ -0,0 +1,861 @@
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);
}
}
+55
View File
@@ -0,0 +1,55 @@
export function dialog (header, body, onclose = async (result, form) => { }) {
const dialog = document.createElement("dialog");
dialog.innerHTML = `
<p class="w3-large" id="prompt" style="text-align: center;"></p>
<div id="body"></div>
<div class="w3-center w3-container">
<button id="cancel" value="cancel" form="form" class="w3-button w3-margin" style="background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);" formnovalidate>CANCEL</button>
<button id="confirm" value="confirm" form="form" class="w3-button w3-margin" style="background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);">CONFIRM</button>
</div>
`;
dialog.className = "w3-container w3-card w3-border-0";
dialog.querySelector("#prompt").innerText = header;
dialog.querySelector("#body").innerHTML = body;
dialog.addEventListener("close", async () => {
const formElem = dialog.querySelector("form");
const formData = formElem ? new FormData(formElem) : null;
await onclose(dialog.returnValue, formData);
dialog.parentElement.removeChild(dialog);
});
if (!dialog.querySelector("form")) {
dialog.querySelector("#confirm").addEventListener("click", async (e) => {
e.preventDefault();
dialog.close(e.target.value);
});
dialog.querySelector("#cancel").addEventListener("click", async (e) => {
e.preventDefault();
dialog.close(e.target.value);
});
}
document.body.append(dialog);
dialog.showModal();
return dialog;
}
export function alert (message) {
const dialog = document.createElement("dialog");
dialog.innerHTML = `
<form method="dialog">
<p class="w3-center" style="margin-bottom: 0px;">${message}</p>
<div class="w3-center">
<button class="w3-button w3-margin" id="submit">OK</button>
</div>
</form>
`;
dialog.className = "w3-container w3-card w3-border-0";
document.body.append(dialog);
dialog.showModal();
dialog.addEventListener("close", () => {
dialog.parentElement.removeChild(dialog);
});
return dialog;
}
+211
View File
@@ -0,0 +1,211 @@
// 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
@@ -0,0 +1,248 @@
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
@@ -0,0 +1,252 @@
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);
+16 -17
View File
@@ -1,13 +1,20 @@
import { goToPage, setAppearance, requestAPI } from "./utils.js";
import { error } from "./dialog.js";
import { requestTicket, goToPage, deleteAllCookies, requestPVE, setTitleAndHeader } from "./utils.js";
import { alert } from "./dialog.js";
window.addEventListener("DOMContentLoaded", init);
async function init () {
setTitleAndHeader();
await deleteAllCookies();
setAppearance();
const formSubmitButton = document.querySelector("#submit");
const realms = await requestPVE("/access/domains", "GET");
const realmSelect = document.querySelector("#realm");
realms.data.forEach((element) => {
realmSelect.add(new Option(element.comment, element.realm));
if ("default" in element && element.default === 1) {
realmSelect.value = element.realm;
}
});
formSubmitButton.addEventListener("click", async (e) => {
e.preventDefault();
const form = document.querySelector("form");
@@ -17,28 +24,20 @@ async function init () {
const ticket = await requestTicket(formData.get("username"), formData.get("password"), formData.get("realm"));
if (ticket.status === 200) {
formSubmitButton.innerText = "LOGIN";
goToPage("index");
goToPage("index.html");
}
else if (ticket.status === 401) {
error("Authenticaton failed.");
alert("Authenticaton failed.");
formSubmitButton.innerText = "LOGIN";
}
else if (ticket.status === 408) {
error("Network error.");
alert("Network error.");
formSubmitButton.innerText = "LOGIN";
}
else {
error(`An error occured: ${JSON.stringify(ticket)}`);
alert("An error occured.");
formSubmitButton.innerText = "LOGIN";
console.error(ticket.error);
}
});
}
async function requestTicket (username, password, realm) {
const response = await requestAPI("/access/ticket", "POST", { username: `${username}@${realm}`, password }, false);
return response;
}
async function deleteAllCookies () {
await requestAPI("/access/ticket", "DELETE");
}
+29
View File
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,269 @@
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
@@ -0,0 +1,85 @@
<!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
@@ -0,0 +1,3 @@
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
+2 -18
View File
@@ -1,4 +1,4 @@
/* W3.CSS 5.02 March 31 2025 by Jan Egil and Borge Refsnes */
/* W3.CSS 4.15 December 2020 by Jan Egil and Borge Refsnes */
html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit}
/* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */
html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}
@@ -108,8 +108,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px}
.w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px}
.w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px}
.w3-grid{display:grid}.w3-grid-padding{display:grid;gap:16px}.w3-flex{display:flex}
.w3-text-center{text-align:center}.w3-text-bold,.w3-bold{font-weight:bold}.w3-text-italic,.w3-italic{font-style:italic}
.w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px}
.w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word}
.w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%}
@@ -150,7 +148,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-button:hover{color:#000!important;background-color:#ccc!important}
.w3-transparent,.w3-hover-none:hover{background-color:transparent!important}
.w3-hover-none:hover{box-shadow:none!important}
.w3-rtl{direction:rtl}.w3-ltr{direction:ltr}
/* Colors */
.w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important}
.w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important}
@@ -178,19 +175,6 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important}
.w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important}
.w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important}
.w3-asphalt,.w3-hover-asphalt:hover{color:#fff!important;background-color:#343a40!important}
.w3-crimson,.w3-hover-crimson:hover{color:#fff!important;background-color:#a20025!important}
.w3-cobalt,w3-hover-cobalt:hover{color:#fff!important;background-color:#0050ef!important}
.w3-emerald,.w3-hover-emerald:hover{color:#fff!important;background-color:#008a00!important}
.w3-olive,.w3-hover-olive:hover{color:#fff!important;background-color:#6d8764!important}
.w3-paper,.w3-hover-paper:hover{color:#000!important;background-color:#f8f9fa!important}
.w3-sienna,.w3-hover-sienna:hover{color:#fff!important;background-color:#a0522d!important}
.w3-taupe,.w3-hover-taupe:hover{color:#fff!important;background-color:#87794e!important}
.w3-danger{color:#fff!important;background-color:#dd0000!important}
.w3-note{color:#000!important;background-color:#fff599!important}
.w3-info{color:#fff!important;background-color:#0a6fc2!important}
.w3-warning{color:#000!important;background-color:#ffb305!important}
.w3-success{color:#fff!important;background-color:#008a00!important}
.w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important}
.w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important}
.w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important}
@@ -248,4 +232,4 @@ hr{border:0;border-top:1px solid #eee;margin:20px 0}
.w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important}
.w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important}
.w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important}
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
.w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important}
-101
View File
@@ -1,101 +0,0 @@
input, select, textarea {
background-color: var(--main-input-bg-color);
color: var(--main-text-color);
}
.input-grid {
display: grid;
gap: 0.5em 1em;
align-items: center;
width: 100%;
}
.input-grid * {
margin-top: 0;
margin-bottom: 0;
padding-top: 0.5em;
padding-bottom: 0.5em;
}
.input-grid input {
padding: 0.5em;
}
.input-grid img {
padding: 0;
}
.input-grid .last-item {
text-align: right;
}
legend {
float: left;
width: 100%;
margin: 0;
padding: 0;
margin-top: 0.25em;
margin-bottom: 0.25em;
}
fieldset {
border: 0;
padding: 0;
}
fieldset > *:last-child {
margin-bottom: 0.5em;
}
fieldset > .input-grid {
float: left;
}
body:not(:-moz-handler-blocked) fieldset {
display: table-cell;
}
input[type="checkbox"] {
width: min-content;
margin-left: auto;
margin-right: auto;
}
input[type="radio"] {
position: inherit;
}
.w3-select, select {
padding: 0.5em;
}
.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: 0.5em;
}
dialog {
margin: auto;
max-width: calc(min(100% - 16px, 80ch));
color: var(--main-text-color);
}
dialog #prompt {
margin-top: 1em;
margin-bottom: 1em;
text-align: center;
}
dialog button[value="confirm"] {
background-color: var(--positive-color, #0f0); color: var(--lightbg-text-color, black);
}
dialog button[value="cancel"] {
background-color: var(--negative-color, #f00); color: var(--lightbg-text-color, black);
}
-216
View File
@@ -1,216 +0,0 @@
:root {
--negative-color: #f00;
--positive-color: #0f0;
--highlight-color: yellow;
--lightbg-text-color: black;
--large-font-size: 32px;
--medium-font-size: 24px;
--small-font-size: 16px;
}
@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-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-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-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-input-bg-color: #404040;
}
}
*, h1, h2, h3, h4, h5, h6, p {
box-sizing: border-box;
font-family: monospace;
line-height: normal;
margin: 0;
padding: 0;
}
h2 {
font-size: var(--large-font-size);
margin-top: 0.5em;
margin-bottom: 0.5em;
}
h3 {
font-size: var(--medium-font-size);
margin-top: 0.5em;
margin-bottom: 0.5em;
}
h4, legend {
font-size: var(--small-font-size);
text-decoration: underline;
margin-top: 0.5em;
margin-bottom: 0.5em;
}
p {
font-size: var(--small-font-size);
}
html {
background-color: var(--main-bg-color);
}
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;
}
img, svg {
height: 1em;
width: 1em;
color: var(--main-text-color)
}
a img {
vertical-align: unset;
}
hr {
border-color: var(--main-text-color);
}
.clickable {
cursor: pointer;
}
.flex {
display: flex;
}
.row {
flex-direction: row;
column-gap: 10px;
align-items: center;
}
.row-reverse {
flex-direction: row-reverse;
row-gap: 10px;
align-items: center;
}
.column {
flex-direction: column;
row-gap: 10px;
align-items: center;
}
.column-reverse {
flex-direction: column-reverse;
row-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 screen and (width >= 993px) {
.hide-large {display: none !important;}
}
/* fixes edge case in w3-hide-medium where width between 992 and 993 */
@media screen and (width <= 993px) and (width >= 601px){
.hide-large {display: none !important;}
.hide-medium {display:none !important}
}
/* fixes edge case in w3-hide-small when width between 600 and 601 */
@media screen and (width <= 601px) and (width >= 440px) {
.hide-large {display: none !important;}
.hide-medium {display:none !important}
.hide-small {display:none !important}
}
@media screen and (width <= 440px) {
.hide-large {display: none !important;}
.hide-medium {display:none !important}
.hide-small {display:none !important}
.hide-tiny { display: none !important;}
}
-21
View File
@@ -1,21 +0,0 @@
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
-3
View File
@@ -1,3 +0,0 @@
module web
go 1.26.0
-73
View File
@@ -1,73 +0,0 @@
<!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>
{{template "header" .}}
<main>
<h2>Account</h2>
<section class="w3-card w3-padding">
<h3>Account Details</h3>
<p id="username">Username: {{.user.Username.UserID}}@{{.user.Username.Realm}}</p>
<p id="email">Email: {{.user.Mail}}</p>
<p>Password: <button class="w3-button" id="change-password" type="button" style="padding: 0em; height: 1.5em; line-height: 1.5em;">Change Password</button></p>
</section>
{{range $poolname, $pool := .pools}}
{{template "pool-resources" $pool}}
{{end}}
</main>
<template id="change-password-dialog">
<link rel="stylesheet" href="modules/w3.css">
<link rel="stylesheet" href="css/style.css">
<link rel="stylesheet" href="css/form.css">
<dialog class="w3-container w3-card w3-border-0">
<p class="w3-large" id="prompt">
Change Password
</p>
<div id="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" autocomplete="new-password" required>
<label for="confirm-password">Confirm Password</label>
<input class="w3-input w3-border" id="confirm-password" name="confirm-password" type="password" autocomplete="new-password" required>
</form>
</div>
<div id="controls" class="w3-center w3-container">
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin">CONFIRM</button>
</div>
</dialog>
</template>
</body>
</html>
-3
View File
@@ -1,3 +0,0 @@
{{range $i, $x := .backups}}
{{template "backup-card" $x}}
{{end}}
-36
View File
@@ -1,36 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{template "head" .}}
<script src="scripts/backups.js" type="module"></script>
<link rel="modulepreload" href="scripts/utils.js">
<link rel="modulepreload" href="scripts/dialog.js">
<style>
</style>
</head>
<body>
{{template "header" .}}
<main>
<h2><a href="index">Instances</a> / {{.config.Name}} / Backups</h2>
<section class="w3-card w3-padding">
<div class="w3-row" style="border-bottom: 1px solid;">
<p class="w3-col l2 m4 s8">Time</p>
<p class="w3-col l6 m6 hide-small">Notes</p>
<p class="w3-col l2 hide-medium">Size</p>
<p class="w3-col l2 m2 s4">Actions</p>
</div>
<div id="backups-container">
{{range $i, $x := .backups}}
{{template "backup-card" $x}}
{{end}}
</div>
<div class="w3-container w3-center">
{{template "backups-add-backup" .}}
</div>
</section>
<div class="w3-container w3-center">
<a class="w3-button w3-margin" id="exit" href="index">EXIT</a>
</div>
</main>
</body>
</html>
-1
View File
@@ -1 +0,0 @@
{{template "boot" .config.Boot}}
-1
View File
@@ -1 +0,0 @@
{{template "devices" .config.Devices}}
-1
View File
@@ -1 +0,0 @@
{{template "nets" .config.Nets}}
-1
View File
@@ -1 +0,0 @@
{{template "volumes" Map "Volumes" .config.Volumes "InstanceType" .config.Type}}
-89
View File
@@ -1,89 +0,0 @@
<!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>
<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>
{{template "header" .}}
<main>
<section>
<h2><a href="index">Instances</a>/{{.config.Name}}/Config</h2>
<form id="config-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" Map "Volumes" .config.Volumes "InstanceType" .config.Type}}
</div>
<div class="w3-container w3-center">
<!--Add Disk Button & Dialog Template-->
{{template "volumes-add-disk" .}}
<!--Add CD Button & Dialog Template-->
{{if eq .config.Type "VM"}}
{{template "volumes-add-cd"}}
{{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">
<!--Add Net Button & Dialog Template-->
{{template "nets-add-net"}}
</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">
<!--Add Device Button & Dialog Template-->
{{template "devices-add-device"}}
</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="submit">EXIT</button>
</div>
</form>
</section>
</main>
</body>
</html>
-3
View File
@@ -1,3 +0,0 @@
{{range .instances}}
{{template "instance-card" .}}
{{end}}
-150
View File
@@ -1,150 +0,0 @@
<!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>
@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);
}
}
@media screen and (width >= 993px) {
#instance-table {
display: grid;
grid-template-columns: repeat(7, auto);
grid-column-gap: 1em;
grid-row-gap: 0.25em;
}
#instance-table-header, #instance-container, #instance-table instance-card {
display: contents;
}
}
@media screen and (width <= 993px) and (width >= 601px){
#instance-table {
display: grid;
grid-template-columns: repeat(5, auto);
grid-column-gap: 1em;
grid-row-gap: 0.25em;
}
#instance-table-header, #instance-container, #instance-table instance-card {
display: contents;
}
}
@media screen and (width <= 601px) and (width >= 440px){
#instance-table {
display: grid;
grid-template-columns: repeat(4, auto);
grid-column-gap: 1em;
grid-row-gap: 0.25em;
}
#instance-table-header, #instance-container, #instance-table instance-card {
display: contents;
}
}
@media screen and (width <= 440px) {
#instance-table {
display: grid;
grid-template-columns: repeat(3, auto);
grid-column-gap: 1em;
grid-row-gap: 0.25em;
}
#instance-table-header, #instance-container, #instance-table instance-card {
display: contents;
}
}
</style>
</head>
<body>
{{template "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" tabindex="0">
<button type="submit" id="submit" class="w3-button" style="padding: 0; width: 1em; height: 1em; line-height: 1em;"><img alt="Search Instances" aria-label="Search Instances" src="images/common/search.svg#symb"></button>
<input type="search" id="search" class="w3-input w3-border" style="height: 1lh; max-width: fit-content;" aria-label="search instances by name">
</form>
<!--Add Instance Button & Dialog Template-->
<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;" alt="Create Instance" aria-label="Create Instance" src="images/actions/instance/add.svg#symb">
</button>
<template id="create-instance-dialog">
<dialog class="w3-container w3-card w3-border-0">
<p class="w3-large" id="prompt">
Create New Instance
</p>
<div id="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" selected-index="-1" required>
<option value="lxc">Container</option>
<option value="qemu">Virtual Machine</option>
</select>
<label for="pool">Pool</label>
<select class="w3-select w3-border" name="pool" id="pool" required></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" type="text" required>
<label for="vmid">ID</label>
<input class="w3-input w3-border" name="vmid" id="vmid" type="number" required>
<label for="cores">Cores (Threads)</label>
<input class="w3-input w3-border" name="cores" id="cores" type="number" min="1" max="8192" required>
<label for="memory">Memory (MiB)</label>
<input class="w3-input w3-border" name="memory" id="memory" type="number" min="16" step="1" required>
<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>
<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>
<label class="container-specific none" for="password">Password</label>
<input class="w3-input w3-border container-specific none" name="password" id="password" type="password" autocomplete="new-password" required disabled>
<label class="container-specific none" for="confirm-password">Confirm Password</label>
<input class="w3-input w3-border container-specific none" name="confirm-password" id="confirm-password" type="password" autocomplete="new-password" required disabled>
</form>
</div>
<div id="controls" class="w3-center w3-container">
<button id="cancel" type="submit" value="cancel" form="form" class="w3-button w3-margin" formnovalidate>CANCEL</button>
<button id="confirm" type="submit" value="confirm" form="form" class="w3-button w3-margin" >CONFIRM</button>
</div>
</dialog>
</template>
</div>
<div id="instance-table">
<div id="instance-table-header">
<p>ID</p>
<p>Name</p>
<p class="hide-tiny">Type</p>
<p class="hide-small">Status</p>
<p class="hide-medium">Host Name</p>
<p class="hide-medium">Host Status</p>
<p>Actions</p>
</div>
<hr style="grid-column: 1 / -1; padding: 0; margin: 0;">
<div id="instance-container">
{{range .instances}}
{{template "instance-card" .}}
{{end}}
</div>
</div>
</div>
</section>
</main>
</body>
</html>
-28
View File
@@ -1,28 +0,0 @@
<!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>
{{template "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>
-84
View File
@@ -1,84 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
{{template "head" .}}
<script src="scripts/settings.js" type="module"></script>
<link rel="modulepreload" href="scripts/utils.js">
<style>
label {
display: flex;
height: fit-content;
width: 100%;
align-items: center;
justify-content: left;
column-gap: 0.5em;
}
label + p {
margin-top: 0.5em;
margin-bottom: 1em;
}
p:last-child {
margin-bottom: 0;
}
#save.disabled {
display: none;
}
#save.enabled {
display: unset;
}
</style>
</head>
<body>
{{template "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>
<label><input class="w3-radio" type="radio" id="sync-never" name="sync-scheme" value="never" required>Never Sync</label>
<p>App will never automatically sync. Reload the page to sync the latest cluster state.</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 disabled" id="save" type="submit">SAVE</button>
</div>
</form>
</main>
</body>
</html>

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