Compare commits

...

631 Commits

Author SHA1 Message Date
Julien Fontanet
155debc864 4.13.0 2016-02-04 19:51:07 +01:00
Julien Fontanet
a5975ac38b Merge branch 'next-release' into stable 2016-02-04 19:50:39 +01:00
Olivier Lambert
204f1cfd6b Merge pull request #674 from vatesfr/pierre-vm-view-ui-enhancements
Select RAM units with dropdown menus
2016-02-04 09:52:50 +01:00
Pierre
2d22e043a0 Dropdown menus to select byte units (Fix #666)
New-VM view: RAM & VDIs sizes
VM view: RAM & VDIs sizes
SR view: VDIs sizes
2016-02-03 16:18:05 +01:00
Olivier Lambert
c26cacaf4e Merge pull request #687 from vatesfr/xo-acl-resolver
Use xo-acl-resolver.
2016-02-03 14:26:40 +01:00
Olivier Lambert
f0048544e2 Merge pull request #681 from vatesfr/olivierlambert-health
WIP: initial work on new health view
2016-02-03 14:16:45 +01:00
Olivier Lambert
cf227dbfa2 Merge pull request #695 from vatesfr/marsaudf-fix-sr-form
fix SR form
2016-02-03 14:00:38 +01:00
Olivier Lambert
a5f8bdbe61 fix issue #693 2016-02-03 13:26:07 +01:00
Olivier Lambert
e442553c6f Fix issue #691 2016-02-03 13:21:17 +01:00
Olivier Lambert
7134acfcd6 Fix issue #690 2016-02-03 13:18:21 +01:00
Olivier Lambert
82439f444e Fix issue #688 2016-02-03 13:03:58 +01:00
Fabrice Marsaud
1a17908488 fix SR form 2016-02-03 12:12:15 +01:00
Julien Fontanet
9af30e99f8 Use xo-acl-resolver. 2016-02-03 11:53:54 +01:00
Fabrice Marsaud
6f942c3417 orphans in sr view 2016-02-03 11:07:07 +01:00
Fabrice Marsaud
57083c90cd Fixes 2016-02-03 10:48:21 +01:00
Fabrice Marsaud
e28bcdd978 Correct rule for orphan snapshots 2016-02-03 10:28:49 +01:00
Olivier Lambert
0b4a5ab2eb working filter 2016-02-02 23:31:00 +01:00
Fabrice Marsaud
034704a330 No warnings for iso SRs 2016-02-02 17:59:59 +01:00
Fabrice Marsaud
5c60eaf6ab SR warnings 2016-02-02 17:45:49 +01:00
Fabrice Marsaud
f5709eac2c orphan snapshots panels 2016-02-02 16:52:57 +01:00
Fabrice Marsaud
5a5e714aca Fixed cpuWeight default choice 2016-02-02 09:43:18 +01:00
Olivier Lambert
747d48e4d9 adding missing files 2016-02-01 18:30:02 +01:00
Olivier Lambert
07a0200f30 WIP: initial work on new health view 2016-02-01 18:28:09 +01:00
Fabrice Marsaud
1c5313f2d9 Fixed cpuWeight type 2016-02-01 17:51:10 +01:00
Olivier Lambert
05e08719fb Merge pull request #678 from vatesfr/olivierlambert-srfixes
Use only physical usage for SRs
2016-02-01 17:31:10 +01:00
Olivier Lambert
ca0e616f88 Only use physical utilization for SR 2016-02-01 17:26:08 +01:00
Fabrice Marsaud
a8d20caba4 CPU Weight can back to default 2016-02-01 16:29:53 +01:00
Olivier Lambert
0d4bbb0a48 Merge pull request #677 from vatesfr/marsaudf-cpu-weigth#633
Edit VM CPU Weight
2016-02-01 16:10:36 +01:00
Fabrice Marsaud
b9cc219530 Handle VM CPU Weight 2016-02-01 15:54:52 +01:00
Olivier Lambert
e204ab5871 Merge pull request #650 from vatesfr/pierre-vm-migration-details
Custom VM migration in VM view
2016-01-28 17:02:35 +01:00
Fabrice Marsaud
16d0c05b4b Fiw attempt on console canvas 2016-01-28 15:33:48 +01:00
Fabrice Marsaud
6f8329d191 VDI multi delete 2016-01-28 15:09:36 +01:00
Olivier Lambert
d751463b26 Merge pull request #660 from vatesfr/abhamonr-avoid-metadata-imp-exp-delta-backups
New delta backup format used. (without 'xva' files) (fix #651)
2016-01-28 11:35:11 +01:00
Olivier Lambert
d3b66eff59 Merge pull request #671 from vatesfr/marsaudf-clear-logs#661
Delete job logs
2016-01-28 11:01:15 +01:00
Olivier Lambert
4257d0332a Merge pull request #672 from vatesfr/marsaudf-console-links#668
Differentiate VM and VM-controller console
2016-01-28 10:02:40 +01:00
Fabrice Marsaud
b80442c061 Fix to remove smb remotes for delta-backups 2016-01-28 09:22:22 +01:00
Fabrice Marsaud
3a0f6820ad Differentiate VM and VM-controller console 2016-01-28 09:02:58 +01:00
Fabrice Marsaud
1bc92f5363 Delete job logs 2016-01-28 08:50:50 +01:00
Olivier Lambert
818ddcf01e Merge pull request #663 from vatesfr/marsaudf-angular-crash#662
Secure VM object concurrent suppression
2016-01-28 08:18:43 +01:00
Olivier Lambert
618ba361c7 Merge pull request #568 from vatesfr/marsaudf-smb-mounts#338
SMB remotes
2016-01-28 08:18:24 +01:00
wescoeur
599160a325 New delta backup format used. (without 'xva' files) (fix #651) 2016-01-27 13:37:54 +01:00
Pierre
35fba6f4ed Custom VM migration from VM view (See #567)
- Migration on the same pool :
	- if the VM's VDIs are on the pool's SRs : standard migration
	- if the VM's VDIs are on local SRs : choose migration network and target SRs
- Migration on another pool : choose migration network, target SRs and target VIFs
2016-01-27 11:36:38 +01:00
Fabrice Marsaud
a14aad75fd external remote url module 2016-01-27 10:12:16 +01:00
Fabrice Marsaud
3513e85b0b remote url composing fix 2016-01-27 10:12:16 +01:00
Fabrice Marsaud
66c0390fc7 No smb remotes for delta backups 2016-01-27 10:12:16 +01:00
Fabrice Marsaud
a6549ccb08 SMB remotes 2016-01-27 10:12:16 +01:00
Julien Fontanet
15d2878014 Merge pull request #669 from Danp2/Danp2-patch-1
Fix spelling of "immediately"
2016-01-27 09:10:44 +01:00
Danp2
d271be8723 Update view.jade 2016-01-26 19:32:09 -06:00
Danp2
6f9d2d99dd Update view.jade 2016-01-26 19:31:43 -06:00
Danp2
5d62664ee3 Update view.jade 2016-01-26 19:30:45 -06:00
Danp2
7124d9f2f8 Update view.jade 2016-01-26 19:26:53 -06:00
Fabrice Marsaud
0459744771 Fixes disk save handling 2016-01-25 17:40:47 +01:00
Fabrice Marsaud
417544b781 Secure VM object concurrent suppression 2016-01-25 17:40:35 +01:00
Olivier Lambert
f9028cb366 Change the word Terminated by Finished for backups 2016-01-22 18:22:50 +01:00
Olivier Lambert
9a264719a9 Avoid broken Angular views. Fix #662 2016-01-21 15:11:42 +01:00
Olivier Lambert
96c213dcc4 Typo about the year in the changelog 2016-01-18 12:56:29 +01:00
Julien Fontanet
dec1a8e204 4.12.0 2016-01-18 10:45:50 +01:00
Olivier Lambert
a17fd697e2 update the CHANGELOG 2016-01-17 16:58:31 +01:00
Fabrice Marsaud
a6ab66e799 Fix #654 2016-01-17 16:12:11 +01:00
Fabrice Marsaud
17095ec3c6 Fix #652 2016-01-17 15:57:58 +01:00
Olivier Lambert
82687147b8 changelog for release 2016-01-17 10:24:28 +01:00
Olivier Lambert
ba76422c1f Merge pull request #648 from vatesfr/abhamonr-continuous-replication-view
Continuous replication view.
2016-01-16 20:04:05 +01:00
wescoeur
083b3c4ece Continuous replication view. (fix #582) 2016-01-15 13:15:43 +01:00
Olivier Lambert
5ecfdf38a8 Merge pull request #600 from vatesfr/abhamonr-button-bootable-disk
Use checkbox to disable/enable bootable disk property. (fix #583)
2016-01-14 16:26:54 +01:00
wescoeur
dd1acf3c2a Use checkbox to disable/enable bootable disk property. (fix #583) 2016-01-14 16:22:45 +01:00
Olivier Lambert
76e9c2d196 Fix issue #643 2016-01-13 18:54:19 +01:00
Julien Fontanet
15f046959d Fix lodash.trim to 3.0.1 (see lodash/lodash#1769). 2016-01-13 16:23:20 +01:00
Olivier Lambert
bf3ba04624 Merge pull request #620 from vatesfr/abhamonr-disable-vm-start-during-delta-import
Disable vm start during delta import and explicit notification. (fix #613)
2016-01-13 11:53:54 +01:00
Olivier Lambert
d997894d9a Merge pull request #614 from vatesfr/pierre-create-multiple-vms
Create multiple VMs at once
2016-01-13 11:33:18 +01:00
wescoeur
c1059db6e5 Disable vm start during delta import and explicit notification. (fix #613) 2016-01-13 11:04:24 +01:00
Pierre
8ad29a2836 Creation of multiple VMs at once
- Panel to enable the creation of multiple VMs at once
- Main name is no longer required when creating multiple VMs
- Number of VMs is checked before creating VMs names input fields
- Redirection to tree view instead of VM view when creating multiple VMs
- Number of new VMs in summary
2016-01-13 10:52:48 +01:00
Olivier Lambert
93a454b835 fix id propagation problem 2016-01-13 10:33:06 +01:00
Olivier Lambert
da899386ec Merge pull request #640 from vatesfr/marsaudf-plugin-view-fix#637
Plugin reload after changes
2016-01-07 11:53:16 +01:00
Fabrice Marsaud
05d22903ea Plugin reload after changes 2016-01-07 11:45:15 +01:00
Olivier Lambert
33945520f1 Fix issue #639 2016-01-06 10:22:52 +01:00
Olivier Lambert
40284809cf choose to boot VM after creation. Fix #635 2016-01-04 16:41:16 +01:00
Olivier Lambert
efc18aaaec ensure CloudConfig drive is created before going on the freshly created VM view 2016-01-04 16:37:04 +01:00
Olivier Lambert
348441b046 improve hostname regex for CloudInit 2016-01-04 12:35:47 +01:00
Olivier Lambert
66601b2e7c remove space in hostname for cloudconfig. Fix #634 2016-01-04 12:33:52 +01:00
Olivier Lambert
724c5e4b73 VM creation auto name, description & select existing install repo 2015-12-31 10:50:15 +01:00
Olivier Lambert
7eff29bc65 remove useless logs 2015-12-31 10:25:49 +01:00
Olivier Lambert
ca002003c2 fix VIFs issues in VM creation 2015-12-31 10:25:23 +01:00
Olivier Lambert
f0675f1f3c Merge pull request #618 from vatesfr/pierre-delete-running-vm
Delete a running or suspended VM (See #616)
2015-12-31 08:56:19 +01:00
Olivier Lambert
976186c525 Merge pull request #631 from vatesfr/olivierlambert-existing-vifs
manage existing VDIs, fix #630
2015-12-30 22:16:00 +01:00
Olivier Lambert
89d5777e52 allow existing VIFs edition during VM creation. Fix #630 2015-12-30 22:12:15 +01:00
Olivier Lambert
8dbb69809c Merge pull request #629 from vatesfr/olivierlambert-custom-templates
Custom templates, fix #627 and #628
2015-12-30 18:18:09 +01:00
Olivier Lambert
7348bd5d15 support templates with existing install_repository, as requested for issue #625 2015-12-30 16:08:39 +01:00
Olivier Lambert
9a46a466f7 properlly manage PV args (related to #625) 2015-12-30 15:27:41 +01:00
Fabrice Marsaud
fafc5c8553 Deltabackup display fix 2015-12-30 13:46:12 +01:00
Julien Fontanet
4ffdfaa506 Merge pull request #619 from vatesfr/pierre-fix-suspend-vm-tree-view
Fixed `suspendVM` in tree view.
2015-12-22 15:56:50 +01:00
Pierre
e3989840ee Fixed suspendVM in tree view. 2015-12-22 15:51:59 +01:00
Pierre
b3e6f531a1 Delete not halted VMs (See #616) 2015-12-22 15:46:19 +01:00
Julien Fontanet
4f6ee34592 4.11.0 2015-12-22 13:05:56 +01:00
Olivier Lambert
3ae58a323e update changelog 2015-12-22 12:36:12 +01:00
Fabrice Marsaud
26b958c270 SR host names displayed when necessary 2015-12-22 12:00:15 +01:00
Olivier Lambert
12a4af5900 fix a broken link 2015-12-22 10:39:18 +01:00
Julien Fontanet
69479d538c Merge pull request #611 from vatesfr/abhamonr-incremental-backups-integration
Delta Backup is now known by xo-web.
2015-12-21 18:52:26 +01:00
wescoeur
829397dd5a Delta Backup is now known by xo-web. 2015-12-21 18:00:31 +01:00
Olivier Lambert
2bc89026db Merge pull request #612 from vatesfr/marsaudf-sr-list#601
All host SRs from the pool are shown for Halted VMs disk edition
2015-12-21 16:03:22 +01:00
Fabrice Marsaud
ebbc44d181 All host SRs from the pool are shwon for Halted VMs disk edition 2015-12-21 15:58:49 +01:00
Olivier Lambert
2228a1e36b update changelog 2015-12-21 12:37:33 +01:00
Olivier Lambert
a8cbf3e8ff Merge pull request #602 from vatesfr/pierre-plugin-config-detailed-errors
Plugin config: feedback on form filling errors
2015-12-21 12:02:57 +01:00
Olivier Lambert
fa32e3d734 Merge pull request #598 from vatesfr/marsaudf-disk-size-edition#587
Marsaudf disk size edition#587
2015-12-21 12:02:13 +01:00
Pierre
0d17148ff0 Minor fixes 2015-12-21 11:30:07 +01:00
Pierre
aa38411cf7 Checking titles for each config group. Displaying errors only for the concerned plugin 2015-12-21 11:11:04 +01:00
Olivier Lambert
4913c8699d Merge pull request #610 from vatesfr/cache-missingPatches-dashboard
Cache # of missing patches in dashboard (fix #609).
2015-12-21 10:42:36 +01:00
Julien Fontanet
1035a11487 Cache # of missing patches in dashboard (fix #609). 2015-12-21 10:38:31 +01:00
Olivier Lambert
15c2efe706 Merge pull request #607 from vatesfr/fix-removeListener-dashboard
Stop computing charts data when no longer on dashboard (fix #604).
2015-12-21 10:09:19 +01:00
Julien Fontanet
d7fd71bb62 Same fix for dataviz view. 2015-12-21 10:05:51 +01:00
Julien Fontanet
b11ee993fa Stop computing charts data when no longer on dashboard (fix #604). 2015-12-21 10:05:46 +01:00
Olivier Lambert
614aa7873c update changelog 2015-12-20 14:21:30 +01:00
Pierre
1adf31fe15 [WIP] Display field title when possible and multiple fixes. 2015-12-18 17:24:19 +01:00
Julien Fontanet
824ffd7b5b Merge pull request #603 from vatesfr/abhamonr-fix-remote-importVm
The vm import call use a sr instead of a host.
2015-12-18 17:10:11 +01:00
wescoeur
c31c6fdebb The vm import call use a sr instead of a host. 2015-12-18 17:03:59 +01:00
Pierre
83f3276429 Plugin config: feedback on form filling errors 2015-12-18 15:25:24 +01:00
Julien Fontanet
d21f68ce54 Merge pull request #586 from vatesfr/pierre-read-only-connection
Added read-only checkboxes in the interface
2015-12-18 12:03:44 +01:00
Pierre
18b1e1b133 Connection to a server in read-only mode. (Fix #439)
Checkboxes in Settings view to connect to a Xen Server in read-only mode and then to toggle mode while connected to the server.
2015-12-18 11:48:29 +01:00
Fabrice Marsaud
0edaa40052 Confirm modals before disk resizing 2015-12-18 10:18:02 +01:00
Fabrice Marsaud
627077c8f3 Better code for Human readable size input 2015-12-18 10:18:02 +01:00
Fabrice Marsaud
a897b1798d bytesToSize only alters numbers 2015-12-18 10:18:02 +01:00
Olivier Lambert
50e39993bf Merge pull request #599 from vatesfr/marsaudf-newmv-disk-size
HR size for new VM new disks
2015-12-17 10:59:50 +01:00
Fabrice Marsaud
5e397dd01e HR size for new VM new disks 2015-12-17 10:53:32 +01:00
Olivier Lambert
f57ff5d5e0 Merge pull request #593 from vatesfr/pierre-no-orderby-when-focus
Settings view: the servers list should not re-order while a field is being edited. Fix #594
2015-12-16 18:26:55 +01:00
Olivier Lambert
5c3e40917c Merge pull request #577 from vatesfr/olivierlambert-configdrive
Allow cloud drive usage for disk templates VMs
2015-12-16 18:20:13 +01:00
Olivier Lambert
90a2dc4581 Merge pull request #597 from vatesfr/marsaudf-disk-size-edition#587
Marsaudf disk size edition. Fix #587
2015-12-16 17:37:15 +01:00
Olivier Lambert
b64243fdd6 add parent :o 2015-12-16 17:12:51 +01:00
Fabrice Marsaud
42db87d305 resize disks in SR View 2015-12-16 16:43:39 +01:00
Fabrice Marsaud
e7ab1b589a resize disks in VM view 2015-12-16 16:17:15 +01:00
Fabrice Marsaud
e9979c9887 Human readable sizes for editing template disks on VM creation 2015-12-16 16:07:08 +01:00
Olivier Lambert
3bb9bb56f0 better placeholder 2015-12-16 16:07:08 +01:00
Olivier Lambert
5a99474c55 add stuff 2015-12-16 16:07:08 +01:00
Olivier Lambert
182ee6c25f add stuff 2015-12-16 16:07:08 +01:00
Fabrice Marsaud
4d3f0a06db Modfified template disk properties are stored for future update 2015-12-16 16:07:08 +01:00
Fabrice Marsaud
0e182c519b Config drive button looks better 2015-12-16 16:07:08 +01:00
Fabrice Marsaud
b1ee30ce7d cloud config message 2015-12-16 16:07:08 +01:00
Fabrice Marsaud
93ba764e23 Config drive can be toggled, modified template disks data are isolated in the controller 2015-12-16 16:07:08 +01:00
Olivier Lambert
433e17bb81 more comments 2015-12-16 16:07:08 +01:00
Olivier Lambert
61c09083ad more modifications 2015-12-16 16:07:08 +01:00
Olivier Lambert
018377e724 reorder the cloud config at the end of the vm creation process 2015-12-16 16:07:08 +01:00
Olivier Lambert
b76f9513ba various fixes 2015-12-16 16:07:08 +01:00
Olivier Lambert
40ebb7ba75 add a removed by error stuff 2015-12-16 16:07:08 +01:00
Olivier Lambert
a9e52e8954 remove preliminar work in existing VM view 2015-12-16 16:07:08 +01:00
Olivier Lambert
3c8876cac7 Allow cloud drive usage for disk templates VMs 2015-12-16 16:07:08 +01:00
Olivier Lambert
b7e005f9c7 fix an indentation problem 2015-12-16 16:06:37 +01:00
Olivier Lambert
e6fe0a19fa Merge pull request #596 from vatesfr/olivierlambert-clean-size-to-byte
clean size to byte filter
2015-12-16 16:04:07 +01:00
Olivier Lambert
fba11b6a44 fix a useless filter 2015-12-16 16:01:10 +01:00
Olivier Lambert
c270e7f5dd clean size to byte filter 2015-12-16 15:41:42 +01:00
Pierre
9ee00d345e Settings: the servers list should not re-order while a field is being edited. (Fix #594)
The angular `orderBy` is triggered when the server list is triggered ie every 3 seconds when every text fields are unfocused.
2015-12-16 13:47:13 +01:00
Julien Fontanet
0379fbc4eb Merge pull request #590 from vatesfr/pierre-no-refresh-when-focus
Servers should not refresh while a field is being edited
2015-12-16 13:44:43 +01:00
Olivier Lambert
9748a3ae91 display correct size in interface (binary scale). fix #592 2015-12-16 11:23:37 +01:00
Pierre
1881944748 Servers infos should not refresh while a field is being edited 2015-12-15 16:56:52 +01:00
Olivier Lambert
3721fa194c remove metadata export. Fix #580 2015-12-11 16:38:03 +01:00
Olivier Lambert
8c3fcad20b Merge pull request #574 from vatesfr/marsaudf-prevent-add-host#466
Check IP of a new server to avoid double connection. Fix #466
2015-12-10 14:02:41 +01:00
Fabrice Marsaud
decf373d0b Check IP of a new server to avoid double connection 2015-12-10 12:25:04 +01:00
Olivier Lambert
ff1d50f993 Merge pull request #573 from vatesfr/olivierlambert-set-default-sr
set default SR. Fix #572
2015-12-10 12:14:14 +01:00
Olivier Lambert
ef34204b59 set default SR. Fix #572 2015-12-10 12:10:47 +01:00
Olivier Lambert
270b636d80 Merge pull request #569 from vatesfr/pierre-users-cannot-add-tags-on-disks
Non-admin users don't see the '+' button to add a tag (Fix Issue #516)
2015-12-09 19:22:48 +01:00
Olivier Lambert
ac01da2ae9 Merge pull request #570 from vatesfr/marsaudf-run-job#562
Buttons to run jobs for one shot (backup or job manager). Fix #570
2015-12-09 19:17:13 +01:00
Fabrice Marsaud
0136310c54 Buttons to run jobs for one shot (backup or job manager) 2015-12-09 16:57:50 +01:00
Pierre
ecf4cf852e Non-admin users don't see the '+' button to add a tag (Issue #516) 2015-12-09 16:51:12 +01:00
Olivier Lambert
c66384adfb Merge pull request #566 from vatesfr/olivierlambert-recoveryStart
Generic recovery start (both PV and HVM compatible). Fix #564
2015-12-07 18:03:33 +01:00
Olivier Lambert
98bdda629d Order ISOs by their name. Fix #565 2015-12-07 17:28:48 +01:00
Olivier Lambert
a8286f9cba minor fix 2015-12-07 17:22:40 +01:00
Olivier Lambert
fa3db4fcf6 Generic recovery start (both PV and HVM) 2015-12-07 15:43:46 +01:00
Olivier Lambert
ddac0cfee1 Display failures on VM boot order modification. Fix #560 2015-12-07 13:48:32 +01:00
Olivier Lambert
9368673459 Display PV args for PV guest even if they are empty. Fix #557 2015-12-07 13:37:25 +01:00
Olivier Lambert
43dc999ab5 display boot order only for HVM guests. Fix #558 2015-12-07 13:32:21 +01:00
Olivier Lambert
3b7333e866 remove most of the left margin 2015-12-04 14:11:38 +01:00
Olivier Lambert
bc0ddbaf16 Merge pull request #554 from vatesfr/abhamonr-plugin-config-avoid-reset-form
Avoid plugin config form reset. Fix #529
2015-12-04 12:38:23 +01:00
Olivier Lambert
45f0ae7e1c Merge pull request #425 from vatesfr/pierre-search-bar
Improved search bar
2015-12-04 12:35:35 +01:00
Pierre
a521c4ae01 Clicking on the search button will always bring to the list view. Lag fix when the text field is emptied. Transparent background for filter menus. 2015-12-04 12:00:55 +01:00
Pierre
5b8238adeb 2 filter menus : 'Types' and 'States'. FontAwesome checkboxes. Bug fix. 2015-12-04 10:48:32 +01:00
Pierre
ec330474fa The 'Filters' menu is below the search bar and appears only in the list view. *disconnected filter shows hosts and SRs which have at least 1 PBD not attached. 2015-12-04 10:48:32 +01:00
Pierre
ece28904a8 All the checkboxes are unchecked when the search field is empty 2015-12-04 10:48:32 +01:00
Pierre
4f1c495afb Added *disconnected option. Added a 'Filters' dropdown menu in the search bar to add/remove options with a Github like behaviour 2015-12-04 10:48:32 +01:00
Pierre
5fdd27b7e6 Added option key-word *halted 2015-12-04 10:48:32 +01:00
Pierre
91f449af9a Search improvements
1) select several types of objects (eg: *vm *host)
2) combine types and states (eg: *vm *running)
3) negation is considered as an option
2015-12-04 10:48:32 +01:00
Pierre
efc0a0dfe3 Added '*running' option to show only running entities (for VMs and hosts). This option can be banned (statusrunning) to show only not running entities 2015-12-04 10:48:32 +01:00
Pierre
fee47baa66 Added key-words to ban some objects from search list (host, vm, sr, pool) + Back to tree view when search field is empty 2015-12-04 10:48:32 +01:00
wescoeur
0ad7bfc7e7 Avoid plugin config form reset. 2015-12-03 15:11:44 +01:00
Julien Fontanet
bd64143ae1 Merge pull request #551 from vatesfr/abhamonr-plugin-root-integer-properties
Root integer properties can be edited in plugins configuration form. …
2015-12-03 15:09:52 +01:00
wescoeur
ec982ba9a3 Root integer properties can be edited in plugins configuration form. fix #550 2015-12-03 15:03:47 +01:00
Julien Fontanet
6280f6ff98 Merge pull request #542 from vatesfr/abhamonr-purge-plugin-config
The plugins configurations can be cleaned.
2015-12-03 14:37:52 +01:00
Olivier Lambert
35d20390a9 hide non auhorized containers for VMs (host or pool). Fix #545 2015-12-03 14:23:32 +01:00
wescoeur
c487c5042f The plugins configurations can be cleaned. fix #539 2015-12-02 16:14:36 +01:00
Olivier Lambert
aaf7927aa2 Cloud config default SR. (Fix #548) 2015-11-30 18:45:29 +01:00
Julien Fontanet
3c677f3d21 Merge pull request #544 from vatesfr/abhamonr-plugin-config-boolean-default-value
Plugin config boolean properties have a default false value.
2015-11-27 17:01:22 +01:00
wescoeur
94eb76b3a6 Plugin config boolean properties have a default false value. fix #543 2015-11-27 16:49:46 +01:00
Julien Fontanet
a921cb2d0d 4.10.0 2015-11-27 14:35:50 +01:00
Olivier Lambert
f3aaa363d8 Merge pull request #541 from vatesfr/marsaudf-UI-fix
Minor UI fix
2015-11-27 13:39:20 +01:00
Fabrice Marsaud
45a79e1920 Minor UI fix 2015-11-27 13:35:27 +01:00
Olivier Lambert
6fd9b2a453 Merge pull request #493 from vatesfr/marsaudf-task-manager
Generic task manager
2015-11-27 12:03:01 +01:00
Olivier Lambert
01d8e89a71 add changelog 2015-11-27 11:58:00 +01:00
Fabrice Marsaud
c89fa63910 Minor UI fix 2015-11-27 11:25:23 +01:00
Fabrice Marsaud
9fc5c49dbf UI enhancements 2015-11-27 11:25:23 +01:00
Fabrice Marsaud
7dfc269df9 Enhanced UI inputs for XO object management 2015-11-27 11:25:18 +01:00
Fabrice Marsaud
76d0b397db Instant one shot for generic jobs 2015-11-27 11:24:25 +01:00
Fabrice Marsaud
5413f887af Bare generic job creation and scheduling 2015-11-27 11:24:13 +01:00
Julien Fontanet
b3d0c61f0e Merge pull request #540 from vatesfr/abhamonr-plugin-input-type-number
Plugin 'number' property use input number in config form (fix #538)
2015-11-27 10:49:32 +01:00
wescoeur
4ce0441d68 Plugin 'number' property use input number in config form 2015-11-27 10:42:45 +01:00
Julien Fontanet
72be34e18d Move clipboard to dev deps. 2015-11-27 10:04:43 +01:00
Julien Fontanet
d2961b7650 Merge pull request #537 from vatesfr/abhamonr-plugins-supports-numbers
Plugin config supports integer properties (fix #531).
2015-11-27 09:54:46 +01:00
wescoeur
fdca1bbf72 Plugin config supports integer properties. 2015-11-27 09:43:33 +01:00
Julien Fontanet
ab7a2f9dee Merge pull request #536 from vatesfr/abhamonr-plugin-boolean-checkbox
Plugin boolean properties use checkboxes (fix #528).
2015-11-27 09:42:06 +01:00
wescoeur
7b72857a3b Plugin boolean properties use checkboxes 2015-11-26 22:44:24 +01:00
Olivier Lambert
4787146658 Merge pull request #533 from vatesfr/marsaudf-backup-ui
better backup log display
2015-11-26 16:30:18 +01:00
Fabrice Marsaud
430f9356c3 Minor button fix 2015-11-26 16:27:40 +01:00
Fabrice Marsaud
70a3b3518f Better schedule state UI in overview 2015-11-26 16:25:00 +01:00
Fabrice Marsaud
c0944c17e0 better backup log display 2015-11-26 16:07:47 +01:00
Julien Fontanet
da1b2a91e7 Merge pull request #526 from vatesfr/pierre-console-keyboard-unfocus
Console has keyboard and mouse focus only when mouse is hovering
2015-11-26 15:59:48 +01:00
Pierre
aa27492713 Console catches keyboard and mouse inputs only when mouse is hovering.
Also, when the mouse enters the VM screen, the current active element is unfocused.
2015-11-26 15:44:01 +01:00
Julien Fontanet
afe589dec3 Merge pull request #527 from vatesfr/abhamonr-plugin-title-property
Support title property in plugin configuration schema
2015-11-26 15:25:25 +01:00
wescoeur
978d140c8f Support title property in plugin configuration schema 2015-11-26 14:31:32 +01:00
Olivier Lambert
2ce213b62c Merge pull request #525 from vatesfr/pierre-clipboard-management-through-console
Clipboard management through console
2015-11-26 11:35:57 +01:00
Pierre
7748266078 Clipboard support in console.
- From VM to client :
	1) Copy text in VM
	2) The text field (above the console) updates automatically with the VM's clipboard content
	3) Click on the 'Copy' button to get the text in the local clipboard
- From client to VM :
	1) Write text in the text field
	2) The VM's clipboard updates automatically with the new content
	3) Paste text anywhere in the VM
2015-11-26 11:23:40 +01:00
Olivier Lambert
83783d07a1 hide action panel for host or VM if only viewer 2015-11-25 14:38:47 +01:00
Olivier Lambert
49a1f2c7c5 Merge pull request #517 from vatesfr/marsaudf-disable-host-buttons#474
Disable host buttons relying on ACLs
2015-11-25 14:31:49 +01:00
Olivier Lambert
ddfc0151fc Merge pull request #515 from vatesfr/marsaudf-backup-display#512
Tag display for backup schedules in overview #512
2015-11-25 14:01:33 +01:00
Fabrice Marsaud
81c508e13c Host view OK 2015-11-25 10:25:10 +01:00
Fabrice Marsaud
7195cfc3cf First step 2015-11-24 17:31:26 +01:00
Fabrice Marsaud
93fe5e2cf7 PR feedback 2015-11-24 17:31:00 +01:00
Fabrice Marsaud
a2bf795d12 Tag display for backup schedules in overview 2015-11-24 17:31:00 +01:00
Julien Fontanet
c8d78f39e0 Upgrade npm to latest on Travis. 2015-11-24 17:17:58 +01:00
Fabrice Marsaud
d9ab8a1c8b Fix #508 2015-11-23 15:24:23 +01:00
Olivier Lambert
5125ad4889 Merge pull request #506 from vatesfr/pierre-emergency-host-shutdown
Emergency button in host view is now calling the server function
2015-11-20 17:32:17 +01:00
Olivier Lambert
951e85b04b Merge pull request #507 from vatesfr/olivierlambert-cloudconfig
CoreOS cloud config management during VM creation
2015-11-20 17:32:10 +01:00
Olivier Lambert
711d922695 CoreOS cloud config during VM creation 2015-11-20 17:10:10 +01:00
Pierre
3692ffcde7 Rename function : emergencyHostShutdown -> emergencyShutdownHost 2015-11-20 10:19:06 +01:00
Pierre
b049420c59 Emergency button in host view is now calling the server function (suspends all the VMs running on the host and then shuts the host down) 2015-11-19 17:07:10 +01:00
Olivier Lambert
241103c369 Merge pull request #501 from vatesfr/pierre-install-patches-on-all-pools
Created panel in dashboard
2015-11-19 14:49:59 +01:00
Pierre
2128367113 Update panel in dashboard. 2015-11-19 12:29:48 +01:00
Julien Fontanet
f555c8190d Revert "nvm (on Travis) does not use stable correctly."
This reverts commit f85dc3b7e7.
2015-11-18 17:32:54 +01:00
Pierre
d5df633def Removed some useless CSS 2015-11-18 17:13:54 +01:00
Olivier Lambert
fe7dc859e3 Merge pull request #499 from vatesfr/pierre-suspend-all-vms-and-shutdown-host
emergency shutdown feature in host view (suspend all VMs then shutdown)
2015-11-18 17:04:04 +01:00
Pierre
569c5046c6 Added an emergency button in Action panel (host view) : suspends all the VMs and shuts the host down. 2015-11-18 16:56:56 +01:00
Julien Fontanet
e0210ae2d8 Stable is the new stable branch. 2015-11-18 16:30:30 +01:00
Julien Fontanet
f85dc3b7e7 nvm (on Travis) does not use stable correctly. 2015-11-18 16:10:07 +01:00
Olivier Lambert
92d4363120 tree view improvements and fix 2015-11-17 15:18:57 +01:00
Olivier Lambert
6c69220de2 add start in recovery mode for HVM guests and support new API call setBootOrder() instead of bootOrder() 2015-11-17 14:59:26 +01:00
Julien Fontanet
3a1229b072 Only test on stable as there is just linting for now. 2015-11-16 16:59:55 +01:00
Olivier Lambert
45538c9f62 add quiesce display in VM view 2015-11-16 13:28:51 +01:00
Julien Fontanet
0c173fde53 Explicit .scss for angular-notify-toaster (fix #488). 2015-11-13 11:53:32 +01:00
Julien Fontanet
77db2bbfec 4.9.0 2015-11-13 11:31:19 +01:00
Julien Fontanet
2987185a9d Fix coding style. 2015-11-13 11:30:47 +01:00
Olivier Lambert
7a7baf7175 change the release date 2015-11-12 16:54:30 +01:00
Olivier Lambert
5645cc0af2 removing an entry in the changelog 2015-11-12 12:35:46 +01:00
Olivier Lambert
63a6756fed add changelog 2015-11-12 11:37:31 +01:00
Olivier Lambert
9f408c98a6 Merge pull request #480 from vatesfr/marsaudf-vm-copy-sr
Copy VM
2015-11-12 11:16:25 +01:00
Olivier Lambert
26d6998d82 Merge pull request #485 from vatesfr/abhamonr-perf-12-cpus
for cpus graph: draw only one line which is an average of all CPUs (i…
2015-11-12 11:06:08 +01:00
wescoeur
6bee44acb7 for cpus graph: draw only one line which is an average of all CPUs (if more than 12 cores) 2015-11-12 10:38:37 +01:00
Fabrice Marsaud
441992cf37 Copy VM, first delivery 2015-11-12 10:33:46 +01:00
Fabrice Marsaud
490c224ac3 Merge pull request #469 from vatesfr/marsaudf-disaster-recovery
Disaster Recovery
2015-11-12 10:31:39 +01:00
Fabrice Marsaud
f5c55048de DR UI enhancemnts 2015-11-12 10:19:49 +01:00
Fabrice Marsaud
8139e124c2 DR features fixes 2015-11-12 10:19:49 +01:00
Fabrice Marsaud
cba73a5139 Disaster recovery feature 2015-11-12 10:19:48 +01:00
Fabrice Marsaud
de0c9367e5 Merge pull request #483 from vatesfr/abhamonr-backup-logs
Abhamonr backup logs
2015-11-12 10:11:44 +01:00
Fabrice Marsaud
630060860c Backup log UI fix 2015-11-12 10:09:09 +01:00
Fabrice Marsaud
dccd11fb7b Backup logs UI 2015-11-12 10:09:09 +01:00
Olivier Lambert
b8a4b2cf16 add duration filter 2015-11-12 10:09:09 +01:00
wescoeur
e52f55bfba backup view (logs) in progress 2015-11-12 10:09:09 +01:00
wescoeur
1cb99e02a9 Logs of jobs in progress in backup/management view 2015-11-12 10:09:09 +01:00
Fabrice Marsaud
c9c5c35e56 Merge pull request #476 from vatesfr/abhamonr-backup-logs
Abhamonr backup logs
2015-11-12 10:06:34 +01:00
Olivier Lambert
bc7c9f9c01 fix inversion in vCPUs and CPUs in dashboard view 2015-11-11 22:10:52 +01:00
Fabrice Marsaud
e4bfc4cb8d Better chosen objects display in ACL UI 2015-11-11 17:44:19 +01:00
Fabrice Marsaud
d64995c4a1 Backup log UI fix 2015-11-10 17:08:59 +01:00
Fabrice Marsaud
2952ea7404 Backup logs UI 2015-11-10 17:08:59 +01:00
Olivier Lambert
f34c807a2c add duration filter 2015-11-10 17:08:59 +01:00
wescoeur
b1f9704055 backup view (logs) in progress 2015-11-10 17:08:59 +01:00
wescoeur
9382829ba5 Logs of jobs in progress in backup/management view 2015-11-10 17:08:59 +01:00
Fabrice Marsaud
373a6ea912 Merge pull request #479 from vatesfr/marsaudf-backup-fixes
Backup UI enhancements
2015-11-10 16:53:00 +01:00
Fabrice Marsaud
72eb4e7b3b Backup UI enhancements 2015-11-10 15:21:24 +01:00
Julien Fontanet
315c0870ed Fix VM exports when not directly under /. 2015-11-09 16:13:17 +01:00
Olivier Lambert
200fa621bf use the new xenTools method 2015-11-06 13:54:13 +01:00
Fabrice Marsaud
80348c1980 Patched a display delay for some stats 2015-11-06 13:47:01 +01:00
Fabrice Marsaud
856dd8403c Patched stat display bug 2015-11-06 13:39:23 +01:00
Olivier Lambert
0bb9acd4c1 Merge pull request #470 from vatesfr/marsaudf-backup-no-compression
Compression can be disabled for backups
2015-11-06 11:40:06 +01:00
Fabrice Marsaud
047a80917f Compression can be disabled for backups 2015-11-06 11:37:31 +01:00
Fabrice Marsaud
e6e8fe4763 Fixed backup overview links 2015-11-06 11:36:17 +01:00
Olivier Lambert
6cd212398e Merge pull request #473 from vatesfr/marsaudf-fix-convertToTemplate
Back to index after VM converts to template
2015-11-06 11:10:34 +01:00
Fabrice Marsaud
44ad6d4247 Back to index after VM converts to template 2015-11-06 11:07:42 +01:00
Olivier Lambert
7302782853 Merge pull request #465 from vatesfr/marsaudf-restore-backup
Restore backups
2015-11-05 15:47:04 +01:00
Fabrice Marsaud
7fa1aba6b8 Backup restore and global backup UI rework 2015-11-05 15:18:01 +01:00
Julien Fontanet
2fed4e3e8b Update deps. 2015-11-03 12:08:05 +01:00
Julien Fontanet
bd343c51a3 4.8.0 2015-10-29 10:43:17 +01:00
Olivier Lambert
8a05f06efa changelog update 2015-10-28 17:56:51 +01:00
Julien Fontanet
27b049eada Fix permissions checking for XO admins. 2015-10-28 16:43:50 +01:00
Julien Fontanet
2d1afb5291 Fixes some permissions checking. 2015-10-28 16:24:13 +01:00
Olivier Lambert
63c17a3abf Merge pull request #456 from vatesfr/abhamonr-avoid-use-of-vms-offline-on-dashboard
Avoid the selection of vms or hosts which are not in the running state
2015-10-28 16:14:57 +01:00
wescoeur
94f9bc5fca Avoid the selection of vms or hosts which are not the running state 2015-10-28 15:46:04 +01:00
Julien Fontanet
ab273430d2 Merge pull request #455 from vatesfr/julien-f-acl-inheritance
Adapt to new ACLs API (and implement inheritance).
2015-10-28 15:30:05 +01:00
Julien Fontanet
0b3dc315ad Adapt to new ACLs API (and implement inheritance). 2015-10-28 15:09:08 +01:00
Olivier Lambert
f26a2d2f13 remove old parameter for granularity 2015-10-28 14:59:57 +01:00
Olivier Lambert
8edf9bf508 Merge pull request #454 from vatesfr/abhamonr-intelligent-stats
Remove default value of granularity in refreshStats
2015-10-28 14:44:20 +01:00
Olivier Lambert
415381cebd fix a missing char in the Jade template 2015-10-28 14:41:29 +01:00
wescoeur
59accec1c0 remove default value of granularity in refreshStats 2015-10-28 14:35:55 +01:00
Olivier Lambert
dd2699fcc1 Merge pull request #453 from vatesfr/abhamonr-intelligent-stats
host, vm and dashboard/health pages use now the new stats format prov…
2015-10-28 13:53:14 +01:00
wescoeur
0986a5f985 host, vm and dashboard/health pages use now the new stats format provided by the server 2015-10-28 12:51:13 +01:00
Olivier Lambert
fa77229b72 Merge pull request #452 from vatesfr/proxy-auth-updater
Proxy auth for update panel
2015-10-28 11:52:31 +01:00
Olivier Lambert
78b5080c9a Merge pull request #440 from vatesfr/reg-renewal#424
Xoa registration can be renewed
2015-10-28 10:49:15 +01:00
Fabrice Marsaud
0641da786c Proxy auth for update panel 2015-10-27 16:05:31 +01:00
Olivier Lambert
3291f3bb3c initial CHANGELOG for 4.8 2015-10-27 14:30:41 +01:00
Olivier Lambert
a0cfef8bda Merge pull request #444 from vatesfr/pierre-install-all-patches-button
Added button to install all the missing patches on host at one go
2015-10-26 15:23:50 +01:00
Julien Fontanet
4d033f4a03 Direct links work again (fix #371). 2015-10-26 12:41:45 +01:00
Pierre
562820180c Added button to install all the missing patches on host at one go 2015-10-23 15:48:03 +02:00
Olivier Lambert
a29832207e Merge pull request #443 from vatesfr/pierre-fix-patches-display
In host : fixed uploaded patches properties (name, description, uuid)
2015-10-23 15:28:29 +02:00
Pierre
2afd549826 In host : fixed uploaded patches properties (name, description, uuid) 2015-10-23 15:21:50 +02:00
Olivier Lambert
8a71b2c6dd Merge pull request #430 from vatesfr/pierre-2-status-patches
Patches installed on a host appear as 'Applied' or 'Not applied' and …
2015-10-23 15:11:08 +02:00
Pierre
d633d2691d Patches installed on a host do not appear as missing 2015-10-23 14:43:44 +02:00
Fabrice Marsaud
f9b1608fd2 Xoa registration can be renewed 2015-10-21 12:34:13 +02:00
Julien Fontanet
4d8ed3f00e Use bundler collapser for production build. 2015-10-21 11:28:46 +02:00
Julien Fontanet
359e7d0543 Merge pull request #437 from vatesfr/julien-f-hvm-network-install
Add PXE installation method for HVM templates. (fix #436)
2015-10-19 15:12:37 +02:00
Julien Fontanet
07bf93e022 Add PXE installation method for HVM templates. (fix #436) 2015-10-19 15:11:43 +02:00
Olivier Lambert
57e27da0c4 update style 2015-10-13 19:19:11 +02:00
Olivier Lambert
9ecbf62d25 fix #136 by adding FreeBSD icons from another icon pack 2015-10-13 18:56:23 +02:00
Olivier Lambert
48ffa591ca display the correct distro icons 2015-10-13 18:46:23 +02:00
Olivier Lambert
b7dd617bb1 update filter 2015-10-13 18:46:23 +02:00
Olivier Lambert
392f9d0775 install of new fonts Mfizz 2015-10-13 18:46:23 +02:00
Fabrice Marsaud
4361b11c68 4.7.0 2015-10-12 16:56:56 +02:00
Fabrice Marsaud
28bccad010 No plugin message 2015-10-12 16:44:47 +02:00
Fabrice Marsaud
29d31a0deb Cleaner plugin conf saving 2015-10-12 16:17:39 +02:00
Fabrice Marsaud
1d9960d349 Plugin configuration fixed and enhanced 2015-10-12 14:49:09 +02:00
Fabrice Marsaud
2747b241ab Notification for plugin conf saved 2015-10-12 12:13:32 +02:00
Fabrice Marsaud
6b8035b116 Plugin conf supports default values + fixes 2015-10-12 11:57:11 +02:00
Olivier Lambert
33ad5f4d45 release date 2015-10-12 10:56:06 +02:00
Fabrice Marsaud
af7ad9251a (not)required plugin conf values of object type are handled + fix 2015-10-12 10:54:23 +02:00
Fabrice Marsaud
4ec9975aa3 Merge pull request #416 from vatesfr/pluginConf#352
Plugin conf#352
2015-10-09 17:41:01 +02:00
Fabrice Marsaud
c6b0841583 Plugin configuration panel 2015-10-09 17:35:26 +02:00
Olivier Lambert
9312435076 Merge pull request #422 from vatesfr/pierre-cant-migrate-vm-to-current-host
Removed current host in migrate list. Disabled button when no other h…
2015-10-08 16:24:43 +02:00
Pierre
49427f1c54 Fix : condition for disabled button 2015-10-08 16:22:06 +02:00
Pierre
82e7e06dc4 Fix : button not diabled if host available on another pool 2015-10-08 16:03:08 +02:00
Pierre
76cf82bb19 Removed current host in migrate list. Disabled button when no other host available 2015-10-08 15:46:11 +02:00
Olivier Lambert
4f8ad2962e changelog updated 2015-10-08 15:22:27 +02:00
Olivier Lambert
fe4be48bff remove useless entries in the menu for hosts 2015-10-08 15:05:36 +02:00
Olivier Lambert
66fc0b421b fix tooltip as explained in #421 2015-10-08 15:03:50 +02:00
Olivier Lambert
c0b4867659 Merge pull request #420 from vatesfr/pierre-lone-host-cant-leave-pool
Added condition to remove 'remove host from pool'-button when host is…
2015-10-08 14:51:58 +02:00
Pierre
95253fbc76 Added condition to remove 'remove host from pool'-button when host is alone 2015-10-08 14:26:20 +02:00
Olivier Lambert
df519b3042 Merge pull request #418 from vatesfr/pierre-add-host-to-pool
Added button to merge host to another pool
2015-10-08 12:30:37 +02:00
Pierre
9ed963ef70 Modified icon for 'moving host to another pool' 2015-10-08 12:26:06 +02:00
Pierre
1dd7993e7a Added button to mode host to another pool. Doesn't work yet 2015-10-08 11:55:03 +02:00
Olivier Lambert
386b33b65d Merge pull request #411 from vatesfr/chartswitch
add chart selector for dataviz
2015-10-07 23:57:08 +02:00
florent
416deb8711 chart selector : less opacity and bigger font 2015-10-07 21:50:24 +02:00
florent
3c7fdac55e add navigation between dataviz
also : fix import in health, fix thumbnail size
2015-10-07 21:13:04 +02:00
Olivier Lambert
392a6af47f Merge pull request #415 from vatesfr/abhamonr-pass-generator-on-users
Password generation is implemented with password-generator module.
2015-10-07 17:30:16 +02:00
wescoeur
6b03e3f603 Password generation is implemented with password-generator module.
One button (with tooltip) is visible to the left of each password field.
One click on this button generate one visible password of 8 characters (upper/lower-case letters, underscore, numbers)
2015-10-07 17:24:09 +02:00
Olivier Lambert
9397f6beda minor changelog fix 2015-10-07 15:56:26 +02:00
Olivier Lambert
d17b386fd6 changelog updated 2015-10-07 15:44:50 +02:00
Olivier Lambert
f6d2e1a447 more explicit modal when host is removed from pool 2015-10-07 15:38:41 +02:00
Olivier Lambert
bd95ef5db6 Merge pull request #407 from vatesfr/desktop-notif
Use desktop notification when available and when the browser is minified
2015-10-07 15:13:13 +02:00
Olivier Lambert
6e76c621b8 Merge pull request #413 from vatesfr/pierre-unfocus-on-cd-list
Unfocusing CD list after selecting one
2015-10-07 15:08:11 +02:00
Olivier Lambert
3e58bee0eb Merge pull request #409 from vatesfr/abhamon-ronan-issue-396
Abhamon ronan issue 396
2015-10-07 14:46:25 +02:00
wescoeur
8c2ed1f581 Password viewer button added to right (with tooltip) and only displayed if input is not empty. 2015-10-07 14:41:31 +02:00
Pierre
500dd3bfaf Unfocusing CD list after selecting one 2015-10-07 12:04:35 +02:00
Olivier Lambert
bc7bacd654 remove Travis test on Node 0.10 2015-10-07 10:58:54 +02:00
florent
fa16b990b6 add chart selector for dataviz 2015-10-06 22:06:33 +02:00
Florent BEAUCHAMP
9d5e9dd9e5 Use desktop notification when avilable and when the browser is minified
without jquery, and using angular $window and $document
2015-10-06 20:37:56 +02:00
Olivier Lambert
4046f9dde1 restrict export button for admins only. Fix #410 2015-10-05 16:45:35 +02:00
Julien Fontanet
fcd82ada14 Fix coding styles and test on Travis. 2015-10-02 19:20:13 +02:00
Olivier Lambert
d616da7f67 Merge pull request #399 from vatesfr/circle
first integration of cricle chart
2015-10-01 21:45:43 +02:00
florent
0c81202bbb how circle chart and sunburst charts + bug fix
correct bug where wraptext was going wild with dimension less node
2015-10-01 20:53:43 +02:00
Julien Fontanet
6284bd3f17 Remove an unecessary console.log(). 2015-10-01 18:38:54 +02:00
Julien Fontanet
7adc9d94b4 Upgrade gulp-livereload to 3.8.1 to support npm 3. 2015-10-01 18:38:42 +02:00
florent
73e030d2f5 d3 circle : hide text if fully zoomed out 2015-09-30 22:07:19 +02:00
florent
3a3b45aa04 d3 charts : externalize a breadcrumbs directive
refactor sunburst and circle to use it
2015-09-30 22:00:39 +02:00
florent
81c19e9964 circle : hide virtual node ( like free ram or free disk) 2015-09-30 18:55:12 +02:00
florent
df856bc4a0 zoomable circle chart 2015-09-30 18:31:24 +02:00
florent
8558dc7ee4 first integration of cricle chart, no zoom
Drop in remplacement of sunburst or treemap
2015-09-30 15:14:42 +02:00
Fabrice Marsaud
087d5f6e58 4.6.1 2015-09-25 12:16:17 +02:00
Fabrice Marsaud
9540bc350a Fix a wrong deletion 2015-09-25 12:14:17 +02:00
Julien Fontanet
09153c6c30 Revert "Minor fix in release process."
This reverts commit 8dbab73d2b.

The release process was already correct.
2015-09-25 10:18:46 +02:00
Olivier Lambert
f66d81f147 4.6.0 2015-09-25 00:10:29 +02:00
Olivier Lambert
75925143b6 Merge pull request #395 from vatesfr/cubism
Multigraph, resolve issue #358
2015-09-24 21:37:28 +02:00
Florent BEAUCHAMP
e7dc00991e remove cubism from package.json 2015-09-24 21:26:45 +02:00
Florent BEAUCHAMP
dd9da82ed3 Merge branch 'cubism' of https://github.com/vatesfr/xo-web into cubism 2015-09-24 21:18:59 +02:00
Florent BEAUCHAMP
c995b8fa81 horizon: sort metrics name, rename cpu average to all cpus 2015-09-24 21:15:53 +02:00
Florent BEAUCHAMP
e7c2994ea3 horizon : delete last traces of cubism 2015-09-24 21:15:52 +02:00
Florent BEAUCHAMP
106997b26c horizon responsive 2015-09-24 21:15:51 +02:00
Florent BEAUCHAMP
fa842c1566 horizon : smaller label on date axis 2015-09-24 21:15:50 +02:00
Florent BEAUCHAMP
be03dd82f9 remove cubism 2015-09-24 21:15:49 +02:00
Florent BEAUCHAMP
39c46995e1 horizon : properly clean existing label 2015-09-24 21:15:48 +02:00
Florent BEAUCHAMP
97adc01e8d horizon : replace checkbox by button, remove bold 2015-09-24 21:15:47 +02:00
Florent BEAUCHAMP
36be881741 handle missing value 2015-09-24 21:15:45 +02:00
Florent BEAUCHAMP
955cc6dff5 horizon : add filter on hover 2015-09-24 21:15:44 +02:00
Florent BEAUCHAMP
2be1399eda horizon : add label, center label vertically, add a checkbox to
synchronize scales
2015-09-24 21:15:43 +02:00
Olivier Lambert
ef5d2a7654 change the panel title 2015-09-24 21:09:56 +02:00
Florent BEAUCHAMP
1cd00cab62 horizon : synchronize scales 2015-09-24 21:09:54 +02:00
Olivier Lambert
7652c231f6 case for foreach 2015-09-24 21:09:53 +02:00
Florent BEAUCHAMP
e17cdf0ca7 horizon : over text lighter 2015-09-24 21:09:51 +02:00
Florent BEAUCHAMP
3317791e68 rizons : two numbers 2015-09-24 21:09:50 +02:00
Florent BEAUCHAMP
c3871bc2ec Horizon : hide cpu x in metrics list 2015-09-24 21:09:49 +02:00
Florent BEAUCHAMP
ebba86f741 horizon : add label before chart
better handling of form when loading new metrics
2015-09-24 21:09:47 +02:00
Florent BEAUCHAMP
5ac84a6a02 replace cubism with a custom implementation 2015-09-24 21:09:46 +02:00
Florent BEAUCHAMP
cf3e9704e8 wip 2015-09-24 21:09:44 +02:00
Florent BEAUCHAMP
37eac8afcf formatting 2015-09-24 21:09:43 +02:00
Florent BEAUCHAMP
692a0535ff move cubism from dataviz to health 2015-09-24 21:09:42 +02:00
Olivier Lambert
0f0d804052 fix a typo in clonedeep import and save the depencency 2015-09-24 21:09:40 +02:00
florent
d189e6b53d first integration of cubism 2015-09-24 21:08:56 +02:00
Florent BEAUCHAMP
5da31691a9 horizon: sort metrics name, rename cpu average to all cpus 2015-09-24 21:02:34 +02:00
Florent BEAUCHAMP
4059a4fd9a horizon : delete last traces of cubism 2015-09-24 20:47:20 +02:00
Florent BEAUCHAMP
e56da71856 horizon responsive 2015-09-24 20:40:15 +02:00
Florent BEAUCHAMP
91e10f627f horizon : smaller label on date axis 2015-09-24 20:04:52 +02:00
Florent BEAUCHAMP
338c686e8d remove cubism 2015-09-24 19:53:31 +02:00
Florent BEAUCHAMP
0007e9ea2b horizon : properly clean existing label 2015-09-24 19:50:08 +02:00
Florent BEAUCHAMP
1e09e9b322 horizon : replace checkbox by button, remove bold 2015-09-24 19:13:48 +02:00
Florent BEAUCHAMP
43c358119a handle missing value 2015-09-24 18:48:03 +02:00
Florent BEAUCHAMP
7a0f251ebd horizon : add filter on hover 2015-09-24 18:36:17 +02:00
Florent BEAUCHAMP
e989321c5f Merge branch 'cubism' of https://github.com/vatesfr/xo-web into cubism 2015-09-24 18:12:42 +02:00
Florent BEAUCHAMP
56f27e6aaa horizon : add label, center label vertically, add a checkbox to
synchronize scales
2015-09-24 18:08:36 +02:00
Julien Fontanet
7c4e5aa667 Move lodash.debounce to dev deps. 2015-09-24 16:48:12 +02:00
Julien Fontanet
d253d826bb Update xo-lib to 0.7.3 to support Node 4. 2015-09-24 16:47:57 +02:00
Olivier Lambert
888fa20ca3 update the changelog 2015-09-24 12:13:31 +02:00
Julien Fontanet
598dbb2b7a Typos. 2015-09-24 11:37:02 +02:00
Julien Fontanet
71eb1eab14 Properly handles invalid auth tokens. 2015-09-24 11:09:22 +02:00
Olivier Lambert
62a6bd99e8 Merge pull request #393 from vatesfr/issue#237
Rely on unified server-side VM migrate
2015-09-24 10:27:29 +02:00
Olivier Lambert
174cdf2149 fix issue #394 2015-09-24 10:24:10 +02:00
Olivier Lambert
5267fbce7b change the panel title 2015-09-23 23:00:18 +02:00
Fabrice Marsaud
dd814e7e95 removed late migrate notification 2015-09-23 22:41:50 +02:00
Florent BEAUCHAMP
f806b45d3d Merge branch 'cubism' of https://github.com/vatesfr/xo-web into cubism 2015-09-23 22:35:30 +02:00
Florent BEAUCHAMP
8575e9eabe horizon : synchronize scales 2015-09-23 22:34:50 +02:00
Olivier Lambert
6ff9e22049 case for foreach 2015-09-23 22:18:42 +02:00
Fabrice Marsaud
caa86fdab7 Rely on unified server-side VM migrate 2015-09-23 22:15:19 +02:00
Florent BEAUCHAMP
48246716cc horizon : over text lighter 2015-09-23 22:04:29 +02:00
Florent BEAUCHAMP
6e07429e8a rizons : two numbers 2015-09-23 22:01:41 +02:00
Florent BEAUCHAMP
1a271c32b6 Horizon : hide cpu x in metrics list 2015-09-23 21:57:07 +02:00
Florent BEAUCHAMP
3fddec8f20 horizon : add label before chart
better handling of form when loading new metrics
2015-09-23 21:42:11 +02:00
Florent BEAUCHAMP
ac3944aece replace cubism with a custom implementation 2015-09-23 20:59:25 +02:00
Florent BEAUCHAMP
958cc2a50c wip 2015-09-22 23:17:32 +02:00
Florent BEAUCHAMP
058dfcfa9f Merge branch 'cubism' of https://github.com/vatesfr/xo-web into cubism 2015-09-22 19:09:35 +02:00
Florent BEAUCHAMP
9dbb1ca386 formatting 2015-09-22 19:06:01 +02:00
Florent BEAUCHAMP
4d1def6e9d move cubism from dataviz to health 2015-09-22 18:51:51 +02:00
Olivier Lambert
ff763b0278 fix a typo in clonedeep import and save the depencency 2015-09-22 13:28:04 +02:00
florent
74f611e0fd first integration of cubism 2015-09-22 09:23:28 +02:00
Olivier Lambert
61f8be1c60 Merge pull request #388 from vatesfr/issue#222
Filter for SR view VDIs #222
2015-09-17 18:01:41 +02:00
Olivier Lambert
96b18dab00 just add a placeholder in the filter for VDIs 2015-09-17 18:01:12 +02:00
Fabrice Marsaud
0a21b239bc Filter for SR view VDIs #222 2015-09-17 17:51:55 +02:00
Olivier Lambert
9c3589aea4 add PV args during VM creation. Fix #112 2015-09-17 16:36:14 +02:00
Olivier Lambert
2433485d13 Merge pull request #387 from vatesfr/issue#350
Issue#350
2015-09-17 14:56:21 +02:00
Fabrice Marsaud
6b5f254e0a Confirmations on tree and SR view 2015-09-17 14:51:33 +02:00
Fabrice Marsaud
e1b41b1e26 Additional confirmations for console view 2015-09-17 14:51:33 +02:00
Fabrice Marsaud
bd7a265df0 Additional confirmations for VM view 2015-09-17 14:51:33 +02:00
Olivier Lambert
039cca9529 Merge pull request #385 from vatesfr/issue#383
Html fixes so form required are no more ignored
2015-09-17 12:35:06 +02:00
Olivier Lambert
963347dbc2 Merge pull request #381 from vatesfr/issue#107
noVnc connection reset on disconnection
2015-09-17 12:19:19 +02:00
Fabrice Marsaud
697cc9f758 Html fixes so form required are no more ignored 2015-09-17 11:00:24 +02:00
Fabrice Marsaud
3892225584 Limited retries to reset consoles after reboot or halt or whatever disconnects... 2015-09-17 10:52:08 +02:00
Fabrice Marsaud
a7880a0ef5 noVnc connection reset on disconnection 2015-09-17 10:52:08 +02:00
Fabrice Marsaud
dd574830f5 Merge pull request #380 from vatesfr/issue#367
Tag management in xo-web
2015-09-17 10:30:10 +02:00
Fabrice Marsaud
71a0d15c35 Tag directive ensures an up to date tag display 2015-09-17 10:25:58 +02:00
Fabrice Marsaud
8a33c4f09a Various enhancements of the tag directive 2015-09-17 10:25:57 +02:00
Fabrice Marsaud
d223ce062a tags for VDIs. A refresh problem in VM view 2015-09-17 10:25:57 +02:00
Fabrice Marsaud
39c8f12963 Fixes for tag management 2015-09-17 10:25:57 +02:00
Fabrice Marsaud
bd4ba8c826 Autofocus, and smaller + button 2015-09-17 10:25:57 +02:00
Fabrice Marsaud
3d38c8e088 Add tag module 2015-09-17 10:25:57 +02:00
Fabrice Marsaud
ce58c80c6d Tag management UI for issue#367 2015-09-17 10:25:57 +02:00
Julien Fontanet
19b3a0781c Deps update. 2015-09-17 10:12:02 +02:00
Olivier Lambert
b42c1971b9 Merge pull request #382 from vatesfr/issue#323
Issue#323
2015-09-16 18:45:14 +02:00
Olivier Lambert
02440941e0 homogenize by using spaces vs nbsp 2015-09-16 17:58:27 +02:00
Olivier Lambert
cd2f986c50 fix a missing space in the list view 2015-09-16 17:56:22 +02:00
Olivier Lambert
e7cbd6b31f Update CHANGELOG.md
Add the fix pushed in the 4.5.1
2015-09-16 15:09:34 +02:00
Fabrice Marsaud
a7f6d5eebd Better order for acl management 2015-09-16 13:45:48 +02:00
Fabrice Marsaud
4f3b8c0906 Acls are editable 2015-09-16 13:45:48 +02:00
Fabrice Marsaud
7126c71943 Merge branch 'master' into next-release 2015-09-16 13:42:27 +02:00
Fabrice Marsaud
489cf16af8 4.5.1 2015-09-16 12:05:00 +02:00
Fabrice Marsaud
b012f44259 Merge pull request #379 from vatesfr/issue378
Fixes issue#378 and adds some cancel edit buttons on VM view. Ready for 4.5.1.
2015-09-16 12:03:00 +02:00
Fabrice Marsaud
5ce765bd27 Fixes issue#378 and adds some cancel edit buttons on VM view 2015-09-16 11:44:46 +02:00
Olivier Lambert
2450edd070 another minor UI fix 2015-09-15 20:01:54 +02:00
Olivier Lambert
fc2a61835c fix edge cases display of tools outdated but installed 2015-09-14 22:12:34 +02:00
Fabrice Marsaud
d06d73d5f7 Merge pull request #376 from vatesfr/issue#362
Any user can manage his password himself
2015-09-14 17:02:10 +02:00
Fabrice Marsaud
2c10996bb3 UI terms 2015-09-14 16:13:33 +02:00
Fabrice Marsaud
b9c85bb1bf Rework on password change 2015-09-14 15:36:22 +02:00
Fabrice Marsaud
f436afb9aa Any user can manage his password himself 2015-09-14 15:36:22 +02:00
Olivier Lambert
750efe4152 Merge pull request #377 from vatesfr/issue#342
Clearer tooltips for heatmap
2015-09-14 14:36:04 +02:00
Fabrice Marsaud
99cee95cd5 Clearer tooltips for heatmap 2015-09-14 14:24:10 +02:00
Olivier Lambert
ec10b84fa6 add new favicon. Fix #369 2015-09-11 22:16:35 +02:00
Olivier Lambert
f0442fe2ce spaces between tags in flat view 2015-09-11 22:16:06 +02:00
Olivier Lambert
7907969696 Merge pull request #372 from vatesfr/issue#368
Fix update panel for issue#368
2015-09-11 18:08:59 +02:00
Julien Fontanet
8dbab73d2b Minor fix in release process. 2015-09-11 18:04:18 +02:00
Fabrice Marsaud
ade8acb4e2 Fix update panel for issue#368 2015-09-11 15:54:08 +02:00
Fabrice Marsaud
9cb78e6954 Fix for Issue#357 2015-09-11 15:34:17 +02:00
Julien Fontanet
e9127bdbb3 4.5.0 2015-09-11 12:59:00 +02:00
Olivier Lambert
9b750bc756 add missing stuff in the changelog 2015-09-10 17:32:22 +02:00
Fabrice Marsaud
c3349e8cc7 Merge pull request #366 from vatesfr/issue#268
Issue#268
2015-09-10 17:24:58 +02:00
Olivier Lambert
f4d7c7f739 hide other buttons 2015-09-10 17:24:55 +02:00
Olivier Lambert
19d51cb1a4 remove a typo 2015-09-10 17:24:55 +02:00
Olivier Lambert
cc9983aa16 hide actions for a viewer and fix others 2015-09-10 17:24:55 +02:00
Fabrice Marsaud
81f8467f66 ng-if solution for unauthorized action button and links 2015-09-10 17:24:55 +02:00
Fabrice Marsaud
df6b23e3c7 disable solution. not very satisfying 2015-09-10 17:24:55 +02:00
Fabrice Marsaud
4dd81e7d59 Merge pull request #365 from vatesfr/issue#330
Issue#330
2015-09-10 17:22:27 +02:00
Fabrice Marsaud
54ce7067b4 Nan and data type bugs fixed 2015-09-10 17:22:32 +02:00
Fabrice Marsaud
2673f790e6 Issue #330 enhanced heatmap, first delivery 2015-09-10 17:22:32 +02:00
Fabrice Marsaud
69bea2ec9b Merge pull request #364 from vatesfr/issue#315
Issue#315
2015-09-10 16:18:58 +02:00
Fabrice Marsaud
37037cf797 Fixed "onlymetadata" backup bugs 2015-09-10 15:59:36 +02:00
Fabrice Marsaud
2a1586aab3 Onlymetadata scheduled backups 2015-09-09 12:12:06 +02:00
florent
915281d138 cleanup and enlarge brush 2015-09-08 23:05:36 +02:00
florent
b53a179ea0 correctly invoke parallel chart + readme + add drag filter 2015-09-08 22:56:32 +02:00
Olivier Lambert
7077e8b50e Merge pull request #359 from vatesfr/dynamic-charts
Dynamic charts
2015-09-08 13:16:57 +02:00
florent
d9181277d9 correct wrong throttle/debounce use * 2015-09-08 12:18:27 +02:00
florent
810c2d6a1a listen to xoApi.onUpdate event to refresh charts, throttle refresh to once every 300ms 2015-09-08 12:18:08 +02:00
Olivier Lambert
f31113fb90 change panel name 2015-09-08 12:08:01 +02:00
florent
1702b9dd37 force domain to start at zero 2015-09-08 12:06:13 +02:00
florent
f8e61c713c first implementation of parrallel chart directive 2015-09-08 12:05:16 +02:00
Julien Fontanet
643132754a xoApi.onUpdate(fn) 2015-09-07 14:10:51 +02:00
Olivier Lambert
4f0a131bd2 fix #341 2015-09-07 13:13:08 +02:00
Olivier Lambert
52aa0350cf update the changelog 2015-09-07 13:12:56 +02:00
Olivier Lambert
c9884f32fe fix #355 2015-09-07 12:36:35 +02:00
Olivier Lambert
aea3ae3d37 fix #356 2015-09-07 12:36:23 +02:00
Olivier Lambert
10f7c3045f fix #348 2015-09-03 15:15:51 +02:00
Olivier Lambert
bf4e158c30 fix #347 2015-09-03 14:48:08 +02:00
Olivier Lambert
4a3155ed22 Merge pull request #346 from vatesfr/performance
Performance improvment
2015-09-02 21:38:03 +02:00
florent
29f1c89fa5 fix sunburst layout on FF/IE 2015-09-02 21:08:54 +02:00
florent
4a92e8a99f improve performance
make suburst graph static and synchronize animation
2015-09-02 20:40:32 +02:00
Olivier Lambert
c6cffb1156 add metadata export in the VM view 2015-09-02 17:15:10 +02:00
Olivier Lambert
f6e4e59905 add outdate tools info in VM view 2015-09-02 15:55:20 +02:00
Olivier Lambert
3a0736c4bf fix #304 2015-09-02 15:43:05 +02:00
Julien Fontanet
47455b2029 Merge pull request #344 from vatesfr/coding-standard
Fix coding style of the dashboard.
2015-09-02 11:24:24 +02:00
Julien Fontanet
05eb7d765f Coding style fixes. 2015-09-02 11:12:37 +02:00
Julien Fontanet
5e786686d0 Remove unecessary dep. 2015-09-02 10:29:40 +02:00
florent
5cb8e3a7c3 comply to coding standards 2015-09-01 20:18:10 +02:00
Fabrice Marsaud
84bd077eac 4.4.0 2015-08-28 17:38:23 +02:00
Julien Fontanet
db39b27119 Fix home page on sign in. 2015-08-28 17:14:59 +02:00
Julien Fontanet
f2d2b35543 Merge pull request #335 from vatesfr/passport
Authentication is moved to xo-server.
2015-08-28 16:09:59 +02:00
Julien Fontanet
5dfd5766f2 Authentication is moved to xo-server. 2015-08-28 15:04:47 +02:00
Fabrice Marsaud
0e4c3e1e92 CSS tree view fixes 2015-08-28 10:53:13 +02:00
Fabrice Marsaud
221f42606c Fixes from Issue#329 2015-08-28 10:45:20 +02:00
Olivier Lambert
742f092ed3 add CSS flexbox issue in the changelog 2015-08-28 10:37:23 +02:00
Fabrice Marsaud
36bffa1475 gulpFilter 3.x 2015-08-28 09:22:35 +02:00
Fabrice Marsaud
936abc1b1a Fix for undefined VDIs (base copy) 2015-08-27 23:45:25 +02:00
Fabrice Marsaud
584bdd545f icon fix 2015-08-27 22:38:12 +02:00
Fabrice Marsaud
99debc18d7 Heatmap modifs 2015-08-27 21:41:34 +02:00
Olivier Lambert
56b896eda0 add better title 2015-08-27 18:27:44 +02:00
Fabrice Marsaud
cab102528d add omitted template 2015-08-27 18:24:29 +02:00
Fabrice Marsaud
1875cdcda2 Imporvements 2015-08-27 18:22:28 +02:00
Fabrice Marsaud
386dcc8d43 Host heatmap seems ok 2015-08-27 17:55:21 +02:00
Fabrice Marsaud
e6d59a47b1 Unstable progress 2015-08-27 17:01:25 +02:00
Fabrice Marsaud
2659393f33 First heatmap correct integration 2015-08-27 17:01:25 +02:00
Fabrice Marsaud
916b2363d9 Fixed storage dataviz (avoid hidden base copy vdis) 2015-08-27 17:01:25 +02:00
Olivier Lambert
bb513790b5 add changelog for 4.4 2015-08-27 17:01:25 +02:00
Olivier Lambert
e5ab15a727 remove permission for dashboard view if non admin 2015-08-27 17:01:25 +02:00
Fabrice Marsaud
bbaa750fda css touches
Conflicts:
	app/modules/dashboard/dataviz/view.jade
2015-08-27 17:01:25 +02:00
Fabrice Marsaud
a46e19210a css touch 2015-08-27 17:01:25 +02:00
Olivier Lambert
9d6772edd1 disable dashboard link menu 2015-08-27 17:01:25 +02:00
Olivier Lambert
3aeaa564a2 remove useless treemap 2015-08-27 17:01:25 +02:00
Olivier Lambert
8baad494e3 minor fixes 2015-08-27 17:01:25 +02:00
florent
fb04753d52 doc dor heatmap 2015-08-27 17:01:25 +02:00
florent
ea37c4ccd8 backport some correction to sunburst charts 2015-08-27 17:01:25 +02:00
florent
80f02b52e1 a littel doc for sunburst 2015-08-27 17:01:25 +02:00
florent
55464845d6 week heatmap 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
8aef4bb455 treemap directive 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
d8a2adbca2 dataviz refacto : split megacronlller in two bigcontroller 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
02e56da08a more transition, less console.log 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
9d9f857e73 sunburst : transition 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
b140c1e65f dataviz > overview : show shared SRS 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
81ff03462e dataviz breadcrumbs are full width 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
2ea7c09c84 dataviz : add cursor pointer if applicable 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
4163ed212c make dataviz clickable 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
c83722c2df dataviz : correct typo in storage sums 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
700db655e6 dataviz : storage is now pool > host > SRS > VDI
hide .iso srs
2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
226428f631 dataviz : replace cpu view by storage view pool>SRS>vdi 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
e2b293e49b no udev in overview list 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
1f6e9d4660 directive - less border 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
ecf2ee888f dataviz : remove logarithmic scale 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
1e8eeadb1d switch to a logarithm scale for ram 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
e7ceccdd83 dataviz : do not show "virtual pool" for poolless host 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
5390b4a4b3 oups, wrong CPU count 2015-08-27 17:01:25 +02:00
Florent BEAUCHAMP
f25ec34bc3 show free ram and cpu 2015-08-27 17:01:24 +02:00
Florent BEAUCHAMP
53ece86816 don't show iso in overview > SR. Order SR by usage ratio desc 2015-08-27 17:01:24 +02:00
Florent BEAUCHAMP
9b128cdfcc add comprehensible tooltip on overview > ram 2015-08-27 17:01:24 +02:00
Florent BEAUCHAMP
c047386755 sunburst : add text info in the center 2015-08-27 17:01:24 +02:00
Florent BEAUCHAMP
b0ffb272b3 sunburst : border width is coherent with graph width 2015-08-27 17:01:24 +02:00
Olivier Lambert
956e21c8db code style now using JS standard 2015-08-27 17:01:24 +02:00
Florent BEAUCHAMP
95057a2b09 use the right visualization 2015-08-27 17:01:24 +02:00
Florent BEAUCHAMP
788aa24a80 overview with real data 2015-08-27 17:01:24 +02:00
Florent BEAUCHAMP
0a72ef91cc less opacity to non hovered sunburst node. text ellipsis in breadcrumbs 2015-08-27 17:01:24 +02:00
Florent BEAUCHAMP
f0f4e0985a sunburst directive : externalized in a module, react to vm changes 2015-08-27 17:01:24 +02:00
Florent BEAUCHAMP
a6bedea4b6 sunburst are responsive 2015-08-27 17:01:24 +02:00
Florent BEAUCHAMP
055316e1ca multiple synchronizated sunburst + breadcrumbs 2015-08-27 17:01:24 +02:00
Olivier Lambert
25fece5947 add initial d3 work of FBP 2015-08-27 17:01:24 +02:00
Olivier Lambert
5ec3cdbcc5 health page mockup 2015-08-27 17:01:24 +02:00
Olivier Lambert
b530ab2ef6 clean copy/pasted code 2015-08-27 17:01:24 +02:00
Olivier Lambert
5164f60c98 Ui fixes for dashboard 2015-08-27 17:01:24 +02:00
Olivier Lambert
19ba3015f7 not responsive graph to avoid a display bug 2015-08-27 17:01:24 +02:00
Olivier Lambert
fb00b2672c start dashboard mockup 2015-08-27 17:01:24 +02:00
Fabrice Marsaud
3e0f936d2a host refreshStats fix 2015-08-27 16:47:58 +02:00
Fabrice Marsaud
f5be146dbb refreshStats fix 2015-08-27 10:01:44 +02:00
Olivier Lambert
95431a0874 disable the scheduler for non admin users 2015-08-26 16:53:15 +02:00
Julien Fontanet
f76b130ca4 Coding style fixes. 2015-08-26 15:03:48 +02:00
Julien Fontanet
d19f8259d0 Upgrade deps. 2015-08-26 15:03:35 +02:00
Julien Fontanet
68cd62d756 Scheduler view is only accessible to admins. 2015-08-26 11:08:58 +02:00
Fabrice Marsaud
ea4a55d3dd grid css enhanced 2015-08-25 19:11:28 +02:00
Fabrice Marsaud
788bdcd35b menu-entry class is back 2015-08-24 18:20:38 +02:00
Fabrice Marsaud
ebd7e24830 group icon fix 2015-08-24 17:21:22 +02:00
Fabrice Marsaud
0b7fbffa0a Icons and styles enhanced a bit 2015-08-24 17:02:14 +02:00
Fabrice Marsaud
2afda9a055 form template touch 2015-08-24 17:02:14 +02:00
Fabrice Marsaud
28dd275bd8 Honest responsive grid 2015-08-24 17:02:14 +02:00
Fabrice Marsaud
9a3cf182ac side menu and other enhancements 2015-08-24 17:02:14 +02:00
Fabrice Marsaud
dd278c28be first steps 2015-08-24 17:02:14 +02:00
Fabrice Marsaud
37572122b0 Reload prompt after upgrade 2015-08-24 16:55:04 +02:00
Fabrice Marsaud
5348f75b5e ACL enhanced object selector 2015-08-10 18:43:44 +02:00
Fabrice Marsaud
6b8873d385 Updater flow and display fixed 2015-07-31 09:27:06 +02:00
Fabrice Marsaud
8db18d87e5 Minor vocabulary fix 2015-07-29 15:18:26 +02:00
Fabrice Marsaud
444920d15c 4.3.0 2015-07-22 15:48:52 +02:00
Fabrice Marsaud
8aa2fab603 Merge branch 'next-release' 2015-07-22 15:39:36 +02:00
Olivier Lambert
9ded4386cc Update CHANGELOG.md 2015-07-22 15:16:13 +02:00
Fabrice Marsaud
cc6b1b5aa1 Fixed delete all host logs 2015-07-22 15:05:54 +02:00
Olivier Lambert
74f20da82f Merge pull request #305 from vatesfr/issue#301
GUI fix for vm delete
2015-07-21 14:34:59 +02:00
Olivier Lambert
4c9c838b70 Merge pull request #300 from vatesfr/marsaud-backup
Scheduled VM Backup feature
2015-07-21 14:34:51 +02:00
Fabrice Marsaud
9a9d27d37a GUI fix for vm delete 2015-07-21 14:26:07 +02:00
Fabrice Marsaud
0347d4cec4 Rolling backup feature 2015-07-21 14:14:15 +02:00
Fabrice Marsaud
b1b189288e Fix issue #302 2015-07-21 14:11:24 +02:00
Fabrice Marsaud
b3220f981b GUI fix for vm delete 2015-07-20 19:29:53 +02:00
Julien Fontanet
a5573e62c6 Update deps. 2015-07-15 09:47:01 +02:00
Fabrice Marsaud
c4ccee8df6 4.2.0 2015-06-29 09:18:43 +02:00
Fabrice Marsaud
fbcf803d06 Merge branch 'next-release' 2015-06-29 09:16:31 +02:00
Olivier Lambert
5247b7a9af Update CHANGELOG.md 2015-06-27 22:42:39 +02:00
Fabrice Marsaud
dc218cc992 rollingSnap view clean 2015-06-26 14:39:42 +02:00
Fabrice Marsaud
c21761d9d4 Merge pull request #289 from vatesfr/scheduler#176
Scheduler#176
2015-06-26 14:24:38 +02:00
Fabrice Marsaud
36c0bf06d7 Rolling VM Snapshot scheduling feature 2015-06-26 12:35:02 +02:00
Fabrice Marsaud
ccdab2b083 Fixed VM snapshots pagination 2015-06-26 12:34:35 +02:00
Fabrice Marsaud
15a8a56807 Fixes service link 2015-06-26 09:47:35 +02:00
Fabrice Marsaud
385d42281b Revert "This code should now be unnecessary."
This reverts commit 5f9cf47003.
2015-06-25 17:29:20 +02:00
Julien Fontanet
b0dc933021 Do not use babel-plugin-closure-elimination. 2015-06-23 15:01:49 +02:00
Olivier Lambert
b73ee1f638 Merge pull request #293 from vatesfr/SRViewFix
Fixed VM display on SR view
2015-06-22 10:40:23 +02:00
Fabrice Marsaud
51c2a54179 Fixed VM display on SR view 2015-06-22 10:25:44 +02:00
Julien Fontanet
2d71a916a2 Update vinyl to 0.5. 2015-06-15 16:47:56 +02:00
Julien Fontanet
5f9cf47003 This code should now be unnecessary. 2015-06-15 15:15:12 +02:00
Julien Fontanet
16b39185dc Fix coding style in gulpfile.js 2015-06-15 14:29:45 +02:00
Julien Fontanet
6f0410f26e Fix Angular to 1.4.x.
Angular's devs tend to break API on minor versions.
2015-06-15 10:58:18 +02:00
Julien Fontanet
0b86845852 Add Babel plugin for closure elimination (disabled for now). 2015-06-11 08:54:58 +02:00
Julien Fontanet
d5f914bd2f Do not distribute examples & tests. 2015-06-11 08:54:58 +02:00
Julien Fontanet
663c65e42e Babel configuration. 2015-06-11 08:54:58 +02:00
Julien Fontanet
b9de86f96c Minor fix in EditorConfig. 2015-06-11 08:54:58 +02:00
Olivier Lambert
bd9c0ffb25 4.1.0 2015-06-10 16:59:17 +02:00
Olivier Lambert
540f977146 Update CHANGELOG.md 2015-06-10 16:24:33 +02:00
156 changed files with 14113 additions and 1501 deletions

14
.babelrc Normal file
View File

@@ -0,0 +1,14 @@
{
"comments": false,
"compact": true,
"optional": [
// Experimental features.
// "minification.constantFolding",
// "minification.deadCodeElimination",
"es7.asyncFunctions",
"es7.decorators",
"es7.functionBind"
//"runtime"
]
}

View File

@@ -46,7 +46,7 @@ indent_size = 2
indent_style = space
# Less
[*.js]
[*.less]
indent_size = 2
indent_style = space

4
.gitignore vendored
View File

@@ -4,3 +4,7 @@
/node_modules/*
!/node_modules/*.js
/node_modules/*.js/
jsconfig.json
.idea
npm-debug.log

9
.npmignore Normal file
View File

@@ -0,0 +1,9 @@
/examples/
example.js
*.example.js
*.example.js.map
/test/
/tests/
*.spec.js
*.spec.js.map

10
.travis.yml Normal file
View File

@@ -0,0 +1,10 @@
language: node_js
node_js:
- 'stable'
# Use containers.
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
sudo: false
before_install:
- npm i -g npm

View File

@@ -1,5 +1,330 @@
# ChangeLog
## **4.12.0** (2016-01-18)
Continuous Replication, Continuous Delta backup...
### Enhancements
- Continuous VM replication [\#582](https://github.com/vatesfr/xo-web/issues/582)
- Continuous Delta Backup [\#576](https://github.com/vatesfr/xo-web/issues/576)
- Scheduler should not run job again if previous instance is not finished [\#642](https://github.com/vatesfr/xo-web/issues/642)
- Boot VM automatically after creation [\#635](https://github.com/vatesfr/xo-web/issues/635)
- Manage existing VIFs in templates [\#630](https://github.com/vatesfr/xo-web/issues/630)
- Support templates with existing install repository [\#627](https://github.com/vatesfr/xo-web/issues/627)
- Remove running VMs [\#616](https://github.com/vatesfr/xo-web/issues/616)
- Prevent a VM to start before delta import is finished [\#613](https://github.com/vatesfr/xo-web/issues/613)
- Spawn multiple VMs at once [\#606](https://github.com/vatesfr/xo-web/issues/606)
- Fixed `suspendVM` in tree view. [\#619](https://github.com/vatesfr/xo-web/pull/619) ([pdonias](https://github.com/pdonias))
### Bug fixes
- User defined MAC address is not fetch in VM install [\#643](https://github.com/vatesfr/xo-web/issues/643)
- CoreOsCloudConfig is not shown with CoreOS [\#639](https://github.com/vatesfr/xo-web/issues/639)
- Plugin activation/deactivation in web UI seems broken [\#637](https://github.com/vatesfr/xo-web/issues/637)
- Issue when creating CloudConfig drive [\#636](https://github.com/vatesfr/xo-web/issues/636)
- CloudConfig hostname shouldn't have space [\#634](https://github.com/vatesfr/xo-web/issues/634)
- Cloned VIFs are not properly deleted on VM creation [\#632](https://github.com/vatesfr/xo-web/issues/632)
- Default PV args missing during VM creation [\#628](https://github.com/vatesfr/xo-web/issues/628)
- VM creation problems from custom templates [\#625](https://github.com/vatesfr/xo-web/issues/625)
- Emergency shutdown race condition [\#622](https://github.com/vatesfr/xo-web/issues/622)
- `vm.delete\(\)` should not delete VDIs attached to other VMs [\#621](https://github.com/vatesfr/xo-web/issues/621)
- VM creation error from template with a disk [\#581](https://github.com/vatesfr/xo-web/issues/581)
- Only delete VDI exports when VM backup is successful [\#644](https://github.com/vatesfr/xo-web/issues/644)
- Change the name of an imported VM during the import process [\#641](https://github.com/vatesfr/xo-web/issues/641)
- Creating a new VIF in view is partially broken [\#652](https://github.com/vatesfr/xo-web/issues/652)
- Grey out the "create button" during VM creation [\#654](https://github.com/vatesfr/xo-web/issues/654)
## **4.11.0** (2015-12-22)
Delta backup, CloudInit...
### Enhancements
- Visible list of SR inside a VM [\#601](https://github.com/vatesfr/xo-web/issues/601)
- VDI move [\#591](https://github.com/vatesfr/xo-web/issues/591)
- Edit pre-existing disk configuration during VM creation [\#589](https://github.com/vatesfr/xo-web/issues/589)
- Allow disk size edition [\#587](https://github.com/vatesfr/xo-web/issues/587)
- Better VDI resize support [\#585](https://github.com/vatesfr/xo-web/issues/585)
- Remove manual VM export metadata in UI [\#580](https://github.com/vatesfr/xo-web/issues/580)
- Support import VM metadata [\#579](https://github.com/vatesfr/xo-web/issues/579)
- Set a default pool SR [\#572](https://github.com/vatesfr/xo-web/issues/572)
- ISOs should be sorted by name [\#565](https://github.com/vatesfr/xo-web/issues/565)
- Button to boot a VM from a disc once [\#564](https://github.com/vatesfr/xo-web/issues/564)
- Ability to boot a PV VM from a disc [\#563](https://github.com/vatesfr/xo-web/issues/563)
- Add an option to manually run backup jobs [\#562](https://github.com/vatesfr/xo-web/issues/562)
- backups to unmounted storage [\#561](https://github.com/vatesfr/xo-web/issues/561)
- Root integer properties cannot be edited in plugins configuration form [\#550](https://github.com/vatesfr/xo-web/issues/550)
- Generic CloudConfig drive [\#549](https://github.com/vatesfr/xo-web/issues/549)
- Auto-discovery of installed xo-server plugins [\#546](https://github.com/vatesfr/xo-web/issues/546)
- Hide info on flat view [\#545](https://github.com/vatesfr/xo-web/issues/545)
- Config plugin boolean properties must have a default value \(undefined prohibited\) [\#543](https://github.com/vatesfr/xo-web/issues/543)
- Present detailed errors on plugin configuration failures [\#530](https://github.com/vatesfr/xo-web/issues/530)
- Do not reset form on failures in plugins configuration [\#529](https://github.com/vatesfr/xo-web/issues/529)
- XMPP alert plugin [\#518](https://github.com/vatesfr/xo-web/issues/518)
- Hide tag adders depending on ACLs [\#516](https://github.com/vatesfr/xo-web/issues/516)
- Choosing a framework for xo-web 5 [\#514](https://github.com/vatesfr/xo-web/issues/514)
- Prevent adding a host in an existing XAPI connection [\#466](https://github.com/vatesfr/xo-web/issues/466)
- Read only connection to Xen servers/pools [\#439](https://github.com/vatesfr/xo-web/issues/439)
- generic notification system [\#391](https://github.com/vatesfr/xo-web/issues/391)
- Data architecture review [\#384](https://github.com/vatesfr/xo-web/issues/384)
- Make filtering easier to understand/add some "default" filters [\#207](https://github.com/vatesfr/xo-web/issues/207)
- Improve performance [\#148](https://github.com/vatesfr/xo-web/issues/148)
### Bug fixes
- VM metadata export should not require a snapshot [\#615](https://github.com/vatesfr/xo-web/issues/615)
- Missing patch for all hosts is continuously refreshed [\#609](https://github.com/vatesfr/xo-web/issues/609)
- Backup import memory issue [\#608](https://github.com/vatesfr/xo-web/issues/608)
- Host list missing patch is buggy [\#604](https://github.com/vatesfr/xo-web/issues/604)
- Servers infos should not been refreshed while a field is being edited [\#595](https://github.com/vatesfr/xo-web/issues/595)
- Servers list should not been re-order while a field is being edited [\#594](https://github.com/vatesfr/xo-web/issues/594)
- Correctly display size in interface \(binary scale\) [\#592](https://github.com/vatesfr/xo-web/issues/592)
- Display failures on VM boot order modification [\#560](https://github.com/vatesfr/xo-web/issues/560)
- `vm.setBootOrder\(\)` should throw errors on failures \(non-HVM VMs\) [\#559](https://github.com/vatesfr/xo-web/issues/559)
- Hide boot order form for non-HVM VMs [\#558](https://github.com/vatesfr/xo-web/issues/558)
- Allow editing PV args even when empty \(but only for PV VMs\) [\#557](https://github.com/vatesfr/xo-web/issues/557)
- Crashes when using legacy event system [\#556](https://github.com/vatesfr/xo-web/issues/556)
- XenServer patches check error for 6.1 [\#555](https://github.com/vatesfr/xo-web/issues/555)
- activation plugin xo-server-transport-email [\#553](https://github.com/vatesfr/xo-web/issues/553)
- Server error with JSON on 32 bits Dom0 [\#552](https://github.com/vatesfr/xo-web/issues/552)
- Cloud Config drive shouldn't be created on default SR [\#548](https://github.com/vatesfr/xo-web/issues/548)
- Deep properties cannot be edited in plugins configuration form [\#521](https://github.com/vatesfr/xo-web/issues/521)
- Aborted VM export should cancel the operation [\#490](https://github.com/vatesfr/xo-web/issues/490)
- VM missing with same UUID after an inter-pool migration [\#284](https://github.com/vatesfr/xo-web/issues/284)
## **4.10.0** (2015-11-27)
Job management, email notifications, CoreOS/Docker, Quiesce snapshots...
### Enhancements
- Job management ([xo-web#487](https://github.com/vatesfr/xo-web/issues/487))
- Patch upload on all connected servers ([xo-web#168](https://github.com/vatesfr/xo-web/issues/168))
- Emergency shutdown ([xo-web#185](https://github.com/vatesfr/xo-web/issues/185))
- CoreOS/docker template install ([xo-web#246](https://github.com/vatesfr/xo-web/issues/246))
- Email for backups ([xo-web#308](https://github.com/vatesfr/xo-web/issues/308))
- Console Clipboard ([xo-web#408](https://github.com/vatesfr/xo-web/issues/408))
- Logs from CLI ([xo-web#486](https://github.com/vatesfr/xo-web/issues/486))
- Save disconnected servers ([xo-web#489](https://github.com/vatesfr/xo-web/issues/489))
- Snapshot with quiesce ([xo-web#491](https://github.com/vatesfr/xo-web/issues/491))
- Start VM in reovery mode ([xo-web#495](https://github.com/vatesfr/xo-web/issues/495))
- Username in logs ([xo-web#498](https://github.com/vatesfr/xo-web/issues/498))
- Delete associated tokens with user ([xo-web#500](https://github.com/vatesfr/xo-web/issues/500))
- Validate plugin configuration ([xo-web#503](https://github.com/vatesfr/xo-web/issues/503))
- Avoid non configured plugins to be loaded ([xo-web#504](https://github.com/vatesfr/xo-web/issues/504))
- Verbose API logs if configured ([xo-web#505](https://github.com/vatesfr/xo-web/issues/505))
- Better backup overview ([xo-web#512](https://github.com/vatesfr/xo-web/issues/512))
- VM auto power on ([xo-web#519](https://github.com/vatesfr/xo-web/issues/519))
- Title property supported in config schema ([xo-web#522](https://github.com/vatesfr/xo-web/issues/522))
- Start VM export only when necessary ([xo-web#534](https://github.com/vatesfr/xo-web/issues/534))
- Input type should be number ([xo-web#538](https://github.com/vatesfr/xo-web/issues/538))
### Bug fixes
- Numbers/int support in plugins config ([xo-web#531](https://github.com/vatesfr/xo-web/issues/531))
- Boolean support in plugins config ([xo-web#528](https://github.com/vatesfr/xo-web/issues/528))
- Keyboard unusable outside console ([xo-web#513](https://github.com/vatesfr/xo-web/issues/513))
- UsernameField for SAML ([xo-web#513](https://github.com/vatesfr/xo-web/issues/513))
- Wrong display of "no plugin found" ([xo-web#508](https://github.com/vatesfr/xo-web/issues/508))
- Bower build error ([xo-web#488](https://github.com/vatesfr/xo-web/issues/488))
- VM cloning should require SR permission ([xo-web#472](https://github.com/vatesfr/xo-web/issues/472))
- Xen tools status ([xo-web#471](https://github.com/vatesfr/xo-web/issues/471))
- Can't delete ghost user ([xo-web#464](https://github.com/vatesfr/xo-web/issues/464))
- Stats with old versions of Node ([xo-web#463](https://github.com/vatesfr/xo-web/issues/463))
## **4.9.0** (2015-11-13)
Automated DR, restore backup, VM copy
### Enhancements
- DR: schedule VM export on other host ([xo-web#447](https://github.com/vatesfr/xo-web/issues/447))
- Scheduler logs ([xo-web#390](https://github.com/vatesfr/xo-web/issues/390) and [xo-web#477](https://github.com/vatesfr/xo-web/issues/477))
- Restore backups ([xo-web#450](https://github.com/vatesfr/xo-web/issues/350))
- Disable backup compression ([xo-web#467](https://github.com/vatesfr/xo-web/issues/467))
- Copy VM to another SR (even remote) ([xo-web#475](https://github.com/vatesfr/xo-web/issues/475))
- VM stats without time sync ([xo-web#460](https://github.com/vatesfr/xo-web/issues/460))
- Stats perfs for high CPU numbers ([xo-web#461](https://github.com/vatesfr/xo-web/issues/461))
### Bug fixes
- Rolling backup bug ([xo-web#484](https://github.com/vatesfr/xo-web/issues/484))
- vCPUs/CPUs inversion in dashboard ([xo-web#481](https://github.com/vatesfr/xo-web/issues/481))
- Machine to template ([xo-web#459](https://github.com/vatesfr/xo-web/issues/459))
### Misc
- Console fix in XenServer ([xo-web#406](https://github.com/vatesfr/xo-web/issues/406))
## **4.8.0** (2015-10-29)
Fully automated patch system, ACLs inheritance, stats performance improved.
### Enhancements
- ACLs inheritance ([xo-web#279](https://github.com/vatesfr/xo-web/issues/279))
- Patch automatically all missing updates ([xo-web#281](https://github.com/vatesfr/xo-web/issues/281))
- Intelligent stats polling ([xo-web#432](https://github.com/vatesfr/xo-web/issues/432))
- Cache latest result of stats request ([xo-web#431](https://github.com/vatesfr/xo-web/issues/431))
- Improve stats polling on multiple objects ([xo-web#433](https://github.com/vatesfr/xo-web/issues/433))
- Patch upload task should display the patch name ([xo-web#449](https://github.com/vatesfr/xo-web/issues/449))
- Backup filename for Windows ([xo-web#448](https://github.com/vatesfr/xo-web/issues/448))
- Specific distro icons ([xo-web#446](https://github.com/vatesfr/xo-web/issues/446))
- PXE boot for HVM ([xo-web#436](https://github.com/vatesfr/xo-web/issues/436))
- Favicon display before sign in ([xo-web#428](https://github.com/vatesfr/xo-web/issues/428))
- Registration renewal ([xo-web#424](https://github.com/vatesfr/xo-web/issues/424))
- Reconnect to the host if pool merge fails ([xo-web#403](https://github.com/vatesfr/xo-web/issues/403))
- Avoid brute force login ([xo-web#339](https://github.com/vatesfr/xo-web/issues/339))
- Missing FreeBSD icon ([xo-web#136](https://github.com/vatesfr/xo-web/issues/136))
- Hide halted objects in the Health view ([xo-web#457](https://github.com/vatesfr/xo-web/issues/457))
- Click on "Remember me" label ([xo-web#438](https://github.com/vatesfr/xo-web/issues/438))
### Bug fixes
- Pool patches in multiple pools not displayed ([xo-web#442](https://github.com/vatesfr/xo-web/issues/442))
- VM Import crashes with Chrome ([xo-web#427](https://github.com/vatesfr/xo-web/issues/427))
- Cannot open a direct link ([xo-web#371](https://github.com/vatesfr/xo-web/issues/371))
- Patch display edge case ([xo-web#309](https://github.com/vatesfr/xo-web/issues/309))
- VM snapshot should require user permission on SR ([xo-web#429](https://github.com/vatesfr/xo-web/issues/429))
## **4.7.0** (2015-10-12)
Plugin config management and browser notifications.
### Enhancements
- Plugin management in the web interface ([xo-web#352](https://github.com/vatesfr/xo-web/issues/352))
- Browser notifications ([xo-web#402](https://github.com/vatesfr/xo-web/issues/402))
- Graph selector ([xo-web#400](https://github.com/vatesfr/xo-web/issues/400))
- Circle packing visualization ([xo-web#374](https://github.com/vatesfr/xo-web/issues/374))
- Password generation ([xo-web#397](https://github.com/vatesfr/xo-web/issues/397))
- Password reveal during user creation ([xo-web#396](https://github.com/vatesfr/xo-web/issues/396))
- Add host to a pool ([xo-web#62](https://github.com/vatesfr/xo-web/issues/62))
- Better modal when removing a host from a pool ([xo-web#405](https://github.com/vatesfr/xo-web/issues/405))
- Drop focus on CD/ISO selector ([xo-web#290](https://github.com/vatesfr/xo-web/issues/290))
- Allow non persistent session ([xo-web#243](https://github.com/vatesfr/xo-web/issues/243))
### Bug fixes
- VM export permission corrected ([xo-web#410](https://github.com/vatesfr/xo-web/issues/410))
- Proper host removal in a pool ([xo-web#402](https://github.com/vatesfr/xo-web/issues/402))
- Sub-optimal tooltip placement ([xo-web#421](https://github.com/vatesfr/xo-web/issues/421))
- VM migrate host incorrect target ([xo-web#419](https://github.com/vatesfr/xo-web/issues/419))
- Alone host can't leave its pool ([xo-web#414](https://github.com/vatesfr/xo-web/issues/414))
## **4.6.0** (2015-09-25)
Tags management and new visualization.
### Enhancements
- Multigraph for correlation ([xo-web#358](https://github.com/vatesfr/xo-web/issues/358))
- Tags management ([xo-web#367](https://github.com/vatesfr/xo-web/issues/367))
- Google Provider for authentication ([xo-web#363](https://github.com/vatesfr/xo-web/issues/363))
- Password change for users ([xo-web#362](https://github.com/vatesfr/xo-web/issues/362))
- Better live migration process ([xo-web#237](https://github.com/vatesfr/xo-web/issues/237))
- VDI search filter in SR view ([xo-web#222](https://github.com/vatesfr/xo-web/issues/222))
- PV args during VM creation ([xo-web#112](https://github.com/vatesfr/xo-web/issues/330))
- PV args management ([xo-web#394](https://github.com/vatesfr/xo-web/issues/394))
- Confirmation dialog on important actions ([xo-web#350](https://github.com/vatesfr/xo-web/issues/350))
- New favicon ([xo-web#369](https://github.com/vatesfr/xo-web/issues/369))
- Filename of VM for exports ([xo-web#370](https://github.com/vatesfr/xo-web/issues/370))
- ACLs rights edited on the fly ([xo-web#323](https://github.com/vatesfr/xo-web/issues/323))
- Heatmap values now human readable ([xo-web#342](https://github.com/vatesfr/xo-web/issues/342))
### Bug fixes
- Export backup fails if no tags specified ([xo-web#383](https://github.com/vatesfr/xo-web/issues/383))
- Wrong login give an obscure error message ([xo-web#373](https://github.com/vatesfr/xo-web/issues/373))
- Update view is broken during updates ([xo-web#356](https://github.com/vatesfr/xo-web/issues/356))
- Settings/dashboard menu incorrect display ([xo-web#357](https://github.com/vatesfr/xo-web/issues/357))
- Console View Not refreshing if the VM restart ([xo-web#107](https://github.com/vatesfr/xo-web/issues/107))
## **4.5.1** (2015-09-16)
An issue in `xo-web` with the VM view.
### Bug fix
- Attach disk/new disk/create interface is broken ([xo-web#378](https://github.com/vatesfr/xo-web/issues/378))
## **4.5.0** (2015-09-11)
A new dataviz (parallel coord), a new provider (GitHub) and faster consoles.
### Enhancements
- Parallel coordinates view ([xo-web#333](https://github.com/vatesfr/xo-web/issues/333))
- Faster consoles ([xo-web#337](https://github.com/vatesfr/xo-web/issues/337))
- Disable/hide button ([xo-web#268](https://github.com/vatesfr/xo-web/issues/268))
- More details on missing-guest-tools ([xo-web#304](https://github.com/vatesfr/xo-web/issues/304))
- Scheduler meta data export ([xo-web#315](https://github.com/vatesfr/xo-web/issues/315))
- Better heatmap ([xo-web#330](https://github.com/vatesfr/xo-web/issues/330))
- Faster dashboard ([xo-web#331](https://github.com/vatesfr/xo-web/issues/331))
- Faster sunburst ([xo-web#332](https://github.com/vatesfr/xo-web/issues/332))
- GitHub provider for auth ([xo-web#334](https://github.com/vatesfr/xo-web/issues/334))
- Filter networks for users ([xo-web#347](https://github.com/vatesfr/xo-web/issues/347))
- Add networks in ACLs ([xo-web#348](https://github.com/vatesfr/xo-web/issues/348))
- Better looking login page ([xo-web#341](https://github.com/vatesfr/xo-web/issues/341))
- Real time dataviz (dashboard) ([xo-web#349](https://github.com/vatesfr/xo-web/issues/349))
### Bug fixes
- Typo in dashboard ([xo-web#355](https://github.com/vatesfr/xo-web/issues/355))
- Global RAM usage fix ([xo-web#356](https://github.com/vatesfr/xo-web/issues/356))
- Re-allowing XO behind a reverse proxy ([xo-web#361](https://github.com/vatesfr/xo-web/issues/361))
## **4.4.0** (2015-08-28)
SSO and Dataviz are the main features for this release.
### Enhancements
- Dataviz storage usage ([xo-web#311](https://github.com/vatesfr/xo-web/issues/311))
- Heatmap in health view ([xo-web#329](https://github.com/vatesfr/xo-web/issues/329))
- SSO for SAML and other providers ([xo-web#327](https://github.com/vatesfr/xo-web/issues/327))
- Better UI for ACL objects attribution ([xo-web#320](https://github.com/vatesfr/xo-web/issues/320))
- Refresh the browser after an update ([xo-web#318](https://github.com/vatesfr/xo-web/issues/318))
- Clean CSS and Flexbox usage ([xo-web#239](https://github.com/vatesfr/xo-web/issues/239))
### Bug fixes
- Admin only accessible views ([xo-web#328](https://github.com/vatesfr/xo-web/issues/328))
- Hide "base copy" VDIs ([xo-web#324](https://github.com/vatesfr/xo-web/issues/324))
- ACLs on VIFs for non-admins ([xo-web#322](https://github.com/vatesfr/xo-web/issues/322))
- Updater display problems ([xo-web#313](https://github.com/vatesfr/xo-web/issues/313))
## **4.3.0** (2015-07-22)
Scheduler for rolling backups
### Enhancements
- Rolling backup scheduler ([xo-web#278](https://github.com/vatesfr/xo-web/issues/278))
- Clean snapshots of removed VMs ([xo-web#301](https://github.com/vatesfr/xo-web/issues/301))
### Bug fixes
- VM export ([xo-web#307](https://github.com/vatesfr/xo-web/issues/307))
- Remove VM VDIs ([xo-web#303](https://github.com/vatesfr/xo-web/issues/303))
- Pagination fails ([xo-web#302](https://github.com/vatesfr/xo-web/issues/302))
## **4.2.0** (2015-06-29)
Huge performance boost, scheduler for rolling snapshots and backward compatibility for XS 5.x series
### Enhancements
- Rolling snapshots scheduler ([xo-web#176](https://github.com/vatesfr/xo-web/issues/176))
- Huge perf boost ([xen-api#1](https://github.com/julien-f/js-xen-api/issues/1))
- Backward compatibility ([xo-web#296](https://github.com/vatesfr/xo-web/issues/296))
### Bug fixes
- VDI attached on a VM missing in SR view ([xo-web#294](https://github.com/vatesfr/xo-web/issues/294))
- Better VM creation process ([xo-web#292](https://github.com/vatesfr/xo-web/issues/292))
## **4.1.0** (2015-06-10)
Add the drag'n drop support from VM live migration, better ACLs groups UI.
@@ -15,7 +340,8 @@ Add the drag'n drop support from VM live migration, better ACLs groups UI.
- Convert to template displayed when the VM is not halted ([xo-web#286](https://github.com/vatesfr/xo-web/issues/286))
- Lost some data when refresh some views ([xo-web#271](https://github.com/vatesfr/xo-web/issues/271))
- Suspend button don't trigger any permission message ([xo-web#270](https://github.com/vatesfr/xo-web/issues/270))
- create network interfaces shouldn't call xoApi directly ([xo-web#269](https://github.com/vatesfr/xo-web/issues/269))
- Create network interfaces shouldn't call xoApi directly ([xo-web#269](https://github.com/vatesfr/xo-web/issues/269))
- Don't plug automatically a disk or a VIF if the VM is not running ([xo-web#287](https://github.com/vatesfr/xo-web/issues/287))
## **4.0.2** (2015-06-01)

View File

@@ -38,8 +38,8 @@ Otherwise, please consider using the [bugtracker of the general repository](http
## Process for new release
```bash
# Switch to the master branch.
git checkout master
# Switch to the stable branch.
git checkout stable
# Fetches latest changes.
git pull --ff-only
@@ -53,12 +53,12 @@ npm version minor
# Go back to the next-release branch.
git checkout next-release
# Fetches the last changes (the merge and version bump) from master to
# Fetches the last changes (the merge and version bump) from stable to
# next-release.
git merge --ff-only master
git merge --ff-only stable
# Push the changes on git.
git push --follow-tags origin master next-release
git push --follow-tags origin stable next-release
# Publish this release to npm.
npm publish

View File

@@ -15,18 +15,21 @@ import xoFilters from 'xo-filters'
import xoServices from 'xo-services'
import aboutState from './modules/about'
import backupState from './modules/backup'
import consoleState from './modules/console'
import dashboardState from './modules/dashboard'
import deleteVmsState from './modules/delete-vms'
import genericModalState from './modules/generic-modal'
import hostState from './modules/host'
import listState from './modules/list'
import loginState from './modules/login'
import migrateVmState from './modules/migrate-vm'
import navbarState from './modules/navbar'
import newSrState from './modules/new-sr'
import newVmState from './modules/new-vm'
import poolState from './modules/pool'
import settingsState from './modules/settings'
import srState from './modules/sr'
import taskScheduler from './modules/task-scheduler'
import treeState from './modules/tree'
import updater from './modules/updater'
import vmState from './modules/vm'
@@ -49,18 +52,21 @@ export default angular.module('xoWebApp', [
xoServices,
aboutState,
backupState,
consoleState,
dashboardState,
deleteVmsState,
genericModalState,
hostState,
listState,
loginState,
migrateVmState,
navbarState,
newSrState,
newVmState,
poolState,
settingsState,
srState,
taskScheduler,
treeState,
updater,
vmState,
@@ -70,7 +76,7 @@ export default angular.module('xoWebApp', [
// Prevent Angular.js from mangling exception stack (interfere with
// source maps).
.factory('$exceptionHandler', () => function (exception) {
throw exception
console.log(exception && exception.stack || exception)
})
.config(function (
@@ -120,26 +126,28 @@ export default angular.module('xoWebApp', [
updater,
xoApi
) {
$rootScope.$on('$stateChangeStart', function (event, state, stateParams) {
let {user} = xoApi
let loggedIn = !!user
let requestedStateName, requestedStateParams
if (state.name === 'login') {
if (loggedIn) {
event.preventDefault()
$rootScope.$watch(() => xoApi.user, (user, previous) => {
// The user just signed in.
if (user && !previous) {
if (requestedStateName) {
$state.go(requestedStateName, requestedStateParams)
requestedStateName = requestedStateParams = null
} else {
$state.go('index')
}
return
}
})
if (!loggedIn) {
$rootScope.$on('$stateChangeStart', function (event, state, stateParams, fromState) {
const { user } = xoApi
if (!user) {
event.preventDefault()
// FIXME: find a better way to pass info to the login controller.
$rootScope._login = { state, stateParams }
requestedStateName = state.name
requestedStateParams = stateParams
$state.go('login')
return
}
@@ -147,23 +155,28 @@ export default angular.module('xoWebApp', [
return
}
// The user must have the `admin` permission to access the
// settings pages.
if (/^settings\..*|tree$/.test(state.name)) {
function forbidState () {
event.preventDefault()
notify.error({
title: 'Restricted area',
message: 'You do not have the permission to view this page'
})
if (fromState.url === '^') {
$state.go('index')
}
}
let {id} = stateParams
if (id && !xoApi.canAccess(id)) {
event.preventDefault()
notify.error({
title: 'Restricted area',
message: 'You do not have the permission to view this page'
})
// Some pages requires the admin permission.
if (state.data && state.data.requireAdmin) {
forbidState()
return
}
const { id } = stateParams
if (id && !xoApi.canInteract(id, 'view')) {
forbidState()
return
}
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
app/images/circle1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
app/images/circle2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
app/images/parcoords.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
app/images/sunburst.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
app/images/sunburst2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -1,13 +1,13 @@
//- TODO: lots of stuff.
.grid
.grid-sm
.panel.panel-default
p.page-title About Xen Orchestra
p.text-center ({{pkg.name}} {{pkg.version}})
.grid
.grid-sm
//- Vates
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-lightbulb-o(style="color: #e25440;")
i.fa.fa-lightbulb-o
| Vates
.panel-body
p.text-center
@@ -22,7 +22,7 @@
//- Open Source
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-thumbs-up(style="color: #e25440;")
i.fa.fa-thumbs-up
| Open Source
.panel-body
p.text-center
@@ -37,7 +37,7 @@
//- Pro support
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-truck(style="color: #e25440;")
i.fa.fa-truck
| Pro Support Delivered
.panel-body
p.text-center
@@ -45,6 +45,6 @@
p.text-center
img(src="images/support.png")
p.text-center
a.btn.btn-primary(href="https://xen-orchestra.com/services/")
a.btn.btn-primary(href="https://vates.fr/services.html")
i.fa.fa-envelope
| Get services

View File

@@ -0,0 +1,268 @@
import angular from 'angular'
import filter from 'lodash.filter'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import map from 'lodash.map'
import prettyCron from 'prettycron'
import size from 'lodash.size'
import trim from 'lodash.trim'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import view from './view'
// ====================================================================
export default angular.module('backup.backup', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.backup', {
url: '/backup/:id',
controller: 'BackupCtrl as ctrl',
template: view
})
})
.controller('BackupCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
const JOBKEY = 'rollingBackup'
this.ready = false
this.running = {}
this.comesForEditing = $stateParams.id
this.scheduleApi = {}
this.formData = {}
const refreshRemotes = () => {
const selectRemoteId = this.formData.remote && this.formData.remote.id
return xo.remote.getAll()
.then(remotes => {
const r = {}
forEach(remotes, remote => {
r[remote.id] = remote
})
this.remotes = r
if (selectRemoteId) {
this.formData.remote = this.remotes[selectRemoteId]
}
})
}
const refreshSchedules = () => {
return xo.schedule.getAll()
.then(schedules => {
const s = {}
forEach(schedules, schedule => {
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
})
this.schedules = s
})
}
const refreshJobs = () => {
return xo.job.getAll()
.then(jobs => {
const j = {}
forEach(jobs, job => {
j[job.id] = job
})
this.jobs = j
})
}
const refresh = () => refreshRemotes().then(refreshJobs).then(refreshSchedules)
this.getReady = () => refresh().then(() => this.ready = true)
this.getReady()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => $interval.cancel(interval))
const toggleState = (toggle, state) => {
const selectedVms = this.formData.selectedVms.slice()
if (toggle) {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
forEach(vms, vm => {
if (vm.power_state === state) {
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
}
})
this.formData.selectedVms = selectedVms
} else {
const keptVms = []
for (let index in this.formData.selectedVms) {
if (this.formData.selectedVms[index].power_state !== state) {
keptVms.push(this.formData.selectedVms[index])
}
}
this.formData.selectedVms = keptVms
}
}
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
this.edit = schedule => {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
const job = this.jobs[schedule.job]
const selectedVms = []
forEach(job.paramsVector.items[0].values, value => {
const vm = find(vms, vm => vm.id === value.id)
vm && selectedVms.push(vm)
})
const tag = job.paramsVector.items[0].values[0].tag
const depth = job.paramsVector.items[0].values[0].depth
const cronPattern = schedule.cron
const remoteId = job.paramsVector.items[0].values[0].remoteId
const onlyMetadata = job.paramsVector.items[0].values[0].onlyMetadata || false
let compress = job.paramsVector.items[0].values[0].compress
if (compress === undefined) {
compress = true // Default value
}
this.resetData()
this.formData.selectedVms = selectedVms
this.formData.tag = tag
this.formData.depth = depth
this.formData.scheduleId = schedule.id
this.formData.remote = this.remotes[remoteId]
this.formData.disableCompression = !compress
this.formData.onlyMetadata = onlyMetadata
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, remoteId, tag, depth, cron, enabled, onlyMetadata, disableCompression) => {
if (!vms.length) {
notify.warning({
title: 'No Vms selected',
message: 'Choose VMs to backup'
})
return
}
const _save = (id === undefined) ? saveNew(vms, remoteId, tag, depth, cron, enabled, onlyMetadata, disableCompression) : save(id, vms, remoteId, tag, depth, cron, onlyMetadata, disableCompression)
return _save
.then(() => {
notify.info({
title: 'Backup',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(refresh)
}
const save = (id, vms, remoteId, tag, depth, cron, onlyMetadata, disableCompression) => {
const schedule = this.schedules[id]
const job = this.jobs[schedule.job]
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
remoteId,
tag,
depth,
onlyMetadata,
compress: !disableCompression
})
})
job.paramsVector.items[0].values = values
return xo.job.set(job)
.then(response => {
if (response) {
return xo.schedule.set(schedule.id, undefined, cron, undefined)
} else {
notify.error({
title: 'Update schedule',
message: 'Job updating failed'
})
throw new Error('Job updating failed')
}
})
}
const saveNew = (vms, remoteId, tag, depth, cron, enabled, onlyMetadata, disableCompression) => {
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
remoteId,
tag,
depth,
onlyMetadata,
compress: !disableCompression
})
})
const job = {
type: 'call',
key: JOBKEY,
method: 'vm.rollingBackup',
paramsVector: {
type: 'crossProduct',
items: [{
type: 'set',
values
}]
}
}
return xo.job.create(job)
.then(jobId => xo.schedule.create(jobId, cron, enabled))
}
this.delete = schedule => {
let jobId = schedule.job
return xo.schedule.delete(schedule.id)
.then(() => xo.job.delete(jobId))
.finally(() => {
if (this.formData.scheduleId === schedule.id) {
this.resetData()
}
refresh()
})
}
this.run = schedule => {
this.running[schedule.id] = true
notify.info({
title: 'Run Job',
message: 'One shot running started. See overview for logs.'
})
const id = schedule.job
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
}
this.sanitizePath = (...paths) => (paths[0] && paths[0].charAt(0) === '/' && '/' || '') + filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/')
this.resetData = () => {
this.formData.allRunning = false
this.formData.allHalted = false
this.formData.selectedVms = []
this.formData.scheduleId = undefined
this.formData.tag = undefined
this.formData.path = undefined
this.formData.depth = undefined
this.formData.enabled = false
this.formData.remote = undefined
this.formData.onlyMetadata = false
this.formData.disableCompression = false
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
}
this.size = size
this.prettyCron = prettyCron.toString.bind(prettyCron)
if (!this.comesForEditing) {
refresh()
} else {
refresh()
.then(() => {
this.edit(this.schedules[this.comesForEditing])
delete this.comesForEditing
})
}
this.resetData()
this.objects = xoApi.all
})
// A module exports its name.
.name

View File

@@ -0,0 +1,146 @@
.panel.panel-default
p.page-title
i.fa.fa-download(style="color: #e25440;")
| Backup
form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.remote.id, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled, ctrl.formData.onlyMetadata, ctrl.formData.disableCompression)')
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.xo-icon-vm
| VMs to backup
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
.container-fluid(ng-if = 'ctrl.formData')
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Backup
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Backup ID {{ ctrl.formData.scheduleId }}
.form-group
label.control-label.col-md-2(for = 'tag') Tag
.col-md-10
input#tag.form-control(form = 'backupform', ng-model = 'ctrl.formData.tag', placeholder = 'Back-up tag', required)
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
label.control-label.col-md-2(for = 'vmlist') VMs
.col-md-8
ui-select(form = 'backupform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose VMs to backup')
i.xo-icon-working(ng-if="isVMWorking($item)")
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
| {{$item.name_label}}
span(ng-if="$item.$container")
| ({{($item.$container | resolve).name_label}})
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
div
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
| {{vm.name_label}}
span(ng-if="vm.$container")
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
.col-md-2
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
span.fa-stack
i.xo-icon-running.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
span.fa-stack
i.xo-icon-halted.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
.form-group
label.control-label.col-md-2(for = 'depth') Depth
.col-md-10
input#depth.form-control(form = 'backupform', ng-model = 'ctrl.formData.depth', placeholder = 'How many backups to rollover', type = 'number', min = '1', required)
.form-group
label.control-label.col-md-2(for = 'remote') Remote
.col-md-10
select#remote.form-control(form = 'backupform', ng-options = 'remote.name group by remote.type for remote in ctrl.remotes', ng-model = 'ctrl.formData.remote' required)
option(value = ''): em -- Choose a file system remote point --
.form-group
.col-md-10.col-md-offset-2
a(ui-sref = 'backup.remote')
i.fa.fa-pencil
|   Manage your remote stores
.form-group
label.control-label.col-md-2(for = 'onlyMetadata')
input#onlyMetadata(form = 'backupform', ng-model = 'ctrl.formData.onlyMetadata', type = 'checkbox')
.help-block.col-md-10 Only MetaData (no disks export)
.form-group
label.control-label.col-md-2(for = 'onlyMetadata')
input#disableCompression(form = 'backupform', ng-model = 'ctrl.formData.disableCompression', type = 'checkbox')
.help-block.col-md-10 Disable compression
.form-group(ng-if = '!ctrl.formData.scheduleId')
label.control-label.col-md-2(for = 'enabled')
input#enabled(form = 'backupform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
.help-block.col-md-10 Enable immediatly after creation
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-clock-o
| Schedule
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
.grid-sm
.panel.panel-default
.panel-body
fieldset.center(ng-disabled = '!ctrl.ready')
button.btn.btn-lg.btn-primary(form = 'backupform', type = 'submit')
i.fa.fa-clock-o
|  
i.fa.fa-arrow-right
|  
i.fa.fa-database
|  Save 
|  
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetData()')
|  Reset 
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-list-ul
| Schedules
.panel-body
.text-center(ng-if = '!ctrl.schedules'): i.xo-icon-loading
.text-center(ng-if = 'ctrl.schedules && !ctrl.size(ctrl.schedules)') No scheduled jobs
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.size(ctrl.schedules)')
tr
th ID
th Tag
th.hidden-xs.hidden-sm VMs to backup
th.hidden-xs Remote
th.hidden-xs Depth
th.hidden-xs Scheduling
th.hidden-xs Only MetaData
th.hidden-xs Compression DISABLED
th Enabled now
th
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
td {{ schedule.id }}
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
td.hidden-xs.hidden-sm
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].id | resolve).name_label }}
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs 
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
div(collapse = '!unCollapsed')
br
ul.list-group
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
span(ng-if = 'item.id | resolve') {{ (item.id | resolve).name_label }}
span(ng-if = '(item.id | resolve).$container')  ({{ ((item.id | resolve).$container | resolve).name_label }})
td.hidden-xs
strong: a(ui-sref = 'scheduler.remote') {{ ctrl.remotes[ctrl.jobs[schedule.job].paramsVector.items[0].values[0].remoteId].name }}
td.hidden-xs {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
td.hidden-xs.text-center
i.fa.fa-check(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values[0].onlyMetadata')
td.hidden-xs.text-center
i.fa.fa-check(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values[0].compress === false')
td.text-center
i.fa.fa-check(ng-if = 'schedule.enabled')
td.text-right
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
|  
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|  
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o

View File

@@ -0,0 +1,222 @@
import angular from 'angular'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import later from 'later'
import prettyCron from 'prettycron'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
later.date.localTime()
import view from './view'
// ====================================================================
export default angular.module('backup.continuousReplication', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.continuousReplication', {
url: '/continuous-replication/:id',
controller: 'ContinuousReplicationCtrl as ctrl',
template: view
})
})
.controller('ContinuousReplicationCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter, bytesToSizeFilter) {
const JOBKEY = 'continuousReplication'
this.ready = false
this.running = {}
this.comesForEditing = $stateParams.id
this.scheduleApi = {}
this.formData = {}
const refreshSchedules = () => xo.schedule.getAll()
.then(schedules => {
const s = {}
forEach(schedules, schedule => {
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
})
this.schedules = s
})
const refreshJobs = () => xo.job.getAll()
.then(jobs => {
const j = {}
forEach(jobs, job => {
j[job.id] = job
})
this.jobs = j
})
const refresh = () => refreshJobs().then(refreshSchedules)
const getReady = () => refresh().then(() => this.ready = true)
getReady()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => $interval.cancel(interval))
const toggleState = (toggle, state) => {
const selectedVms = this.formData.selectedVms.slice()
if (toggle) {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
forEach(vms, vm => {
if (vm.power_state === state) {
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
}
})
this.formData.selectedVms = selectedVms
} else {
const keptVms = []
for (let index in this.formData.selectedVms) {
if (this.formData.selectedVms[index].power_state !== state) {
keptVms.push(this.formData.selectedVms[index])
}
}
this.formData.selectedVms = keptVms
}
}
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
this.edit = schedule => {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
const job = this.jobs[schedule.job]
const selectedVms = []
forEach(job.paramsVector.items[0].values, value => {
const vm = find(vms, vm => vm.id === value.vm)
vm && selectedVms.push(vm)
})
const tag = job.paramsVector.items[0].values[0].tag
const selectedSr = xoApi.get(job.paramsVector.items[0].values[0].sr)
const cronPattern = schedule.cron
this.resetData()
// const formData = this.formData
this.formData.selectedVms = selectedVms
this.formData.tag = tag
this.formData.selectedSr = selectedSr
this.formData.scheduleId = schedule.id
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, tag, sr, cron, enabled) => {
if (!vms.length) {
notify.warning({
title: 'No Vms selected',
message: 'Choose VMs to copy'
})
return
}
const _save = (id === undefined) ? saveNew(vms, tag, sr, cron, enabled) : save(id, vms, tag, sr, cron)
return _save
.then(() => {
notify.info({
title: 'Continuous Replication',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(refresh)
}
const save = (id, vms, tag, sr, cron) => {
const schedule = this.schedules[id]
const job = this.jobs[schedule.job]
const values = []
forEach(vms, vm => {
values.push({vm: vm.id, tag, sr: sr.id})
})
job.paramsVector.items[0].values = values
return xo.job.set(job)
.then(response => {
if (response) {
return xo.schedule.set(schedule.id, undefined, cron, undefined)
} else {
notify.error({
title: 'Update schedule',
message: 'Job updating failed'
})
throw new Error('Job updating failed')
}
})
}
const saveNew = (vms, tag, sr, cron, enabled) => {
const values = []
forEach(vms, vm => {
values.push({vm: vm.id, tag, sr: sr.id})
})
const job = {
type: 'call',
key: JOBKEY,
method: 'vm.deltaCopy',
paramsVector: {
type: 'crossProduct',
items: [{
type: 'set',
values
}]
}
}
return xo.job.create(job)
.then(jobId => xo.schedule.create(jobId, cron, enabled))
}
this.delete = schedule => {
let jobId = schedule.job
return xo.schedule.delete(schedule.id)
.then(() => xo.job.delete(jobId))
.finally(() => {
if (this.formData.scheduleId === schedule.id) {
this.resetData()
}
refresh()
})
}
this.run = schedule => {
this.running[schedule.id] = true
notify.info({
title: 'Run Job',
message: 'One shot running started. See overview for logs.'
})
const id = schedule.job
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
}
this.inTargetPool = vm => vm.$poolId === (this.formData.selectedSr && this.formData.selectedSr.$poolId)
this.resetData = () => {
this.formData.allRunning = false
this.formData.allHalted = false
this.formData.selectedVms = []
this.formData.scheduleId = undefined
this.formData.tag = undefined
this.formData.selectedSr = undefined
this.formData.enabled = false
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
}
this.collectionLength = col => Object.keys(col).length
this.prettyCron = prettyCron.toString.bind(prettyCron)
if (!this.comesForEditing) {
refresh()
} else {
refresh()
.then(() => {
this.edit(this.schedules[this.comesForEditing])
delete this.comesForEditing
})
}
this.resetData()
this.objects = xoApi.all
})
// A module exports its name.
.name

View File

@@ -0,0 +1,135 @@
.panel.panel-default
p.page-title
i.fa.fa-map-signs(style="color: #e25440;")
| Continuous Replication
form#ciform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.tag, ctrl.formData.selectedSr, ctrl.formData.cronPattern, ctrl.formData.enabled)')
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.xo-icon-vm(style='color: #e25440;')
| VMs to copy
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
.container-fluid(ng-if = 'ctrl.formData')
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Continuous Replication
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Continuous Replication ID {{ ctrl.formData.scheduleId }}
.form-group
label.control-label.col-md-2(for = 'tag') Tag
.col-md-10
input#tag.form-control(form = 'ciform', ng-model = 'ctrl.formData.tag', placeholder = 'VM copy tag', required)
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
label.control-label.col-md-2(for = 'vmlist') VMs
.col-md-8
ui-select#vmlist(form = 'ciform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose VMs to copy')
span(ng-class = '{"bg-danger": ctrl.inTargetPool($item)}')
i.xo-icon-working(ng-if="isVMWorking($item)")
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
| {{$item.name_label}}
span(ng-if="$item.$container")
| ({{($item.$container | resolve).name_label}})
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
div
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
| {{vm.name_label}}
span(ng-if="vm.$container")
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
.col-md-2
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
input.hidden(form = 'ciform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
span.fa-stack
i.xo-icon-running.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
input.hidden(form = 'ciform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
span.fa-stack
i.xo-icon-halted.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
.form-group(ng-if = '(ctrl.formData.selectedVms | filter:ctrl.inTargetPool).length')
.col-md-offset-2.col-md-10
.alert.alert-warning
i.fa.fa-exclamation-triangle
|  At the moment, the selected VMs displayed in red are in the copy target pool.
.form-group
label.control-label.col-md-2(for = 'sr') To SR
.col-md-10
ui-select#sr(form = 'ciform', ng-model = 'ctrl.formData.selectedSr', required)
ui-select-match(placeholder = 'Choose destination SR')
i(class="xo-icon-sr")
| {{$select.selected.name_label}}
span(ng-if="$select.selected.$container")
| ({{($select.selected.$container | resolve).name_label}})
ui-select-choices(repeat = 'sr in ctrl.objects | selectHighLevel | filter:{type: "sr", content_type: "!iso"} | filter:$select.search | orderBy:["$container", "name_label"] track by sr.id')
div
i(class="xo-icon-sr")
| {{sr.name_label}} ({{sr.size - sr.physical_usage | bytesToSize }})
span(ng-if="sr.$container")
| ({{(sr.$container | resolve).name_label || ((sr.$container | resolve).master | resolve).name_label}})
.form-group(ng-if = '!ctrl.formData.scheduleId')
label.control-label.col-md-2(for = 'enabled')
input#enabled(form = 'ciform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
.help-block.col-md-8 Enable immediately after creation
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-clock-o
| Schedule
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
.grid-sm
.panel.panel-default
.panel-body
fieldset.center(ng-disabled = '!ctrl.ready')
button.btn.btn-lg.btn-primary(form = 'ciform', type = 'submit')
i.fa.fa-clock-o
|  
i.fa.fa-arrow-right
|  
i.fa.fa-database
|  Save 
|  
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetData()')
|  Reset 
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-list-ul
| Schedules
.panel-body
.text-center(ng-if = '!ctrl.schedules'): i.xo-icon-loading
.text-center(ng-if = 'ctrl.schedules && !ctrl.collectionLength(ctrl.schedules)') No scheduled jobs
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
tr
th ID
th Tag
th.hidden-xs.hidden-sm VMs to Copy
th.hidden-xs To SR
th.hidden-xs Scheduling
th Enabled now
th
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
td {{ schedule.id }}
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
td.hidden-xs.hidden-sm
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].vm | resolve).name_label }}
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs 
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
div(collapse = '!unCollapsed')
ul.list-group
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
span(ng-if = 'item.vm | resolve') {{ (item.vm | resolve).name_label }}
span(ng-if = '(item.vm | resolve).$container')  ({{ ((item.vm | resolve).$container | resolve).name_label }})
td.hidden-xs {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].sr | resolve).name_label }}
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
td.text-center
i.fa.fa-check(ng-if = 'schedule.enabled')
td.text-right
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
|  
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|  
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o

View File

@@ -0,0 +1,259 @@
import angular from 'angular'
import filter from 'lodash.filter'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import map from 'lodash.map'
import prettyCron from 'prettycron'
import size from 'lodash.size'
import trim from 'lodash.trim'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import { parse } from 'xo-remote-parser'
import view from './view'
// ====================================================================
export default angular.module('backup.deltaBackup', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.deltaBackup', {
url: '/delta-backup/:id',
controller: 'DeltaBackupCtrl as ctrl',
template: view
})
})
.controller('DeltaBackupCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
const JOBKEY = 'deltaBackup'
this.ready = false
this.running = {}
this.comesForEditing = $stateParams.id
this.scheduleApi = {}
this.formData = {}
const refreshRemotes = () => {
const selectRemoteId = this.formData.remote && this.formData.remote.id
return xo.remote.getAll()
.then(remotes => {
const r = {}
forEach(remotes, remote => {
remote = parse(remote)
if (remote.type !== 'smb') {
r[remote.id] = remote
}
})
this.remotes = r
if (selectRemoteId) {
this.formData.remote = this.remotes[selectRemoteId]
}
})
}
const refreshSchedules = () => {
return xo.schedule.getAll()
.then(schedules => {
const s = {}
forEach(schedules, schedule => {
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
})
this.schedules = s
})
}
const refreshJobs = () => {
return xo.job.getAll()
.then(jobs => {
const j = {}
forEach(jobs, job => {
j[job.id] = job
})
this.jobs = j
})
}
const refresh = () => refreshRemotes().then(refreshJobs).then(refreshSchedules)
this.getReady = () => refresh().then(() => this.ready = true)
this.getReady()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => $interval.cancel(interval))
const toggleState = (toggle, state) => {
const selectedVms = this.formData.selectedVms.slice()
if (toggle) {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
forEach(vms, vm => {
if (vm.power_state === state) {
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
}
})
this.formData.selectedVms = selectedVms
} else {
const keptVms = []
for (let index in this.formData.selectedVms) {
if (this.formData.selectedVms[index].power_state !== state) {
keptVms.push(this.formData.selectedVms[index])
}
}
this.formData.selectedVms = keptVms
}
}
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
this.edit = schedule => {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
const job = this.jobs[schedule.job]
const selectedVms = []
forEach(job.paramsVector.items[0].values, value => {
const vm = find(vms, vm => vm.id === value.vm)
vm && selectedVms.push(vm)
})
const tag = job.paramsVector.items[0].values[0].tag
const depth = job.paramsVector.items[0].values[0].depth
const cronPattern = schedule.cron
const remoteId = job.paramsVector.items[0].values[0].remote
this.resetData()
this.formData.selectedVms = selectedVms
this.formData.tag = tag
this.formData.depth = depth
this.formData.scheduleId = schedule.id
this.formData.remote = this.remotes[remoteId]
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, remoteId, tag, depth, cron, enabled) => {
if (!vms.length) {
notify.warning({
title: 'No Vms selected',
message: 'Choose VMs to backup'
})
return
}
const _save = (id === undefined) ? saveNew(vms, remoteId, tag, depth, cron, enabled) : save(id, vms, remoteId, tag, depth, cron)
return _save
.then(() => {
notify.info({
title: 'Backup',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(refresh)
}
const save = (id, vms, remoteId, tag, depth, cron) => {
const schedule = this.schedules[id]
const job = this.jobs[schedule.job]
const values = []
forEach(vms, vm => {
values.push({
vm: vm.id,
remote: remoteId,
tag,
depth
})
})
job.paramsVector.items[0].values = values
return xo.job.set(job)
.then(response => {
if (response) {
return xo.schedule.set(schedule.id, undefined, cron, undefined)
} else {
notify.error({
title: 'Update schedule',
message: 'Job updating failed'
})
throw new Error('Job updating failed')
}
})
}
const saveNew = (vms, remoteId, tag, depth, cron, enabled) => {
const values = []
forEach(vms, vm => {
values.push({
vm: vm.id,
remote: remoteId,
tag,
depth
})
})
const job = {
type: 'call',
key: JOBKEY,
method: 'vm.rollingDeltaBackup',
paramsVector: {
type: 'crossProduct',
items: [{
type: 'set',
values
}]
}
}
return xo.job.create(job)
.then(jobId => xo.schedule.create(jobId, cron, enabled))
}
this.delete = schedule => {
let jobId = schedule.job
return xo.schedule.delete(schedule.id)
.then(() => xo.job.delete(jobId))
.finally(() => {
if (this.formData.scheduleId === schedule.id) {
this.resetData()
}
refresh()
})
}
this.run = schedule => {
this.running[schedule.id] = true
notify.info({
title: 'Run Job',
message: 'One shot running started. See overview for logs.'
})
const id = schedule.job
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
}
this.sanitizePath = (...paths) => (paths[0] && paths[0].charAt(0) === '/' && '/' || '') + filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/')
this.resetData = () => {
this.formData.allRunning = false
this.formData.allHalted = false
this.formData.selectedVms = []
this.formData.scheduleId = undefined
this.formData.tag = undefined
this.formData.path = undefined
this.formData.depth = undefined
this.formData.enabled = false
this.formData.remote = undefined
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
}
this.size = size
this.prettyCron = prettyCron.toString.bind(prettyCron)
if (!this.comesForEditing) {
refresh()
} else {
refresh()
.then(() => {
this.edit(this.schedules[this.comesForEditing])
delete this.comesForEditing
})
}
this.resetData()
this.objects = xoApi.all
})
// A module exports its name.
.name

View File

@@ -0,0 +1,132 @@
.panel.panel-default
p.page-title
i.fa.fa-download(style="color: #e25440;")
| Delta Backup
form#backupform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.remote.id, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled)')
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.xo-icon-vm
| VMs to backup
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
.container-fluid(ng-if = 'ctrl.formData')
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Backup
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Backup ID {{ ctrl.formData.scheduleId }}
.form-group
label.control-label.col-md-2(for = 'tag') Tag
.col-md-10
input#tag.form-control(form = 'backupform', ng-model = 'ctrl.formData.tag', placeholder = 'Back-up tag', required)
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
label.control-label.col-md-2(for = 'vmlist') VMs
.col-md-8
ui-select(form = 'backupform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose VMs to backup')
i.xo-icon-working(ng-if="isVMWorking($item)")
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
| {{$item.name_label}}
span(ng-if="$item.$container")
| ({{($item.$container | resolve).name_label}})
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
div
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
| {{vm.name_label}}
span(ng-if="vm.$container")
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
.col-md-2
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
span.fa-stack
i.xo-icon-running.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
span.fa-stack
i.xo-icon-halted.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
.form-group
label.control-label.col-md-2(for = 'depth') Depth
.col-md-10
input#depth.form-control(form = 'backupform', ng-model = 'ctrl.formData.depth', placeholder = 'How many backups to rollover', type = 'number', min = '1', required)
.form-group
label.control-label.col-md-2(for = 'remote') Remote
.col-md-10
select#remote.form-control(form = 'backupform', ng-options = 'remote.name group by remote.type for remote in ctrl.remotes', ng-model = 'ctrl.formData.remote' required)
option(value = ''): em -- Choose a file system remote point --
.form-group
.col-md-10.col-md-offset-2
a(ui-sref = 'backup.remote')
i.fa.fa-pencil
|   Manage your remote stores
.form-group(ng-if = '!ctrl.formData.scheduleId')
label.control-label.col-md-2(for = 'enabled')
input#enabled(form = 'backupform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
.help-block.col-md-10 Enable immediately after creation
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-clock-o
| Schedule
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
.grid-sm
.panel.panel-default
.panel-body
fieldset.center(ng-disabled = '!ctrl.ready')
button.btn.btn-lg.btn-primary(form = 'backupform', type = 'submit')
i.fa.fa-clock-o
|  
i.fa.fa-arrow-right
|  
i.fa.fa-database
|  Save 
|  
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetData()')
|  Reset 
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-list-ul
| Schedules
.panel-body
.text-center(ng-if = '!ctrl.schedules'): i.xo-icon-loading
.text-center(ng-if = 'ctrl.schedules && !ctrl.size(ctrl.schedules)') No scheduled jobs
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.size(ctrl.schedules)')
tr
th ID
th Tag
th.hidden-xs.hidden-sm VMs to backup
th.hidden-xs Remote
th.hidden-xs Depth
th.hidden-xs Scheduling
th Enabled now
th
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
td {{ schedule.id }}
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
td.hidden-xs.hidden-sm
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].vm | resolve).name_label }}
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs 
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
div(collapse = '!unCollapsed')
br
ul.list-group
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
span(ng-if = 'item.vm | resolve') {{ (item.vm | resolve).name_label }}
span(ng-if = '(item.vm | resolve).$container')  ({{ ((item.vm | resolve).$container | resolve).name_label }})
td.hidden-xs
strong: a(ui-sref = 'scheduler.remote') {{ ctrl.remotes[ctrl.jobs[schedule.job].paramsVector.items[0].values[0].remote].name }}
td.hidden-xs {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
td.text-center
i.fa.fa-check(ng-if = 'schedule.enabled')
td.text-right
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
|  
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|  
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o

View File

@@ -0,0 +1,224 @@
import angular from 'angular'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import later from 'later'
import prettyCron from 'prettycron'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
later.date.localTime()
import view from './view'
// ====================================================================
export default angular.module('backup.disasterrecovery', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.disasterrecovery', {
url: '/disasterrecovery/:id',
controller: 'DisasterRecoveryCtrl as ctrl',
template: view
})
})
.controller('DisasterRecoveryCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
const JOBKEY = 'disasterRecovery'
this.ready = false
this.running = {}
this.comesForEditing = $stateParams.id
this.scheduleApi = {}
this.formData = {}
const refreshSchedules = () => xo.schedule.getAll()
.then(schedules => {
const s = {}
forEach(schedules, schedule => {
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
})
this.schedules = s
})
const refreshJobs = () => xo.job.getAll()
.then(jobs => {
const j = {}
forEach(jobs, job => {
j[job.id] = job
})
this.jobs = j
})
const refresh = () => refreshJobs().then(refreshSchedules)
const getReady = () => refresh().then(() => this.ready = true)
getReady()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => $interval.cancel(interval))
const toggleState = (toggle, state) => {
const selectedVms = this.formData.selectedVms.slice()
if (toggle) {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
forEach(vms, vm => {
if (vm.power_state === state) {
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
}
})
this.formData.selectedVms = selectedVms
} else {
const keptVms = []
for (let index in this.formData.selectedVms) {
if (this.formData.selectedVms[index].power_state !== state) {
keptVms.push(this.formData.selectedVms[index])
}
}
this.formData.selectedVms = keptVms
}
}
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
this.edit = schedule => {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
const job = this.jobs[schedule.job]
const selectedVms = []
forEach(job.paramsVector.items[0].values, value => {
const vm = find(vms, vm => vm.id === value.id)
vm && selectedVms.push(vm)
})
const tag = job.paramsVector.items[0].values[0].tag
const selectedPool = xoApi.get(job.paramsVector.items[0].values[0].pool)
const depth = job.paramsVector.items[0].values[0].depth
const cronPattern = schedule.cron
this.resetData()
// const formData = this.formData
this.formData.selectedVms = selectedVms
this.formData.tag = tag
this.formData.selectedPool = selectedPool
this.formData.depth = depth
this.formData.scheduleId = schedule.id
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, tag, pool, depth, cron, enabled) => {
if (!vms.length) {
notify.warning({
title: 'No Vms selected',
message: 'Choose VMs to copy'
})
return
}
const _save = (id === undefined) ? saveNew(vms, tag, pool, depth, cron, enabled) : save(id, vms, tag, pool, depth, cron)
return _save
.then(() => {
notify.info({
title: 'Disaster Recovery',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(refresh)
}
const save = (id, vms, tag, pool, depth, cron) => {
const schedule = this.schedules[id]
const job = this.jobs[schedule.job]
const values = []
forEach(vms, vm => {
values.push({id: vm.id, tag, pool: pool.id, depth})
})
job.paramsVector.items[0].values = values
return xo.job.set(job)
.then(response => {
if (response) {
return xo.schedule.set(schedule.id, undefined, cron, undefined)
} else {
notify.error({
title: 'Update schedule',
message: 'Job updating failed'
})
throw new Error('Job updating failed')
}
})
}
const saveNew = (vms, tag, pool, depth, cron, enabled) => {
const values = []
forEach(vms, vm => {
values.push({id: vm.id, tag, pool: pool.id, depth})
})
const job = {
type: 'call',
key: JOBKEY,
method: 'vm.rollingDrCopy',
paramsVector: {
type: 'crossProduct',
items: [{
type: 'set',
values
}]
}
}
return xo.job.create(job)
.then(jobId => xo.schedule.create(jobId, cron, enabled))
}
this.delete = schedule => {
let jobId = schedule.job
return xo.schedule.delete(schedule.id)
.then(() => xo.job.delete(jobId))
.finally(() => {
if (this.formData.scheduleId === schedule.id) {
this.resetData()
}
refresh()
})
}
this.run = schedule => {
this.running[schedule.id] = true
notify.info({
title: 'Run Job',
message: 'One shot running started. See overview for logs.'
})
const id = schedule.job
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
}
this.inTargetPool = vm => vm.$poolId === (this.formData.selectedPool && this.formData.selectedPool.id)
this.resetData = () => {
this.formData.allRunning = false
this.formData.allHalted = false
this.formData.selectedVms = []
this.formData.scheduleId = undefined
this.formData.tag = undefined
this.formData.selectedPool = undefined
this.formData.depth = undefined
this.formData.enabled = false
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
}
this.collectionLength = col => Object.keys(col).length
this.prettyCron = prettyCron.toString.bind(prettyCron)
if (!this.comesForEditing) {
refresh()
} else {
refresh()
.then(() => {
this.edit(this.schedules[this.comesForEditing])
delete this.comesForEditing
})
}
this.resetData()
this.objects = xoApi.all
})
// A module exports its name.
.name

View File

@@ -0,0 +1,145 @@
.panel.panel-default
p.page-title
i.fa.fa-medkit(style="color: #e25440;")
| Disaster Recovery
form#drform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.tag, ctrl.formData.selectedPool, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled)')
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.xo-icon-vm(style='color: #e25440;')
| VMs to copy
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
.container-fluid(ng-if = 'ctrl.formData')
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Disaster Recovery
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Disaster Recovery ID {{ ctrl.formData.scheduleId }}
.form-group
label.control-label.col-md-2(for = 'tag') Tag
.col-md-10
.input-group
span.input-group-addon DR_
input#tag.form-control(form = 'drform', ng-model = 'ctrl.formData.tag', placeholder = 'VM copy tag', required)
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
label.control-label.col-md-2(for = 'vmlist') VMs
.col-md-8
ui-select#vmlist(form = 'drform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose VMs to copy')
span(ng-class = '{"bg-danger": ctrl.inTargetPool($item)}')
i.xo-icon-working(ng-if="isVMWorking($item)")
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
| {{$item.name_label}}
span(ng-if="$item.$container")
| ({{($item.$container | resolve).name_label}})
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
div
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
| {{vm.name_label}}
span(ng-if="vm.$container")
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
.col-md-2
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
input.hidden(form = 'drform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
span.fa-stack
i.xo-icon-running.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
input.hidden(form = 'drform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
span.fa-stack
i.xo-icon-halted.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
.form-group(ng-if = '(ctrl.formData.selectedVms | filter:ctrl.inTargetPool).length')
.col-md-offset-2.col-md-10
.alert.alert-warning
i.fa.fa-exclamation-triangle
|  At the moment, the selected VMs displayed in red are in the copy target pool.
.form-group
label.control-label.col-md-2(for = 'pool') To Pool
.col-md-10
ui-select#pool(form = 'drform', ng-model = 'ctrl.formData.selectedPool', required)
ui-select-match(placeholder = 'Choose destination pool')
i(class="xo-icon-pool")
| {{$select.selected.name_label}}
span(ng-if="$select.selected.$container")
| ({{($select.selected.$container | resolve).name_label}})
ui-select-choices(repeat = 'pool in ctrl.objects | selectHighLevel | filter:{type: "pool"} | filter:$select.search | orderBy:["$container", "name_label"] track by pool.id')
div
i(class="xo-icon-pool")
| {{pool.name_label}}
span(ng-if="pool.$container")
| ({{(pool.$container | resolve).name_label || ((pool.$container | resolve).master | resolve).name_label}})
.form-group
label.control-label.col-md-2(for = 'depth') Depth
.col-md-10
input#depth.form-control(form = 'drform', ng-model = 'ctrl.formData.depth', placeholder = 'How many VM copies to rollover', type = 'number', min = '1', required)
.form-group(ng-if = '!ctrl.formData.scheduleId')
label.control-label.col-md-2(for = 'enabled')
input#enabled(form = 'drform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
.help-block.col-md-8 Enable immediately after creation
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-clock-o
| Schedule
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
.grid-sm
.panel.panel-default
.panel-body
fieldset.center(ng-disabled = '!ctrl.ready')
button.btn.btn-lg.btn-primary(form = 'drform', type = 'submit')
i.fa.fa-clock-o
|  
i.fa.fa-arrow-right
|  
i.fa.fa-database
|  Save 
|  
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetData()')
|  Reset 
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-list-ul
| Schedules
.panel-body
.text-center(ng-if = '!ctrl.schedules'): i.xo-icon-loading
.text-center(ng-if = 'ctrl.schedules && !ctrl.collectionLength(ctrl.schedules)') No scheduled jobs
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
tr
th ID
th Tag
th.hidden-xs.hidden-sm VMs to Copy
th.hidden-xs To Pool
th.hidden-xs Depth
th.hidden-xs Scheduling
th Enabled now
th
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
td {{ schedule.id }}
td
span.label.label-default DR_
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
td.hidden-xs.hidden-sm
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].id | resolve).name_label }}
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs 
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
div(collapse = '!unCollapsed')
ul.list-group
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
span(ng-if = 'item.id | resolve') {{ (item.id | resolve).name_label }}
span(ng-if = '(item.id | resolve).$container')  ({{ ((item.id | resolve).$container | resolve).name_label }})
td.hidden-xs {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].pool | resolve).name_label }}
td.hidden-xs {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
td.text-center
i.fa.fa-check(ng-if = 'schedule.enabled')
td.text-right
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
|  
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
|  
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o

View File

@@ -0,0 +1,51 @@
import angular from 'angular'
import later from 'later'
import scheduler from 'scheduler'
import uiRouter from 'angular-ui-router'
later.date.localTime()
import backup from './backup'
import continuousReplication from './continuous-replication'
import deltaBackup from './delta-backup'
import disasterRecovery from './disaster-recovery'
import management from './management'
import mount from './remote'
import restore from './restore'
import rollingSnapshot from './rolling-snapshot'
import view from './view'
export default angular.module('backup', [
uiRouter,
backup,
continuousReplication,
deltaBackup,
disasterRecovery,
management,
mount,
restore,
rollingSnapshot,
scheduler
])
.config(function ($stateProvider) {
$stateProvider.state('backup', {
abstract: true,
data: {
requireAdmin: true
},
template: view,
url: '/backup'
})
// Redirect to default sub-state.
$stateProvider.state('backup.index', {
url: '',
controller: function ($state) {
$state.go('backup.management')
}
})
})
.name

View File

@@ -0,0 +1,210 @@
import angular from 'angular'
import filter from 'lodash.filter'
import forEach from 'lodash.foreach'
import prettyCron from 'prettycron'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import view from './view'
// ====================================================================
export default angular.module('backup.management', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.management', {
url: '/management',
controller: 'ManagementCtrl as ctrl',
template: view
})
})
.controller('ManagementCtrl', function (
$interval,
$scope,
$state,
$stateParams,
filterFilter,
modal,
notify,
selectHighLevelFilter,
xo,
xoApi
) {
this.running = {}
const mapJobKeyToState = {
continuousReplication: 'continuousReplication',
deltaBackup: 'deltaBackup',
disasterRecovery: 'disasterrecovery',
rollingBackup: 'backup',
rollingSnapshot: 'rollingsnapshot',
__none: 'index'
}
const mapJobKeyToJobDisplay = {
continuousReplication: 'Continuous Replication',
deltaBackup: 'Delta Backup',
disasterRecovery: 'Disaster Recovery',
rollingBackup: 'Backup',
rollingSnapshot: 'Rolling Snapshot',
__none: '[unknown]'
}
this.currentLogPage = 1
this.logPageSize = 10
const refreshSchedules = () => {
xo.schedule.getAll()
.then(schedules => this.schedules = filter(schedules, schedule => this.jobs[schedule.job] && this.jobs[schedule.job].key in mapJobKeyToState))
xo.scheduler.getScheduleTable()
.then(table => this.scheduleTable = table)
}
const getLogs = () => {
xo.logs.get('jobs').then(logs => {
const viewLogs = {}
const logsToClear = []
forEach(logs, (log, logKey) => {
const data = log.data
const [time] = logKey.split(':')
if (data.event === 'job.start' && data.key in mapJobKeyToState) {
logsToClear.push(logKey)
viewLogs[logKey] = {
logKey,
jobId: data.jobId,
key: data.key,
userId: data.userId,
start: time,
calls: {},
time
}
} else {
const runJobId = data.runJobId
const entry = viewLogs[runJobId]
if (!entry) {
return
}
logsToClear.push(logKey)
if (data.event === 'job.end') {
if (data.error) {
entry.error = data.error
}
entry.end = time
entry.duration = time - entry.start
entry.status = 'Finished'
} else if (data.event === 'jobCall.start') {
entry.calls[logKey] = {
callKey: logKey,
params: resolveParams(data.params),
method: data.method,
time
}
} else if (data.event === 'jobCall.end') {
const call = entry.calls[data.runCallId]
if (data.error) {
call.error = data.error
entry.hasErrors = true
} else {
call.returnedValue = resolveReturn(data.returnedValue)
}
}
}
})
forEach(viewLogs, log => {
if (log.end === undefined) {
log.status = 'In progress'
}
})
this.logs = viewLogs
this.logsToClear = logsToClear
})
}
const resolveParams = params => {
for (let key in params) {
const xoObject = xoApi.get(params[key])
if (xoObject) {
const newKey = xoObject.type || key
params[newKey] = xoObject.name_label || xoObject.name || params[key]
newKey !== key && delete params[key]
}
}
return params
}
const resolveReturn = returnValue => {
const xoObject = xoApi.get(returnValue)
let xoName = xoObject && (xoObject.name_label || xoObject.name)
xoName && (xoName += xoObject.type && ` (${xoObject.type})` || '')
returnValue = xoName || returnValue
return returnValue
}
this.prettyCron = prettyCron.toString.bind(prettyCron)
const refreshJobs = () => {
return xo.job.getAll()
.then(jobs => {
const j = {}
forEach(jobs, job => j[job.id] = job)
this.jobs = j
})
}
const refresh = () => {
refreshJobs().then(refreshSchedules)
getLogs()
}
refresh()
const interval = $interval(() => {
refresh()
}, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.clearLogs = () => {
modal.confirm({
title: 'Clear logs',
message: 'Are you sure you want to delete all logs ?'
})
.then(() => xo.logs.delete('jobs', this.logsToClear))
}
this.enable = id => {
this.working[id] = true
return xo.scheduler.enable(id)
.finally(() => { this.working[id] = false })
.then(refreshSchedules)
}
this.disable = id => {
this.working[id] = true
return xo.scheduler.disable(id)
.finally(() => { this.working[id] = false })
.then(refreshSchedules)
}
this.run = schedule => {
this.running[schedule.id] = true
notify.info({
title: 'Run Job',
message: 'One shot running started. See overview for logs.'
})
const id = schedule.job
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
}
this.resolveJobKey = schedule => mapJobKeyToState[this.jobs[schedule.job] && this.jobs[schedule.job].key || '__none']
this.displayJobKey = schedule => mapJobKeyToJobDisplay[this.jobs[schedule.job] && this.jobs[schedule.job].key || '__none']
this.displayLogKey = log => mapJobKeyToJobDisplay[log.key]
this.resolveScheduleJobTag = schedule => this.jobs[schedule.job] && this.jobs[schedule.job].paramsVector && this.jobs[schedule.job].paramsVector.items[0].values[0].tag || schedule.id
this.collectionLength = col => Object.keys(col).length
this.working = {}
})
// A module exports its name.
.name

View File

@@ -0,0 +1,88 @@
.panel.panel-default
p.page-title
i.fa.fa-eye(style="color: #e25440;")
| Backup Overview
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-clock-o
| Schedules
.panel-body
//- The 2 tables below are here for a "full-width" effect of the content vs the menu (cf sheduler/view.jade)
table.table(ng-if = '!ctrl.schedules')
tr
td.text-center: i.xo-icon-loading
table.table(ng-if = 'ctrl.schedules && !ctrl.collectionLength(ctrl.schedules)')
tr
td.text-center No scheduled jobs
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
tr
th Job
th Tag
th.hidden-xs Scheduling
th State
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
td {{ ctrl.displayJobKey(schedule) }}
td: a(ui-sref = 'backup.{{ctrl.resolveJobKey(schedule)}}({id: schedule.id})') {{ ctrl.resolveScheduleJobTag(schedule) }}
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
td
span.label.label-success.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === true') enabled
span.label.label-default.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === false') disabled
span.label.label-warning.hidden-xs(ng-if = 'ctrl.scheduleTable[schedule.id] === undefined') unknown
fieldset.pull-right(ng-disabled = 'ctrl.working[schedule.id]')
button.btn(ng-if = 'ctrl.scheduleTable[schedule.id] === false', type = 'button', ng-click = 'ctrl.enable(schedule.id)'): i.fa.fa-toggle-off
button.btn.btn-success(ng-if = 'ctrl.scheduleTable[schedule.id] === true', type = 'button', ng-click = 'ctrl.disable(schedule.id)'): i.fa.fa-toggle-on
|  
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-file-text
| Logs
span.quick-edit(ng-if = 'ctrl.logs | isNotEmpty', tooltip = 'Remove all logs', xo-click = 'ctrl.clearLogs()')
i.fa.fa-trash-o.fa-fw
.panel-body
table.table.table-hover(ng-if = 'ctrl.logs')
thead
tr
th Job ID
th Job
th Start
th End
th Duration
th Status
tbody(ng-repeat = 'log in ctrl.logs | map | filter:ctrl.logSearch | orderBy:"-time" | slice:(ctrl.logPageSize * (ctrl.currentLogPage - 1)):(ctrl.logPageSize * ctrl.currentLogPage) track by log.logKey')
tr
td
button.btn.btn-sm(type = 'button', tooltip = 'See calls', ng-click = 'seeCalls = !seeCalls', ng-class = '{"btn-default": !log.hasErrors, "btn-danger": log.hasErrors}'): i.fa(ng-class = '{"fa-caret-down": !seeCalls, "fa-caret-up": seeCalls}')
|  {{ log.jobId }}
td {{ ctrl.displayLogKey(log) }}
td {{ log.start | date:'medium' }}
td {{ log.end | date:'medium' }}
td {{ log.duration | duration}}
td
span(ng-if = 'log.status === "Finished"')
span.label(ng-class = '{"label-success": (!log.error && !log.hasErrors), "label-danger": (log.error || log.hasErrors)}') {{ log.status }}
span.label(ng-if = 'log.status !== "Finished"', ng-class = '{"label-warning": log.status === "In progress", "label-default": !log.status}') {{ log.status || "unknown" }}
p.text-danger(ng-if = 'log.error')  {{ log.error }}
tr.bg-info(collapse = '!seeCalls')
td(colspan = '6')
ul.list-group
li.list-group-item(ng-repeat = 'call in log.calls | map | orderBy:"-time" track by call.callKey')
strong.text-info {{ call.method }}: 
span(ng-repeat = '(key, param) in call.params')
strong {{ key }}:
|  {{ param }} 
span(ng-if = 'call.returnedValue')
|  
i.text-primary.fa.fa-arrow-right
|  {{ call.returnedValue }}
span.text-danger(ng-if = 'call.error')
|  
i.fa.fa-times
|  {{ call.error }}
.form-inline
.input-group
.input-group-addon: i.fa.fa-search
input.form-control(type = 'text', ng-model = 'ctrl.logSearch', placeholder = 'Search logs...')
.center(ng-if = '(ctrl.logs | map | filter:ctrl.logSearch | count) > ctrl.logPageSize || currentLogPage > 1')
pagination.pagination-sm(boundary-links = 'true', total-items = 'ctrl.logs | map | filter:ctrl.logSearch | count', ng-model = 'ctrl.currentLogPage', items-per-page = 'ctrl.logPageSize', max-size = '10', previous-text = '<', next-text = '>', first-text = '<<', last-text = '>>')

View File

@@ -0,0 +1,58 @@
import angular from 'angular'
import map from 'lodash.map'
import size from 'lodash.size'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import {format, parse} from 'xo-remote-parser'
import view from './view'
// ====================================================================
export default angular.module('backup.remote', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.remote', {
url: '/remote',
controller: 'RemoteCtrl as ctrl',
template: view
})
})
.controller('RemoteCtrl', function ($scope, $state, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
this.ready = false
const refresh = () => {
return xo.remote.getAll()
.then(remotes => this.backUpRemotes = map(remotes, parse))
}
this.getReady = () => {
return refresh()
.then(() => this.ready = true)
}
this.getReady()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.prepareUrl = (type, host, path, username, password, domain) => format({type, host, path, username, password, domain})
const reset = () => {
this.path = this.host = this.name = undefined
this.remoteType = 'file'
}
this.add = (name, url) => xo.remote.create(name, url).then(reset).then(refresh)
this.remove = id => xo.remote.delete(id).then(refresh)
this.enable = id => xo.remote.set(id, undefined, undefined, true).then(refresh)
this.disable = id => xo.remote.set(id, undefined, undefined, false).then(refresh)
this.size = size
reset()
})
// A module exports its name.
.name

View File

@@ -0,0 +1,143 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-plug(style="color: #e25440;")
| Remotes stores for backup
.grid-sm
.panel.panel-default
.panel-body
//- {{ ctrl.backUpRemotes }} {{ ctrl.size(ctrl.backUpRemotes) }}
.text-center(ng-if = '!ctrl.size(ctrl.backUpRemotes)') No remotes
table.table.table-hover(ng-if = 'ctrl.size(ctrl.backUpRemotes)')
tbody(ng-if = '(ctrl.backUpRemotes | filter:{type:"local"}).length')
tr
th.text-info Local
th Name
th Path
th
th State
th Error
th
tr(ng-repeat = 'remote in ctrl.backUpRemotes | filter:{type:"local"} | orderBy:["name"] track by remote.id')
td
td {{ remote.name }}
td {{ remote.path }}
td
td
span(ng-if = 'remote.enabled')
span.text-success
| Accessible&nbsp;
i.fa.fa-check
//- button.btn.btn-warning.pull-right(type = 'button', ng-click = 'ctrl.disable(remote.id)'): i.fa.fa-chain-broken
span(ng-if = '!remote.enabled')
span.text-muted Unaccessible&nbsp;
button.btn.btn-primary.pull-right(type = 'button', ng-click = 'ctrl.enable(remote.id)'): i.fa.fa-link
td: span.text-muted {{ remote.error }}
td: button.btn.btn-danger.pull-right(type = 'button', ng-click = 'ctrl.remove(remote.id)'): i.fa.fa-trash
tbody(ng-if = '(ctrl.backUpRemotes | filter:{type:"nfs"}).length')
tr
th.text-info NFS
th Name
th Device
th
th State
th Error
th
tr(ng-repeat = 'remote in ctrl.backUpRemotes | filter:{type:"nfs"} | orderBy:["name"] track by remote.id')
td
td {{ remote.name }}
td {{ remote.host }}:{{ remote.share }}
td
td
span(ng-if = 'remote.enabled')
span.text-success
| Mounted&nbsp;
i.fa.fa-check
button.btn.btn-warning.pull-right(type = 'button', ng-click = 'ctrl.disable(remote.id)'): i.fa.fa-chain-broken
span(ng-if = '!remote.enabled')
span.text-muted Unmounted&nbsp;
button.btn.btn-primary.pull-right(type = 'button', ng-click = 'ctrl.enable(remote.id)'): i.fa.fa-link
td: span.text-muted {{ remote.error }}
td: button.btn.btn-danger.pull-right(type = 'button', ng-click = 'ctrl.remove(remote.id)'): i.fa.fa-trash
tbody(ng-if = '(ctrl.backUpRemotes | filter:{type:"smb"}).length')
tr
th.text-info SMB
th Name
th Share
th Auth
th State
th Error
th
tr(ng-repeat = 'remote in ctrl.backUpRemotes | filter:{type:"smb"} | orderBy:["name"] track by remote.id')
td
td {{ remote.name }}
td
strong.text-info &bsol;&bsol;
| {{ remote.host }}
strong.text-info &bsol;
| {{ remote.path }}
td {{ remote.username }}@{{remote.domain}}
td
span(ng-if = 'remote.enabled')
span.text-success
| Accessible&nbsp;
i.fa.fa-check
button.btn.btn-warning.pull-right(type = 'button', ng-click = 'ctrl.disable(remote.id)'): i.fa.fa-chain-broken
span(ng-if = '!remote.enabled')
span.text-muted Unaccessible&nbsp;
button.btn.btn-primary.pull-right(type = 'button', ng-click = 'ctrl.enable(remote.id)'): i.fa.fa-link
td: span.text-muted {{ remote.error }}
td: button.btn.btn-danger.pull-right(type = 'button', ng-click = 'ctrl.remove(remote.id)'): i.fa.fa-trash
form(ng-submit = 'ctrl.add(ctrl.name, ctrl.prepareUrl(ctrl.remoteType, ctrl.host, ctrl.path, ctrl.username, ctrl.password, ctrl.domain))')
fieldset
legend New File System Remote
.form-inline
.form-group
label.sr-only Type
select.form-control(ng-model = 'ctrl.remoteType')
option(value = 'file') Local
option(value = 'nfs') NFS
option(value = 'smb') SMB
| &nbsp;
.form-group
label.sr-only Name
input.form-control(type = 'text', ng-model = 'ctrl.name', placeholder = 'Name', required)
| &nbsp;
br
.form-inline
.form-group(ng-if = 'ctrl.remoteType === "nfs"')
label.sr-only Host
input.form-control(type = 'text', ng-model = 'ctrl.host', placeholder = 'host', required)
strong &nbsp;:&nbsp;
.input-group(ng-if = 'ctrl.remoteType !== "smb"')
span.input-group-addon /
label.sr-only Path
input.form-control(type = 'text', ng-model = 'ctrl.path', placeholder = 'path/to/backup')
.form-group(ng-if = 'ctrl.remoteType === "smb"')
.input-group
span.input-group-addon &bsol;&bsol;
label.sr-only Share
input.form-control(type = 'text', ng-model = 'ctrl.host', placeholder = 'share', required)
.input-group
span.input-group-addon &bsol;
label.sr-only Path
input.form-control(type = 'text', ng-model = 'ctrl.path', placeholder != 'path&bsol;to&bsol;backup')
br
.form-inline(ng-if = 'ctrl.remoteType === "smb"')
.form-group
label.sr-only User Name
input.form-control(type = 'text', ng-model = 'ctrl.username', placeholder = 'username', required)
| &nbsp;
.form-group
label.sr-only Password
input.form-control(type = 'password', ng-model = 'ctrl.password', placeholder = 'password', required)
| &nbsp;
.form-group
label.sr-only Domain
input.form-control(type = 'text', ng-model = 'ctrl.domain', placeholder = 'domain', required)
br
br
.form-group
button.btn.btn-primary(type = 'submit', ng-disabled = '!ctrl.ready')
| Save&nbsp;
i.fa.fa-floppy-o

View File

@@ -0,0 +1,118 @@
import angular from 'angular'
import filter from 'lodash.filter'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import size from 'lodash.size'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import view from './view'
// ====================================================================
export default angular.module('backup.restore', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.restore', {
url: '/restore',
controller: 'RestoreCtrl as ctrl',
template: view
})
})
.controller('RestoreCtrl', function ($scope, $interval, xo, xoApi, notify, $upload, bytesToSizeFilter) {
this.loaded = {}
const srs = xoApi.getView('SRs').all
this.bytesToSize = bytesToSizeFilter
this.isEmpty = backups => backups && !(Object.keys(backups.delta) || backups.other.length)
this.size = size
const refresh = () => {
return xo.remote.getAll()
.then(remotes => {
forEach(this.backUpRemotes, remote => {
if (remote.backups) {
const freshRemote = find(remotes, {id: remote.id})
freshRemote && (freshRemote.backups = remote.backups)
}
})
this.backUpRemotes = remotes
this.writable_SRs = filter(srs, (sr) => sr.content_type !== 'iso')
})
}
refresh()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
const deltaBuilder = (backups, uuid, name, tag, value) => {
let deltaBackup = backups[uuid]
? backups[uuid]
: backups[uuid] = {}
deltaBackup = deltaBackup[name]
? deltaBackup[name]
: deltaBackup[name] = {}
deltaBackup = deltaBackup[tag]
? deltaBackup[tag]
: deltaBackup[tag] = []
deltaBackup.push(value)
}
this.list = id => {
return xo.remote.list(id)
.then(files => {
const remote = find(this.backUpRemotes, {id})
if (remote) {
const backups = remote.backups = {
delta: {},
other: []
}
forEach(files, file => {
const arr = /^vm_delta_(.*)_([^\/]+)\/([^_]+)_(.*)$/.exec(file)
if (arr) {
const [ , tag, uuid, date, name ] = arr
const value = {
path: file,
date
}
deltaBuilder(backups.delta, uuid, name, tag, value)
} else {
backups.other.push(file)
}
})
}
this.loaded[remote.id] = true
})
}
const notification = {
title: 'VM import started',
message: 'Starting the VM import'
}
this.importBackup = (id, path, sr) => {
notify.info(notification)
return xo.vm.importBackup(id, path, sr)
}
this.importDeltaBackup = (id, path, sr) => {
notify.info(notification)
return xo.vm.importDeltaBackup(id, path, sr)
}
})
// A module exports its name.
.name

View File

@@ -0,0 +1,68 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-upload(style="color: #e25440;")
| Backup Restore
.grid-sm
.panel.panel-default
.panel-body
.text-center(ng-if = '!ctrl.size(ctrl.backUpRemotes)') No remotes
.panel.panel-default(ng-repeat = 'remote in ctrl.backUpRemotes | orderBy:["name"] track by remote.id')
.panel-body(ng-if = '!remote.enabled || remote.error', ng-class = '{"bg-danger": remote.error, "bg-muted": !remote.error}')
a(ui-sref = 'backup.remote') {{ remote.name }}
span(ng-if = 'remote.error') &nbsp;(on error)
span(ng-if = '!remote.error') &nbsp;(disabled)
.panel-body(ng-if = 'remote.enabled')
.row
.col-sm-2
p
| {{ remote.name }}&nbsp;
button.btn.btn-default.pull-right(type = 'button', ng-click = 'ctrl.list(remote.id)'): i.fa(ng-class = '{"fa-eye": !ctrl.loaded[remote.id], "fa-refresh": ctrl.loaded[remote.id]}')
br
br
.col-sm-10
div(ng-if = 'ctrl.loaded[remote.id] && ctrl.isEmpty(remote.backups)') No backups available
div(ng-if = 'ctrl.size(remote.backups.delta)')
div(ng-repeat = '(uuid, backups) in remote.backups.delta')
.row
.col-sm-2
| {{ uuid }}
.col-sm-10
div(ng-repeat = '(name, backups) in backups')
.row
.col-sm-2
| {{ name }}
.col-sm-10
div(ng-repeat = '(tag, backups) in backups')
.row
.col-sm-2
| {{ tag }}
.col-sm-10
div(ng-repeat = 'backup in backups')
| {{ backup.date | date:'medium' }}
span.pull-right.dropdown(dropdown)
button.btn.btn-default(type = 'button', dropdown-toggle)
| Import&nbsp;
span.caret
ul.dropdown-menu(role="menu")
li(ng-repeat = 'sr in ctrl.writable_SRs | orderBy:natural("name_label") track by sr.id')
a(xo-click = "ctrl.importDeltaBackup(remote.id, backup.path, sr.id)")
i.xo-icon-host.fa-fw
| To {{sr.name_label}} ({{sr.size - sr.physical_usage | bytesToSize }})
span &nbsp;{{ (sr.$container | resolve).name_label }}
hr
hr
div(ng-if = 'ctrl.size(remote.backups.other)')
div(ng-repeat = 'backup in remote.backups.other')
| {{ backup }}
span.pull-right.dropdown(dropdown)
button.btn.btn-default(type = 'button', dropdown-toggle)
| Import&nbsp;
span.caret
ul.dropdown-menu(role="menu")
li(ng-repeat = 'sr in ctrl.writable_SRs | orderBy:natural("name_label") track by sr.id')
a(xo-click = "ctrl.importBackup(remote.id, backup, sr.id)")
i.xo-icon-host.fa-fw
| To {{sr.name_label}} ({{sr.size - sr.physical_usage | bytesToSize }})
span &nbsp;{{ (sr.$container | resolve).name_label }}
hr

View File

@@ -0,0 +1,242 @@
import angular from 'angular'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import later from 'later'
import prettyCron from 'prettycron'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
later.date.localTime()
import view from './view'
// ====================================================================
export default angular.module('backup.rollingSnapshot', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('backup.rollingsnapshot', {
url: '/rollingsnapshot/:id',
controller: 'RollingSnapshotCtrl as ctrl',
template: view
})
})
.controller('RollingSnapshotCtrl', function ($scope, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
const JOBKEY = 'rollingSnapshot'
this.ready = false
this.running = {}
this.comesForEditing = $stateParams.id
this.scheduleApi = {}
this.formData = {}
const refreshSchedules = () => {
return xo.schedule.getAll()
.then(schedules => {
const s = {}
forEach(schedules, schedule => {
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
})
this.schedules = s
})
}
const refreshJobs = () => {
return xo.job.getAll()
.then(jobs => {
const j = {}
forEach(jobs, job => j[job.id] = job)
this.jobs = j
})
}
const refresh = () => {
return refreshJobs().then(refreshSchedules)
}
this.getReady = () => refresh().then(() => this.ready = true)
this.getReady()
const interval = $interval(() => {
refresh()
}, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
const toggleState = (toggle, state) => {
const selectedVms = this.formData.selectedVms.slice()
if (toggle) {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
forEach(vms, vm => {
if (vm.power_state === state) {
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
}
})
this.formData.selectedVms = selectedVms
} else {
const keptVms = []
for (let index in this.formData.selectedVms) {
if (this.formData.selectedVms[index].power_state !== state) {
keptVms.push(this.formData.selectedVms[index])
}
}
this.formData.selectedVms = keptVms
}
}
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
this.edit = schedule => {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
const job = this.jobs[schedule.job]
const selectedVms = []
forEach(job.paramsVector.items[0].values, value => {
const vm = find(vms, vm => vm.id === value.id)
vm && selectedVms.push(vm)
})
const tag = job.paramsVector.items[0].values[0].tag
const depth = job.paramsVector.items[0].values[0].depth
const cronPattern = schedule.cron
this.resetData()
// const formData = this.formData
this.formData.selectedVms = selectedVms
this.formData.tag = tag
this.formData.depth = depth
this.formData.scheduleId = schedule.id
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, tag, depth, cron, enabled) => {
if (!vms.length) {
notify.warning({
title: 'No Vms selected',
message: 'Choose VMs to snapshot'
})
return
}
const _save = (id === undefined) ? saveNew(vms, tag, depth, cron, enabled) : save(id, vms, tag, depth, cron)
return _save
.then(() => {
notify.info({
title: 'Rolling snapshot',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(() => {
refresh()
})
}
const save = (id, vms, tag, depth, cron) => {
const schedule = this.schedules[id]
const job = this.jobs[schedule.job]
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
tag,
depth
})
})
job.paramsVector.items[0].values = values
return xo.job.set(job)
.then(response => {
if (response) {
return xo.schedule.set(schedule.id, undefined, cron, undefined)
} else {
notify.error({
title: 'Update schedule',
message: 'Job updating failed'
})
throw new Error('Job updating failed')
}
})
}
const saveNew = (vms, tag, depth, cron, enabled) => {
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
tag,
depth
})
})
const job = {
type: 'call',
key: JOBKEY,
method: 'vm.rollingSnapshot',
paramsVector: {
type: 'crossProduct',
items: [
{
type: 'set',
values
}
]
}
}
return xo.job.create(job)
.then(jobId => {
return xo.schedule.create(jobId, cron, enabled)
})
}
this.delete = schedule => {
let jobId = schedule.job
return xo.schedule.delete(schedule.id)
.then(() => xo.job.delete(jobId))
.finally(() => {
if (this.formData.scheduleId === schedule.id) {
this.resetData()
}
refresh()
})
}
this.run = schedule => {
this.running[schedule.id] = true
notify.info({
title: 'Run Job',
message: 'One shot running started. See overview for logs.'
})
const id = schedule.job
return xo.job.runSequence([id]).finally(() => delete this.running[schedule.id])
}
this.resetData = () => {
this.formData.allRunning = false
this.formData.allHalted = false
this.formData.selectedVms = []
this.formData.scheduleId = undefined
this.formData.tag = undefined
this.formData.depth = undefined
this.formData.enabled = false
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
}
this.collectionLength = col => Object.keys(col).length
this.prettyCron = prettyCron.toString.bind(prettyCron)
if (!this.comesForEditing) {
refresh()
} else {
refresh()
.then(() => {
this.edit(this.schedules[this.comesForEditing])
delete this.comesForEditing
})
}
this.resetData()
this.objects = xoApi.all
})
// A module exports its name.
.name

View File

@@ -0,0 +1,119 @@
.panel.panel-default
p.page-title
i.xo-icon-snapshot(style="color: #e25440;")
| Rolling snapshots
form#snapform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled)')
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.xo-icon-vm(style='color: #e25440;')
| VMs to snapshot
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
.container-fluid(ng-if = 'ctrl.formData')
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Rolling Snapshot
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Rolling Snapshot ID {{ ctrl.formData.scheduleId }}
.form-group
label.control-label.col-md-2(for = 'tag') Tag
.col-md-10
input#tag.form-control(form = 'snapform', ng-model = 'ctrl.formData.tag', placeholder = 'Rolling snapshot tag', required)
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
label.control-label.col-md-2(for = 'vmlist') VMs
.col-md-8
ui-select(form = 'snapform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose VMs to snapshot')
i.xo-icon-working(ng-if="isVMWorking($item)")
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
| {{$item.name_label}}
span(ng-if="$item.$container")
| ({{($item.$container | resolve).name_label}})
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
div
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
| {{vm.name_label}}
span(ng-if="vm.$container")
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
.col-md-2
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
input.hidden(form = 'snapform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
span.fa-stack
i.xo-icon-running.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
input.hidden(form = 'snapform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
span.fa-stack
i.xo-icon-halted.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
.form-group
label.control-label.col-md-2(for = 'depth') Depth
.col-md-10
input#depth.form-control(form = 'snapform', ng-model = 'ctrl.formData.depth', placeholder = 'How many snapshots to rollover', type = 'number', min = '1', required)
.form-group(ng-if = '!ctrl.formData.scheduleId')
label.control-label.col-md-2(for = 'enabled')
input#enabled(form = 'snapform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
.help-block.col-md-8 Enable immediately after creation
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-clock-o
| Schedule
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
.grid-sm
.panel.panel-default
.panel-body
fieldset.center(ng-disabled = '!ctrl.ready')
button.btn.btn-lg.btn-primary(form = 'snapform', type = 'submit')
i.fa.fa-clock-o
| &nbsp;
i.fa.fa-arrow-right
| &nbsp;
i.fa.fa-database
| &nbsp;Save&nbsp;
| &nbsp;
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetData()')
| &nbsp;Reset&nbsp;
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-list-ul
| Schedules
.panel-body
.text-center(ng-if = '!ctrl.schedules'): i.xo-icon-loading
.text-center(ng-if = 'ctrl.schedules && !ctrl.collectionLength(ctrl.schedules)') No scheduled jobs
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
tr
th ID
th Tag
th.hidden-xs.hidden-sm VMs to snapshot
th.hidden-xs Depth
th.hidden-xs Scheduling
th Enabled now
th
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
td {{ schedule.id }}
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
td.hidden-xs.hidden-sm
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length == 1')
| {{ (ctrl.jobs[schedule.job].paramsVector.items[0].values[0].id | resolve).name_label }}
div(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values.length > 1')
button.btn.btn-info(type = 'button', ng-click = 'unCollapsed = !unCollapsed')
| {{ ctrl.jobs[schedule.job].paramsVector.items[0].values.length }} VMs&nbsp;
i.fa(ng-class = '{"fa-chevron-down": !unCollapsed, "fa-chevron-up": unCollapsed}')
div(collapse = '!unCollapsed')
br
ul.list-group
li.list-group-item(ng-repeat = 'item in ctrl.jobs[schedule.job].paramsVector.items[0].values')
span(ng-if = 'item.id | resolve') {{ (item.id | resolve).name_label }}
span(ng-if = '(item.id | resolve).$container') &nbsp;({{ ((item.id | resolve).$container | resolve).name_label }})
td.hidden-xs {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
td.hidden-xs {{ ctrl.prettyCron(schedule.cron) }}
td.text-center
i.fa.fa-check(ng-if = 'schedule.enabled')
td.text-right
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(schedule)'): i.fa.fa-pencil
| &nbsp;
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(schedule)', ng-disabled = 'ctrl.running[schedule.id]'): i.fa.fa-play
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o

View File

@@ -0,0 +1,104 @@
accordion(ng-if = 'ctrl.data', close-others= 'false', ng-click = 'ctrl.update()')
accordion-group
accordion-heading Month
tabset
tab(select = 'ctrl.data.month = "all"', active = 'ctrl.tabs.month.all')
tab-heading every month
tab(select = 'ctrl.data.month = "select"', active = 'ctrl.tabs.month.select')
tab-heading each selected month
br
table.table.table-bordered
tr(ng-repeat = 'line in ctrl.months')
td(ng-click = 'ctrl.selectMonth(month.v)', ng-class = '{"bg-success": ctrl.isSelectedMonth(month.v)}',ng-repeat = 'month in line') {{ month.l }}
accordion-group
accordion-heading Day of the month
tabset
tab(select = 'ctrl.data.day = "all"', active = 'ctrl.tabs.day.all')
tab-heading every day
tab(select = 'ctrl.data.day = "select"', active = 'ctrl.tabs.day.select')
tab-heading each selected day
br
p.text-warning
i.fa.fa-warning
| &nbsp;This selection can restrict or be restricted by "Day of week" selections below. Use the summary preview to ensure your choice.
br
table.table.table-bordered
tr(ng-repeat = 'line in ctrl.days')
td(ng-click = 'ctrl.selectDay(day)', ng-class = '{"bg-success": ctrl.isSelectedDay(day)}',ng-repeat = 'day in line') {{ day }}
accordion-group
accordion-heading Day of week
tabset
tab(select = 'ctrl.data.dayWeek = "all"', active = 'ctrl.tabs.dayWeek.all')
tab-heading every day of week
tab(select = 'ctrl.data.dayWeek = "select"', active = 'ctrl.tabs.dayWeek.select')
tab-heading each selected day of week
br
p.text-warning
i.fa.fa-warning
| &nbsp;This selection can restrict or be restricted by "Day of the month" selections up ahead. Use the summary preview to ensure your choice.
br
table.table.table-bordered
tr
td(ng-click = 'ctrl.selectDayWeek(dayWeek.v)', ng-class = '{"bg-success": ctrl.isSelectedDayWeek(dayWeek.v)}',ng-repeat = 'dayWeek in ctrl.dayWeeks') {{ dayWeek.l }}
accordion-group
accordion-heading Hour
button.btn.btn-primary(ng-if = '!ctrl.noHourPlan()', type = 'button', ng-click = 'ctrl.noHourPlan(true)') Plan nothing on a hourly grain
button.btn.btn-primary.disabled(ng-if = 'ctrl.noHourPlan()', type = 'button')
i.fa.fa-info-circle
| &nbsp;Nothing planned on a hourly grain
br
br
tabset
tab(select = 'ctrl.data.hour = "all"', active = 'ctrl.tabs.hour.all')
tab-heading every hour
tab(select = 'ctrl.data.hour = "range"', active = 'ctrl.tabs.hour.range')
tab-heading every N hour
br
.form-group
label.col-sm-2.control-label {{ ctrl.data.hourRange }}
.col-sm-10
input.form-control(type = 'range', min = '2', max = '23', step = '1', ng-model = 'ctrl.data.hourRange', ng-change = 'ctrl.update()')
tab(select = 'ctrl.data.hour = "select"', active = 'ctrl.tabs.hour.select')
tab-heading each selected hour
br
table.table.table-bordered
tr(ng-repeat = 'line in ctrl.hours')
td(ng-click = 'ctrl.selectHour(hour)', ng-class = '{"bg-success": ctrl.isSelectedHour(hour)}',ng-repeat = 'hour in line') {{ hour }}
accordion-group
accordion-heading Minute
button.btn.btn-primary(ng-if = '!ctrl.noMinutePlan()', type = 'button', ng-click = 'ctrl.noMinutePlan(true)') Plan nothing on a minute grain
button.btn.btn-primary.disabled(ng-if = 'ctrl.noMinutePlan()', type = 'button')
i.fa.fa-info-circle
| &nbsp;Nothing planned on a minute grain
br
br
tabset
tab(select = 'ctrl.data.min = "all"', active = 'ctrl.tabs.min.all')
tab-heading every minute
tab(select = 'ctrl.data.min = "range"', active = 'ctrl.tabs.min.range')
tab-heading every N minutes
br
.form-group
label.col-sm-2.control-label {{ ctrl.data.minRange }}
.col-sm-10
input.form-control(type = 'range', min = '2', max = '59', step = '1', ng-model = 'ctrl.data.minRange', ng-change = 'ctrl.update()')
tab(select = 'ctrl.data.min = "select"', active = 'ctrl.tabs.min.select')
tab-heading each selected minute
br
table.table.table-bordered
tr(ng-repeat = 'line in ctrl.minutes')
td(ng-click = 'ctrl.selectMinute(min)', ng-class = '{"bg-success": ctrl.isSelectedMinute(min)}',ng-repeat = 'min in line') {{ min }}
input.form-control.hidden(type ='text', readonly, ng-model = 'ctrl.data.cronPattern')
.text-center(ng-if = '!ctrl.data'): i.xo-icon-loading
div(ng-if = 'ctrl.data')
p
strong Scheduled to run:&nbsp;
| {{ ctrl.prettyCron(ctrl.data.cronPattern) }}
.form-inline.container-fluid
.form-group
label Preview:&nbsp;
input.form-control(type = 'range', min = '0', max = '{{ ctrl.data.summary.length - 3 }}', step = '1', ng-model = 'ctrl.data.previewLimit')
br
ul
li(ng-repeat = 'occurence in ctrl.data.summary | limitTo: +ctrl.data.previewLimit+3') {{ occurence }}
li ...

View File

@@ -0,0 +1,37 @@
.menu-grid
.side-menu
ul.nav
li
a(ui-sref = '.management', ui-sref-active = 'active')
i.fa.fa-fw.fa-eye.fa-menu
span.menu-entry Overview
li
a(ui-sref = '.rollingsnapshot')
i.xo-icon-snapshot.fa-fw.fa-menu
span.menu-entry Rolling snapshots
li
a(ui-sref = '.remote')
i.fa.fa-fw.fa-plug.fa-menu
span.menu-entry Remote stores
li
a(ui-sref = '.backup')
i.fa.fa-fw.fa-download.fa-menu
span.menu-entry Backup
li
a(ui-sref = '.deltaBackup')
i.fa.fa-fw.fa-code-fork.fa-menu
span.menu-entry Delta Backup
li
a(ui-sref = '.restore')
i.fa.fa-fw.fa-upload.fa-menu
span.menu-entry Restore
li
a(ui-sref = '.disasterrecovery')
i.fa.fa-fw.fa-medkit.fa-menu
span.menu-entry Disaster Recovery
li
a(ui-sref = '.continuousReplication')
i.fa.fa-fw.fa-map-signs.fa-menu
span.menu-entry Continuous Replication
.side-content(ui-view = '')

View File

@@ -1,8 +1,9 @@
angular = require 'angular'
forEach = require('lodash.foreach')
includes = require('lodash.includes')
Clipboard = require('clipboard')
isoDevice = require('../iso-device')
isoDevice = require('iso-device')
#=====================================================================
@@ -17,7 +18,7 @@ module.exports = angular.module 'xoWebApp.console', [
url: '/consoles/:id'
controller: 'ConsoleCtrl'
template: require './view'
.controller 'ConsoleCtrl', ($scope, $stateParams, xoApi, xo, xoHideUnauthorizedFilter) ->
.controller 'ConsoleCtrl', ($scope, $stateParams, xoApi, xo, xoHideUnauthorizedFilter, modal) ->
{id} = $stateParams
{get} = xoApi
@@ -71,13 +72,48 @@ module.exports = angular.module 'xoWebApp.console', [
)
$scope.startVM = xo.vm.start
$scope.stopVM = xo.vm.stop
$scope.rebootVM = xo.vm.restart
$scope.stopVM = (id) ->
modal.confirm
title: 'VM shutdown'
message: 'Are you sure you want to shutdown this VM ?'
.then ->
xo.vm.stop id
$scope.rebootVM = (id) ->
modal.confirm
title: 'VM reboot'
message: 'Are you sure you want to reboot this VM ?'
.then ->
xo.vm.restart id
$scope.eject = ->
xo.vm.ejectCd id
$scope.insert = (disc_id) ->
xo.vm.insertCd id, disc_id, true
$scope.vmClipboard = ''
$scope.setClipboard = (text) ->
$scope.vmClipboard = text
$scope.$applyAsync()
$scope.shutdownHost = (id) ->
modal.confirm({
title: 'Shutdown host'
message: 'Are you sure you want to shutdown this host?'
}).then ->
xo.host.stop id
$scope.rebootHost = (id) ->
modal.confirm({
title: 'Reboot host'
message: 'Are you sure you want to reboot this host? It will be disabled then rebooted'
}).then ->
xo.host.restart id
$scope.startHost = (id) ->
xo.host.start id
clipboard = new Clipboard('.copy')
clipboard.on('error', (e) -> console.log('Clipboard', e))
# A module exports its name.
.name

View File

@@ -7,24 +7,37 @@
i.xo-icon-console.fa-stack-1x(class = 'xo-color-{{VM.power_state | lowercase}}')
| &nbsp;
a(
ng-if = 'VM.type === "VM"'
class = 'xo-color-{{VM.power_state | lowercase}}'
ui-sref = 'VMs_view({id: VM.id})'
) {{VM.name_label}}
a(
ng-if = 'VM.type === "VM-controller"'
class = 'xo-color-{{VM.power_state | lowercase}}'
ui-sref = 'hosts_view({id: VM.$container})'
) {{VM.name_label}}
.list-group
//- Toolbar
.list-group-item: .row.text-center
.col-sm-6: iso-device(ng-if = 'VM && SRs', vm = 'VM', srs = 'SRs')
.col-sm-3: button.btn.btn-default(
.col-sm-4: iso-device(ng-if = 'VM && SRs', vm = 'VM', srs = 'SRs')
.col-sm-2: button.btn.btn-default(
ng-click = 'vncRemote.sendCtrlAltDel()'
)
i.fa.fa-keyboard-o
| &nbsp;
| Ctrl+Alt+Del
.col-sm-4
.input-group
input#vm-clipboard.form-control(ng-model='vmClipboard' ng-change='vncRemote.pasteToClipboard(vmClipboard)')
span.input-group-btn
button.btn.btn-default.copy(data-clipboard-target='#vm-clipboard' tooltip="Copy text into local clipboard")
i.fa.fa-clipboard
| Copy
//- Action panel
.col-sm-3
.btn-group
.col-sm-2
.btn-group(ng-if = 'VM.type === "VM"')
button.btn.btn-default.inversed(
ng-if = "VM.power_state == ('Running' || 'Paused')"
tooltip = "Stop VM"
@@ -46,9 +59,32 @@
xo-click = "rebootVM(VM.id)"
)
i.fa.fa-refresh.fa-fw
.btn-group(ng-if = 'VM.type === "VM-controller"')
button.btn.btn-default.inversed(
ng-if = "VM.power_state == ('Running' || 'Paused')"
tooltip = "Shutdown Host"
type = "button"
xo-click = "shutdownHost(VM.$container)"
)
i.fa.fa-stop.fa-fw
button.btn.btn-default.inversed(
ng-if = "VM.power_state == ('Halted')"
tooltip = "Start Host"
type = "button"
xo-click = "startHost(VM.$container)"
)
i.fa.fa-play.fa-fw
button.btn.btn-default.inversed(
ng-if = "VM.power_state == ('Running' || 'Paused')"
tooltip = "Reboot Host"
type = "button"
xo-click = "rebootHost(VM.$container)"
)
i.fa.fa-refresh.fa-fw
//- Console
.list-group-item
no-vnc(
url = '{{consoleUrl}}'
remote-control = 'vncRemote'
remote-control = 'vncRemote',
on-clipboard-change = 'setClipboard(clipboardContent)'
)

View File

@@ -0,0 +1,363 @@
'use strict'
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import debounce from 'lodash.debounce'
import filter from 'lodash.filter'
import foreach from 'lodash.foreach'
import xoApi from 'xo-api'
import xoCircleD3 from 'xo-circle-d3'
import xoParallelD3 from 'xo-parallel-d3'
import xoSunburstD3 from 'xo-sunburst-d3'
import view from './view'
export default angular.module('dashboard.dataviz', [
uiRouter,
uiSelect,
xoApi,
xoCircleD3,
xoParallelD3,
xoSunburstD3
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard.dataviz', {
controller: 'Dataviz as ctrl',
data: {
requireAdmin: true
},
url: '/dataviz/:chart',
template: view
})
})
.filter('type', () => {
return function (objects, type) {
if (!type) {
return objects
}
return filter(objects, object => object.type === type)
}
})
.controller('Dataviz', function ($scope, $state) {
$scope.selectedChart = ''
$scope.availablecharts = {
sunburst: {
name: 'Sunburst charts',
imgs: ['images/sunburst.png', 'images/sunburst2.png'],
url: '/dataviz/sunburst'
},
circle: {
name: 'Circles charts',
imgs: ['images/circle1.png', 'images/circle2.png'],
url: '/dataviz/circle'
},
parcoords: {
name: 'VM properties',
imgs: ['images/parcoords.png'],
url: '/dataviz/parcoords'
}
}
$scope.$on('$stateChangeSuccess', function updatePage () {
$scope.selectedChart = $state.params.chart
})
})
.controller('DatavizParcoords', function DatavizParcoords (xoApi, $scope, $timeout, $interval, $state, bytesToSizeFilter) {
let hostsByPool, vmsByContainer, data
data = []
hostsByPool = xoApi.getIndex('hostsByPool')
vmsByContainer = xoApi.getIndex('vmsByContainer')
/* parallel charts */
function populateChartsData () {
foreach(xoApi.getView('pools').all, function (pool, pool_id) {
foreach(hostsByPool[pool_id], function (host, host_id) {
console.log(host_id)
foreach(vmsByContainer[host_id], function (vm, vm_id) {
let nbvdi, vdisize
nbvdi = 0
vdisize = 0
foreach(vm.$VBDs, function (vbd_id) {
let vbd
vbd = xoApi.get(vbd_id)
if (!vbd.is_cd_drive && vbd.attached) {
nbvdi++
vdisize += xoApi.get(vbd.VDI).size
}
})
data.push({
name: vm.name_label,
id: vm_id,
vcpus: vm.CPUs.number,
vifs: vm.VIFs.length,
ram: vm.memory.size / (1024 * 1024 * 1024)/* memory size in GB */,
nbvdi: nbvdi,
vdisize: vdisize / (1024 * 1024 * 1024)/* disk size in Gb */
})
})
})
})
$scope.charts = {
data: data,
labels: {
vcpus: 'vCPUs number',
ram: 'RAM quantity',
vifs: 'VIF number',
nbvdi: 'VDI number',
vdisize: 'Total space'
}
}
}
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
debouncedPopulate()
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
})
.controller('DatavizStorageHierarchical', function DatavizStorageHierarchical (xoApi, $scope, $timeout, $interval, $state, bytesToSizeFilter) {
$scope.charts = {
selected: {},
data: {
name: 'storage',
children: []
},
click: function (d) {
if (d.virtual) {
return
}
switch (d.type) {
case 'pool':
$state.go('pools_view', {
id: d.id
})
break
case 'host':
$state.go('hosts_view', {
id: d.id
})
break
case 'srs':
$state.go('SRs_view', {
id: d.id
})
break
}
}
}
function populateChartsData () {
function populatestorage (root, container_id) {
let srs = filter(xoApi.getIndex('srsByContainer')[container_id], (one_srs) => one_srs.SR_type !== 'iso' && one_srs.SR_type !== 'udev')
foreach(srs, function (one_srs) {
let srs_used_size = 0
const srs_storage = {
name: one_srs.name_label,
id: one_srs.id,
children: [],
size: one_srs.size,
textSize: bytesToSizeFilter(one_srs.size),
type: 'srs'
}
root.size += one_srs.size
foreach(one_srs.VDIs, function (vdi_id) {
let vdi = xoApi.get(vdi_id)
if (vdi && vdi.name_label.indexOf('.iso') === -1) {
let vdi_storage = {
name: vdi.name_label,
id: vdi_id,
size: vdi.size,
textSize: bytesToSizeFilter(vdi.size),
type: 'vdi'
}
srs_used_size += vdi.size
srs_storage.children.push(vdi_storage)
}
})
if (one_srs.size > srs_used_size) {// some unallocated space
srs_storage.children.push({
color: 'white',
name: 'Free',
id: 'free' + one_srs.id,
size: one_srs.size - srs_used_size,
textSize: bytesToSizeFilter(one_srs.size - srs_used_size),
type: 'vdi',
virtual: true
})
}
root.children.push(srs_storage)
})
root.textSize = bytesToSizeFilter(root.size)
}
let storage_children,
pools,
hostsByPool,
pool_shared_storage
storage_children = []
pools = xoApi.getView('pools')
hostsByPool = xoApi.getIndex('hostsByPool')
foreach(pools.all, function (pool, pool_id) {
let pool_storage, hosts
pool_storage = {
name: pool.name_label || 'no pool',
id: pool_id,
children: [],
size: 0,
color: pool.name_label ? null : 'white',
type: 'pool',
virtual: !pool.name_label
}
pool_shared_storage = {
name: 'Shared',
id: 'Shared' + pool_id,
children: [],
size: 0,
type: 'host',
virtual: true
}
populatestorage(pool_shared_storage, pool_id)
pool_storage.children.push(pool_shared_storage)
pool_storage.size += pool_shared_storage.size
// by hosts
hosts = hostsByPool[pool_id]
foreach(hosts, function (host, host_id) {
// there's also SR attached top
let host_storage = {
name: host.name_label,
id: host.id,
children: [],
size: 0,
type: 'host'
}
populatestorage(host_storage, host_id)
pool_storage.size += host_storage.size
pool_storage.children.push(host_storage)
})
pool_storage.textSize = bytesToSizeFilter(pool_storage.size)
storage_children.push(pool_storage)
})
$scope.charts.data.children = storage_children
}
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
debouncedPopulate()
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
})
.controller('DatavizRamHierarchical', function DatavizRamHierarchical (xoApi, $scope, $timeout, $state, bytesToSizeFilter) {
$scope.charts = {
selected: {},
data: {
name: 'ram',
children: []
},
click: function (d) {
if (d.virtual) {
return
}
switch (d.type) {
case 'pool':
$state.go('pools_view', {id: d.id})
break
case 'host':
$state.go('hosts_view', {id: d.id})
break
case 'vm':
$state.go('VMs_view', {id: d.id})
break
}
}
}
function populateChartsData () {
let ram_children,
pools,
vmsByContainer,
hostsByPool
ram_children = []
pools = xoApi.getView('pools')
vmsByContainer = xoApi.getIndex('vmsByContainer')
hostsByPool = xoApi.getIndex('hostsByPool')
foreach(pools.all, function (pool, pool_id) {
let pool_ram, hosts
// by hosts
pool_ram = {
name: pool.name_label || 'no pool',
id: pool_id,
children: [],
size: 0,
color: pool.name_label ? null : 'white',
type: 'pool',
virtual: !pool.name_label
}
hosts = hostsByPool[pool_id]
foreach(hosts, function (host, host_id) {
// there's also SR attached top
let vm_ram_size = 0
let host_ram = {
name: host.name_label,
id: host_id,
children: [],
size: host.memory.size,
type: 'host'
}
let VMs = vmsByContainer[host_id]
foreach(VMs, function (VM, vm_id) {
let vm_ram = {
name: VM.name_label,
id: vm_id,
size: VM.memory.size,
textSize: bytesToSizeFilter(VM.memory.size),
type: 'vm'
}
if (vm_ram.size) {
vm_ram_size += vm_ram.size
host_ram.children.push(vm_ram)
}
})
if (host_ram.size !== vm_ram_size) {
host_ram.children.push({
color: 'white',
name: 'Free',
id: 'free' + host.id,
size: host.memory.size - vm_ram_size,
textSize: bytesToSizeFilter(host.memory.size - vm_ram_size),
type: 'vm',
virtual: true
})
}
host_ram.textSize = bytesToSizeFilter(host_ram.size)
pool_ram.size += host_ram.size
pool_ram.children.push(host_ram)
})
if (pool_ram.children.length) {
pool_ram.textSize = bytesToSizeFilter(pool_ram.size)
ram_children.push(pool_ram)
}
})
$scope.charts.data.children = ram_children
}
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
debouncedPopulate()
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
})
// A module exports its name.
.name

View File

@@ -0,0 +1,85 @@
.grid-sm(ng-if="!selectedChart")
.panel.panel-default
p.page-title
i.fa.fa-pie-chart
| Dataviz
.panel-body.text-center
.chart-selector(
ng-repeat="(id,chart) in availablecharts"
ui-sref="dashboard.dataviz({chart:id})")
div {{chart.name }}
img.img-thumbnail(
ng-repeat="img in chart.imgs"
ng-src="{{img}}"
)
.grid-sm(ng-if="selectedChart =='sunburst'")
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| Memory usage
.panel-body.text-center(
ng-controller="DatavizRamHierarchical as ram"
style="position:relative"
)
sunburst-chart(
click="charts.click(d)"
chart-data="charts.data"
)
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr
| Storage
.panel-body.text-center(
ng-controller="DatavizStorageHierarchical as storage"
style="position:relative"
)
sunburst-chart(
click="charts.click(d)"
chart-data="charts.data"
)
.grid-sm(ng-if="selectedChart =='circle'")
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| Memory usage
.panel-body.text-center(
ng-controller="DatavizRamHierarchical as ram"
style="position:relative"
)
circle-chart(
click="charts.click(d)"
chart-data="charts.data"
)
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr
| Storage
.panel-body.text-center(
ng-controller="DatavizStorageHierarchical as storage"
style="position:relative"
)
circle-chart(
click="charts.click(d)"
chart-data="charts.data"
)
.grid-sm(ng-if="selectedChart == 'parcoords'")
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| VMs properties
.panel-body.text-center(
ng-controller="DatavizParcoords as parcoords"
)
parallel-chart(
click="charts.click(d)"
chart-labels="charts.labels"
chart-data="charts.data"
)

View File

@@ -0,0 +1,89 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import filter from 'lodash.filter'
import forEach from 'lodash.foreach'
import xoApi from 'xo-api'
import view from './view'
export default angular.module('dashboard.health', [
uiRouter,
xoApi
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard.health', {
controller: 'Health as ctrl',
data: {
requireAdmin: true
},
url: '/health',
template: view
})
})
.controller('Health', function (xo, xoApi, $scope, modal) {
this.currentVdiPage = 1
this.currentVmPage = 1
const vms = xoApi.getView('VM-snapshot').all
const vdis = xoApi.getView('VDI-snapshot').all
const srs = xoApi.getView('SR').all
$scope.$watchCollection(() => vdis, () => {
const orphanVdiSnapshots = filter(vdis, vdi => vdi && !vdi.$snapshot_of)
this.orphanVdiSnapshots = orphanVdiSnapshots
})
$scope.$watchCollection(() => vms, () => {
const orphanVmSnapshots = filter(vms, vm => vm && !vm.$snapshot_of)
this.orphanVmSnapshots = orphanVmSnapshots
})
$scope.$watchCollection(() => srs, () => {
const warningSrs = filter(srs, sr => sr.content_type !== 'iso' && (sr.physical_usage / sr.size) >= 0.8 && (sr.physical_usage / sr.size) < 0.9)
const dangerSrs = filter(srs, sr => sr.content_type !== 'iso' && (sr.physical_usage / sr.size) >= 0.9)
this.warningSrs = warningSrs
this.dangerSrs = dangerSrs
})
this.selectedVdiForDelete = {}
this.selectedVmForDelete = {}
this.deleteVdiSnapshot = function (id) {
modal.confirm({
title: 'VDI snapshot deletion',
message: 'Are you sure you want to delete this snapshot?'
}).then(() => xo.vdi.delete(id))
}
this.deleteVmSnapshot = function (id) {
modal.confirm({
title: 'VM snapshot deletion',
message: 'Are you sure you want to delete this snapshot? (including its disks)'
}).then(() => xo.vm.delete(id, true))
}
this.deleteSelectedVdis = function () {
return modal.confirm({
title: 'VDI snapshot deletion',
message: 'Are you sure you want to delete all selected VDI snapshots? This operation is irreversible.'
}).then(() => {
forEach(this.selectedVdiForDelete, (selected, id) => console.log(id))
forEach(this.selectedVdiForDelete, (selected, id) => selected && xo.vdi.delete(id))
this.selectedVdiForDelete = {}
})
}
this.deleteSelectedVms = function () {
return modal.confirm({
title: 'VM snapshot deletion',
message: 'Are you sure you want to delete all selected VM snapshots? This operation is irreversible.'
}).then(() => {
forEach(this.selectedVmForDelete, (selected, id) => selected && xo.vm.delete(id, true))
this.selectedVmForDelete = {}
})
}
})
.name

View File

@@ -0,0 +1,88 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-heartbeat
| Health
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-hdd-o
| Orphaned VDI snapshots
.panel-body
.center(ng-if = 'ctrl.orphanVdiSnapshots | isEmpty') No orphaned snapshots found
table.table.table-hover(ng-if = 'ctrl.orphanVdiSnapshots | isNotEmpty')
tr
th Name
th Description
th Tags
th Size
th SR
span.pull-right: button.btn.btn-danger(xo-click = 'ctrl.deleteSelectedVdis()', tooltip = 'Delete selected snapshots'): i.fa.fa-trash
tr(ng-repeat = 'vdi in ctrl.orphanVdiSnapshots | filter:vdiSearch | orderBy:natural("name_label") | slice:(10*(ctrl.currentVdiPage-1)):(10*ctrl.currentVdiPage) track by vdi.id')
td.oneliner {{ vdi.name_label }}
td.oneliner {{ vdi.name_description }}
td: xo-tag(object = 'vdi')
td {{ vdi.size | bytesToSize}}
td.oneliner
a(xo-sref="SRs_view({id: (vdi.$SR | resolve).id})")
| {{(vdi.$SR | resolve).name_label}} ({{((vdi.$SR | resolve).$container | resolve).name_label}})
span.pull-right
.btn-group.quick-buttons
a(xo-click="ctrl.deleteVdiSnapshot(vdi.id)"): i.fa.fa-trash-o.fa-lg(tooltip="Destroy this snapshot")
input(type = 'checkbox', ng-model = 'ctrl.selectedVdiForDelete[vdi.id]', tooltip = 'select for deletion')
.form-inline
.input-group
.input-group-addon: i.fa.fa-filter
input.form-control(type = 'text', ng-model = 'vdiSearch', placeholder = 'Enter your search here')
.center(ng-if = '(ctrl.orphanVdiSnapshots | filter:vdiSearch).length > 10 || ctrl.currentVdiPage > 1')
pagination(boundary-links="true", total-items="(ctrl.orphanVdiSnapshots | filter:vdiSearch).length", ng-model="ctrl.currentVdiPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-camera
| Orphaned VM snapshots
.panel-body
.center(ng-if = 'ctrl.orphanVmSnapshots | isEmpty') No orphaned snapshots found
table.table.table-hover(ng-if = 'ctrl.orphanVmSnapshots | isNotEmpty')
tr
th Name
th Description
th OS
th Container
span.pull-right: button.btn.btn-danger(xo-click = 'ctrl.deleteSelectedVms()', tooltip = 'Delete selected snapshots'): i.fa.fa-trash
tr(ng-repeat = 'vm in ctrl.orphanVmSnapshots | orderBy:natural("name_label") | slice:(10*(ctrl.currentVmPage-1)):(10*ctrl.currentVmPage) track by vm.id')
td.oneliner
i.xo-icon-working(ng-if="vm.current_operations | isNotEmpty", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="vm.current_operations | isEmpty", tooltip="{{vm.power_state}}")
| &nbsp;&nbsp;{{ vm.name_label }}
td.oneliner {{ vm.name_description }}
td.onliner {{ vm.os_version.name }}
td.oneliner {{ (vm.$container | resolve).name_label }}
span.pull-right
.btn-group.quick-buttons
a(xo-click="ctrl.deleteVmSnapshot(vm.id)"): i.fa.fa-trash-o.fa-lg(tooltip="Destroy this snapshot")
input(type = 'checkbox', ng-model = 'ctrl.selectedVmForDelete[vm.id]', tooltip = 'select for deletion')
.form-inline
.input-group
.input-group-addon: i.fa.fa-filter
input.form-control(type = 'text', ng-model = 'vmSearch', placeholder = 'Enter your search here')
.center(ng-if = '(ctrl.orphanVmSnapshots | filter:vmSearch).length > 10 || ctrl.currentVmPage > 1')
pagination(boundary-links="true", total-items="(ctrl.orphanVmSnapshots | filter:vmSearch).length", ng-model="ctrl.currentVmPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-database
| SR Warnings
.panel-body
.center(ng-if = '(ctrl.warningSrs | isEmpty) && (ctrl.dangerSrs | isEmpty)') No warnings found
table.table.table-hover(ng-if = '(ctrl.warningSrs | isNotEmpty) || (ctrl.dangerSrs | isNotEmpty)')
tr
th SR
th Physical usage
tr(ng-repeat = 'sr in ctrl.warningSrs')
td: a(xo-sref="SRs_view({id: sr.id})") {{ sr.name_label }} ({{ (sr.$container | resolve).name_label }})
td: span.label.label-warning {{ [sr.physical_usage, sr.size] | percentage }}
tr(ng-repeat = 'sr in ctrl.dangerSrs')
td: a(xo-sref="SRs_view({id: sr.id})") {{ sr.name_label }} ({{ (sr.$container | resolve).name_label }})
td: span.label.label-danger {{ [sr.physical_usage, sr.size] | percentage }}

View File

@@ -0,0 +1,43 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import dataviz from './dataviz'
import filter from 'lodash.filter'
import health from './health'
import stats from './stats'
import overview from './overview'
import view from './view'
export default angular.module('dashboard', [
uiRouter,
dataviz,
health,
stats,
overview
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard', {
abstract: true,
data: {
requireAdmin: true
},
template: view,
url: '/dashboard'
})
// Redirect to default sub-state.
$stateProvider.state('dashboard.index', {
url: '',
controller: function ($state) {
$state.go('dashboard.overview')
}
})
})
.filter('underStat', () => {
let isUnderStat = object => object.type === 'host' || object.type === 'VM'
return objects => filter(objects, isUnderStat)
})
.name

View File

@@ -0,0 +1,177 @@
'use strict'
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import clone from 'lodash.clonedeep'
import debounce from 'lodash.debounce'
import foreach from 'lodash.foreach'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
export default angular.module('dashboard.overview', [
uiRouter,
uiSelect,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard.overview', {
controller: 'Overview as ctrl',
data: {
requireAdmin: true
},
url: '/overview',
template: view
})
})
.controller('Overview', function ($scope, $window, xoApi, xo, $timeout, bytesToSizeFilter, modal) {
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
angular.extend($scope, {
pools: {
nb: 0
},
hosts: {
nb: 0
},
vms: {
nb: 0,
running: 0,
halted: 0,
action: 0
},
ram: [0, 0],
cpu: [0, 0],
srs: []
})
$scope.installAllPatches = function () {
modal.confirm({
title: 'Install all the missing patches',
message: 'Are you sure you want to install all the missing patches? This could take a while...'
}).then(() =>
foreach($scope.pools.all, function (pool, pool_id) {
let pool_hosts = $scope.hostsByPool[pool_id]
foreach(pool_hosts, function (host, host_id) {
console.log('Installing all missing patches on host ', host_id)
xo.host.installAllPatches(host_id)
})
})
)
}
$scope.installHostPatches = function (hostId) {
modal.confirm({
title: 'Update host (' + $scope.nbUpdates[hostId] + ' patch(es))',
message: 'Are you sure you want to install all the missing patches on this host? This could take a while...'
}).then(() => {
console.log('Installing all missing patches on host ', hostId)
xo.host.installAllPatches(hostId)
})
}
const nbUpdates = $scope.nbUpdates = {}
function populateChartsData () {
let pools,
vmsByContainer,
hostsByPool,
nb_hosts,
nb_pools,
vms,
srsByContainer,
srs
nb_pools = 0
nb_hosts = 0
vms = {
nb: 0,
states: [0, 0, 0, 0]
}
const runningStateToIndex = {
Running: 0,
Halted: 1,
Suspended: 2,
Action: 3
}
nb_pools = 0
srs = []
// update vdi, set them to the right host
$scope.pools = pools = xoApi.getView('pools')
srsByContainer = xoApi.getIndex('srsByContainer')
vmsByContainer = xoApi.getIndex('vmsByContainer')
$scope.hostsByPool = hostsByPool = xoApi.getIndex('hostsByPool')
foreach(pools.all, function (pool, pool_id) {
let pool_hosts = hostsByPool[pool_id]
foreach(pool_hosts, function (host, host_id) {
if (host_id in nbUpdates) {
return
}
xo.host.listMissingPatches(host_id)
.then(result => {
nbUpdates[host_id] = result.length
}
)
})
})
foreach(pools.all, function (pool, pool_id) {
nb_pools++
let pool_srs = srsByContainer[pool_id]
foreach(pool_srs, (one_srs) => {
if (one_srs.SR_type !== 'iso' && one_srs.SR_type !== 'udev') {
one_srs = clone(one_srs)
one_srs.ratio = one_srs.size ? one_srs.physical_usage / one_srs.size : 0
one_srs.pool_label = pool.name_label
srs.push(one_srs)
}
})
let VMs = vmsByContainer[pool_id]
foreach(VMs, function (VM) {
// non running VM
vms.states[runningStateToIndex[VM['power_state']]]++
vms.nb++
})
let hosts = hostsByPool[pool_id]
foreach(hosts, function (host, host_id) {
let hosts_srs = srsByContainer[host_id]
foreach(hosts_srs, (one_srs) => {
if (one_srs.SR_type !== 'iso' && one_srs.SR_type !== 'udev') {
one_srs = clone(one_srs)
one_srs.ratio = one_srs.size ? one_srs.physical_usage / one_srs.size : 0
one_srs.host_label = host.name_label
one_srs.pool_label = pool.name_label
srs.push(one_srs)
}
})
nb_hosts++
let VMs = vmsByContainer[host_id]
foreach(VMs, function (VM) {
vms.states[runningStateToIndex[VM['power_state']]]++
vms.nb++
})
})
})
$scope.hosts.nb = nb_hosts
$scope.vms = vms
$scope.pools.nb = nb_pools
$scope.srs = srs
$scope.ram = [xoApi.stats.$memory.usage, xoApi.stats.$memory.size - xoApi.stats.$memory.usage]
$scope.cpu = [[xoApi.stats.$vCPUs], [xoApi.stats.$CPUs]]
}
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
debouncedPopulate()
$scope.$on('$destroy', xoApi.onUpdate(debouncedPopulate))
})
.name

View File

@@ -0,0 +1,139 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-dashboard
| Dashboard
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cloud
| Pools
.panel-body.text-center
p.big-stat {{pools.nb}}
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-server
| Hosts
.panel-body.text-center
p.big-stat {{hosts.nb}}
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-desktop
| VMs
.panel-body.text-center
p.big-stat {{vms.nb}}
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| Global RAM usage
.panel-body.text-center
canvas(
id="doughnut"
class="chart chart-doughnut"
data="ram"
labels="['Used', 'Free']"
options='{responsive: false,tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}'
)
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-dashboard
| vCPUs/CPUs
.panel-body.text-center
canvas(
id="bar"
class="chart chart-bar"
data="cpu"
labels="['']"
series="['vCPUs','CPUs']"
options="{scaleShowGridLines: false, barDatasetSpacing : 10, showScale: false, responsive: false}"
)
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-question-circle
| VMs power state
.panel-body.text-center
canvas(
id="pie"
class="chart chart-pie"
data="vms.states"
labels="['Running', 'Halted', 'Suspended', 'Action']"
colours="['#5cb85c', '#d9534f', '#5bc0de', '#f0ad4e']"
options="{responsive: false}"
)
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-database
| Storage
.panel-body
table.table.table-hover
tr
th Name
th pool
th host
th Format
th Size
th Physical/Allocated usage
//- TODO: display PBD status for each SR of this host (connected or not)
//- Shared SR
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in srs | orderBy:'-ratio' track by SR.id")
td.oneliner
| {{SR.name_label}}
td.oneliner
| {{SR.pool_label}}
td.oneliner
| {{SR.host_label}}
td {{SR.SR_type}}
td {{SR.size | bytesToSize}}
td
.progress-condensed
.progress-bar(
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{SR.physical_usage}}",
aria-valuemax="{{SR.size}}",
style="width: {{[SR.physical_usage, SR.size] | percentage}}",
tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}"
)
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-refresh
| Updates
span.quick-edit(
tooltip="Update all"
ng-click="installAllPatches()"
)
i.fa.fa-download.fa-fw
.panel-body
table.table.table-hover
tr
th Pool
th Host
th Description
th Missing patches
th Install
tbody(ng-repeat="pool in pools.all | orderBy:'name_label'")
tr( ng-repeat="host in hostsByPool[pool.id]" ng-if="nbUpdates[host.id]")
td.oneliner
| {{ pool.name_label }}
td.oneliner
| {{ host.name_label }}
td.oneliner
| {{ host.name_description }}
td {{ nbUpdates[host.id] }}
td
button.btn.btn-success(ng-click="installHostPatches(host.id)" tooltip="Install {{ nbUpdates[host.id] }} patch(es)")
| Update host

View File

@@ -0,0 +1,363 @@
import angular from 'angular'
import Bluebird from 'bluebird'
import uiRouter from 'angular-ui-router'
import filter from 'lodash.filter'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import sortBy from 'lodash.sortby'
import xoApi from 'xo-api'
import xoHorizon from'xo-horizon'
import xoServices from 'xo-services'
import xoWeekHeatmap from'xo-week-heatmap'
import view from './view'
export default angular.module('dashboard.stats', [
uiRouter,
xoApi,
xoHorizon,
xoServices,
xoWeekHeatmap
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard.stats', {
controller: 'stats as bigController',
data: {
requireAdmin: true
},
url: '/stats',
template: view
})
})
.filter('type', () => {
return function (objects, type) {
if (!type) {
return objects
}
return filter(objects, object => object.type === type)
}
})
.controller('stats', function () {})
.controller('statsHeatmap', function (xoApi, xo, xoAggregate, notify, bytesToSizeFilter) {
this.charts = {
heatmap: null
}
this.objects = xoApi.all
this.prepareTypeFilter = function (selection) {
const object = selection[0]
this.typeFilter = object && object.type || undefined
}
this.selectAll = function (type) {
this.selected = filter(this.objects, object =>
(object.type === type && object.power_state === 'Running'))
this.typeFilter = type
}
this.prepareMetrics = function (objects) {
this.chosen = objects && objects.slice()
this.metrics = undefined
this.selectedMetric = undefined
if (this.chosen && this.chosen.length) {
this.loadingMetrics = true
const statPromises = []
forEach(this.chosen, object => {
const apiType = (object.type === 'host' && 'host') || (object.type === 'VM' && 'vm') || undefined
if (!apiType) {
notify.error({
title: 'Unhandled object ' + (objects.name_label || ''),
message: 'There is no stats available for this type of objects'
})
object._ignored = true
} else {
delete object._ignored
statPromises.push(
xo[apiType].refreshStats(object.id, 'hours') // hours granularity (7 * 24 hours)
.then(result => {
if (result.stats === undefined) {
object._ignored = true
throw new Error('No stats')
}
return {object, result}
})
.catch(error => {
error.object = object
object._ignored = true
throw error
})
)
}
})
Bluebird.settle(statPromises)
.then(stats => {
const averageMetrics = {}
let averageObjectLayers = {}
let averageCPULayers = 0
forEach(stats, statePromiseInspection => { // One object...
if (statePromiseInspection.isRejected()) {
notify.warning({
title: 'Error fetching stats',
message: 'Metrics do not include ' + statePromiseInspection.reason().object.name_label
})
} else if (statePromiseInspection.isFulfilled()) {
const {object, result} = statePromiseInspection.value()
// Make date array
result.stats.date = []
let timestamp = result.endTimestamp
for (let i = result.stats.memory.length - 1; i >= 0; i--) {
result.stats.date.unshift(timestamp)
timestamp -= 3600
}
const averageCPU = averageMetrics['All CPUs'] && averageMetrics['All CPUs'].values || []
forEach(result.stats.cpus, (values, metricKey) => { // Every CPU metric of this object
metricKey = 'CPU ' + metricKey
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
averageCPULayers++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(values, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
if (averageCPU[key] === undefined) { // first overall value
averageCPU.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous overall value
averageCPU[key].value = (averageCPU[key].value * (averageCPULayers - 1) + value) / averageCPULayers
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues
}
})
averageMetrics['All CPUs'] = {
key: 'All CPUs',
values: averageCPU
}
forEach(result.stats.vifs, (vif, vifType) => {
const rw = (vifType === 'rx') ? 'out' : 'in'
forEach(vif, (values, metricKey) => {
metricKey = 'Network ' + metricKey + ' ' + rw
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(values, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues,
filter: bytesToSizeFilter
}
})
})
forEach(result.stats.pifs, (pif, pifType) => {
const rw = (pifType === 'rx') ? 'out' : 'in'
forEach(pif, (values, metricKey) => {
metricKey = 'NIC ' + metricKey + ' ' + rw
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(values, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues,
filter: bytesToSizeFilter
}
})
})
forEach(result.stats.xvds, (xvd, xvdType) => {
const rw = (xvdType === 'r') ? 'read' : 'write'
forEach(xvd, (values, metricKey) => {
metricKey = 'Disk ' + metricKey + ' ' + rw
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(values, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues,
filter: bytesToSizeFilter
}
})
})
if (result.stats.load) {
const metricKey = 'Load average'
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(result.stats.load, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value,
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues
}
}
if (result.stats.memoryUsed) {
const metricKey = 'RAM Used'
averageObjectLayers[metricKey] !== undefined || (averageObjectLayers[metricKey] = 0)
averageObjectLayers[metricKey]++
const mapValues = averageMetrics[metricKey] && averageMetrics[metricKey].values || [] // already fed or not
forEach(result.stats.memoryUsed, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value * (object.type === 'host' ? 1024 : 1),
date: +result.stats.date[key] * 1000
})
} else { // average with previous
mapValues[key].value = ((mapValues[key].value || 0) * (averageObjectLayers[metricKey] - 1) + (+value)) / averageObjectLayers[metricKey]
}
})
averageMetrics[metricKey] = {
key: metricKey,
values: mapValues,
filter: bytesToSizeFilter
}
}
}
})
this.metrics = sortBy(averageMetrics, (_, key) => key)
this.loadingMetrics = false
})
}
}
})
.controller('statsHorizons', function ($scope, xoApi, xoAggregate, xo, $timeout) {
let ctrl, stats
ctrl = this
ctrl.synchronizescale = true
ctrl.objects = xoApi.all
ctrl.chosen = []
this.prepareTypeFilter = function (selection) {
const object = selection[0]
ctrl.typeFilter = object && object.type || undefined
}
this.selectAll = function (type) {
ctrl.selected = filter(ctrl.objects, object =>
(object.type === type && object.power_state === 'Running'))
ctrl.typeFilter = type
}
this.prepareMetrics = function (objects) {
ctrl.chosen = objects
ctrl.selectedMetric = null
ctrl.loadingMetrics = true
xoAggregate
.refreshStats(ctrl.chosen, 'hours')
.then(function (result) {
stats = result
ctrl.metrics = stats.keys
ctrl.stats = {}
// $timeout(refreshStats, 1000)
ctrl.loadingMetrics = false
})
.catch(function (e) {
console.log(' ERROR ', e)
})
}
this.toggleSynchronizeScale = function () {
ctrl.synchronizescale = !ctrl.synchronizescale
if (ctrl.selectedMetric) {
ctrl.prepareStat()
}
}
this.prepareStat = function () {
let min, max
max = 0
min = 0
ctrl.stats = {}
// compute a global extent => the chart will have the same scale
if (ctrl.synchronizescale) {
forEach(stats.details, function (stat, object_id) {
forEach(stat[ctrl.selectedMetric], function (val) {
if (!isNaN(val.value)) {
max = Math.max(val.value || 0, max)
}
})
})
ctrl.extents = [min, max]
} else {
ctrl.extents = null
}
forEach(stats.details, function (stat, object_id) {
const label = find(ctrl.chosen, {id: object_id})
ctrl.stats[label.name_label] = stat[ctrl.selectedMetric]
})
}
})
.name

View File

@@ -0,0 +1,155 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-bar-chart
| Stats
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-fire
| Weekly Heatmap
.panel-body(ng-controller='statsHeatmap as heatmap')
| {{heatmap.toto}}
form
.grid-sm
.grid-cell.grid--gutters
.container-fluid
.form-group
ui-select.form-control(ng-model = 'heatmap.selected', ng-change = 'heatmap.prepareTypeFilter(heatmap.selected)', multiple, close-on-select = 'false')
ui-select-match(placeholder = 'Choose an object')
i(class = 'xo-icon-{{ $item.type | lowercase }}')
| {{ $item.name_label }}
ui-select-choices(repeat = 'object in heatmap.objects | underStat | type:heatmap.typeFilter | filter:{ power_state: "Running" } | filter:$select.search | orderBy:["type", "name_label"] track by object.id')
div
i(class = 'xo-icon-{{ object.type | lowercase }}')
| {{ object.name_label }}
span(ng-if='(object.type === "SR" || object.type === "VM") && object.$container')
| ({{ (object.$container | resolve).name_label }})
//- br
.btn-group(role = 'group')
button.btn.btn-default(ng-click = 'heatmap.selected = []', tooltip = 'Clear selection')
i.fa.fa-times
button.btn.btn-default(ng-click = 'heatmap.selectAll("VM")', tooltip = 'Choose all VMs')
i.xo-icon-vm
button.btn.btn-default(ng-click = 'heatmap.selectAll("host")', tooltip = 'Choose all hosts')
i.xo-icon-host
button.btn.btn-success(ng-click = 'heatmap.prepareMetrics(heatmap.selected)', tooltip = 'Load metrics')
i.fa.fa-check
| &nbsp;Select
.grid-cell.grid--gutters
.container-fluid
span(ng-if = 'heatmap.loadingMetrics')
| Loading metrics ...&nbsp;
i.fa.fa-circle-o-notch.fa-spin
.form-group(ng-if = 'heatmap.metrics')
ui-select(ng-model = 'heatmap.selectedMetric')
ui-select-match(placeholder = 'Choose a metric') {{ $select.selected.key }}
ui-select-choices(repeat = 'metric in heatmap.metrics | filter:$select.search | orderBy:["key"]') {{ metric.key }}
br
p.text-center(ng-if = 'heatmap.chosen.length')
span(ng-repeat = 'object in heatmap.chosen', ng-class = '{"text-danger": object._ignored}')
i(class = 'xo-icon-{{ object.type | lowercase }}')
| &nbsp;
span(ng-if = '!object._ignored') {{ object.name_label }}
del(ng-if = 'object._ignored') {{ object.name_label }}
| &nbsp;&ensp;
weekheatmap(ng-if = 'heatmap.selectedMetric', chart-data='heatmap.selectedMetric')
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-fire
| Weekly Charts
.panel-body(ng-controller="statsHorizons as horizons")
form
.grid-sm
.grid-cell.grid--gutters
.container-fluid
.form-group
ui-select.form-control(
ng-model = 'horizons.selected',
ng-change = 'horizons.prepareTypeFilter(horizons.selected)',
multiple,
close-on-select = 'false'
)
ui-select-match(placeholder = 'Choose an object')
i(class = 'xo-icon-{{ $item.type | lowercase }}')
| {{ $item.name_label }}
ui-select-choices(repeat = 'object in horizons.objects | underStat | type:horizons.typeFilter | filter:{ power_state: "Running" } | filter:$select.search | orderBy:["type", "name_label"] track by object.id')
div
i(class = 'xo-icon-{{ object.type | lowercase }}')
| {{ object.name_label }}
span(ng-if='(object.type === "SR" || object.type === "VM") && object.$container')
| ({{ (object.$container | resolve).name_label }})
//- br
.btn-group(role = 'group')
button.btn.btn-default(ng-click = 'horizons.selected = []', tooltip = 'Clear selection')
i.fa.fa-times
button.btn.btn-default(ng-click = 'horizons.selectAll("VM")', tooltip = 'Choose all VMs')
i.xo-icon-vm
button.btn.btn-default(ng-click = 'horizons.selectAll("host")', tooltip = 'Choose all hosts')
i.xo-icon-host
button.btn.btn-success(ng-click = 'horizons.prepareMetrics(horizons.selected)', tooltip = 'Load metrics')
i.fa.fa-check
| &nbsp;Select
.grid-cell.grid--gutters
.container-fluid
span(ng-if = 'horizons.loadingMetrics')
| Loading metrics ...&nbsp;
i.fa.fa-circle-o-notch.fa-spin
.form-group(ng-if = 'horizons.metrics && !horizons.loadingMetrics')
ui-select(ng-model = 'horizons.selectedMetric',ng-change='horizons.prepareStat()')
ui-select-match(placeholder = 'Choose a metric') {{ $select.selected }}
ui-select-choices(repeat = 'metric in horizons.metrics | filter:$select.search | orderBy:["key"]') {{ metric }}
br
button.btn.btn-primary.pull-right(
tooltip="Desynchronize Scale",
ng-click="horizons.toggleSynchronizeScale()"
ng-if='horizons.synchronizescale && horizons.selectedMetric'
)
i.fa.fa-balance-scale
button.btn.btn-default.pull-right(
tooltip="Synchronize Scale",
ng-click="horizons.toggleSynchronizeScale()"
ng-if='!horizons.synchronizescale && horizons.selectedMetric'
)
i.fa.fa-balance-scale
br
p.text-center(ng-if = 'horizons.chosen.length')
span(ng-repeat = 'object in horizons.chosen', ng-class = '{"text-danger": object._ignored}')
i(class = 'xo-icon-{{ object.type | lowercase }}')
| &nbsp;
span(ng-if = '!object._ignored') {{ object.name_label }}
del(ng-if = 'object._ignored') {{ object.name_label }}
| &nbsp;&ensp;
div(
ng-repeat='(label,stat) in horizons.stats'
ng-if='!horizons.loadingMetrics'
style='position:relative'
)
horizon(
ng-if='$first'
chart-data='stat'
show-axis='true'
axis-orientation='top'
selected='horizons.selectedDate'
extent='horizons.extents'
label='{{label}}'
)
horizon(
ng-if='$middle'
chart-data='stat'
selected='horizons.selectedDate'
extent='horizons.extents'
label='{{label}}'
)
horizon(
ng-if='$last && !$first'
chart-data='stat'
show-axis='true'
axis-orientation='bottom'
selected='horizons.selectedDate'
extent='horizons.extents'
label='{{label}}'
)

View File

@@ -0,0 +1,20 @@
.menu-grid
.side-menu
ul.nav
li
a(ui-sref = '.overview', ui-sref-active = 'active')
i.fa.fa-fw.fa-dashboard.fa-menu
span.menu-entry Overview
li
a(ui-sref = '.dataviz({chart:""})')
i.fa.fa-fw.fa-pie-chart.fa-menu
span.menu-entry Dataviz
li
a(ui-sref = '.stats')
i.fa.fa-fw.fa-bar-chart.fa-menu
span.menu-entry Stats
li
a(ui-sref = '.health')
i.fa.fa-fw.fa-heartbeat.fa-menu
span.menu-entry Health
.side-content(ui-view = '')

View File

@@ -17,6 +17,9 @@ form(ng-submit="delete()")
td {{VM.name_description}}
td
input(type="checkbox", ng-model="disks[VM.id]")
p
i.fa.fa-exclamation-triangle
| &nbsp;All snapshots will be deleted too
.modal-footer
button.btn.btn-primary(type="submit")
| Delete

View File

@@ -4,7 +4,7 @@ import uiBootstrap from 'angular-ui-bootstrap'
// ===================================================================
export default angular.module('xoWebApp.genericModal', [
uiBootstrap
uiBootstrap
])
.controller('GenericModalCtrl', function ($scope, $modalInstance, options) {
$scope.title = options.title

View File

@@ -5,12 +5,16 @@ map = require 'lodash.map'
omit = require 'lodash.omit'
sum = require 'lodash.sum'
throttle = require 'lodash.throttle'
find = require 'lodash.find'
filter = require 'lodash.filter'
pluck = require 'lodash.pluck'
#=====================================================================
module.exports = angular.module 'xoWebApp.host', [
require 'angular-file-upload'
require 'angular-ui-router'
require 'tag'
]
.config ($stateProvider) ->
$stateProvider.state 'hosts_view',
@@ -155,12 +159,22 @@ module.exports = angular.module 'xoWebApp.host', [
$scope.pool_addHost = (id) ->
xo.host.attach id
$scope.pools = xoApi.getView('pools')
$scope.hostsByPool = xoApi.getIndex('hostsByPool')
$scope.pool_moveHost = (target) ->
modal.confirm({
title: 'Move host to another pool'
message: 'Are you sure you want to move this host?'
}).then ->
xo.pool.mergeInto({ source: $scope.pool.id, target: target.id })
$scope.pool_removeHost = (id) ->
modal.confirm({
title: 'Remove host from pool'
message: 'Are you sure you want to detach this host from its pool? It will be automatically rebooted'
message: 'Are you sure you want to detach this host from its pool? It will be automatically rebooted AND LOCAL STORAGE WILL BE ERASED.'
}).then ->
xo.host.detach id
$scope.rebootHost = (id) ->
modal.confirm({
title: 'Reboot host'
@@ -201,6 +215,14 @@ module.exports = angular.module 'xoWebApp.host', [
}).then ->
xo.host.stop id
$scope.emergencyShutdownHost = (hostId) ->
modal.confirm({
title: 'Shutdown host'
message: 'Are you sure you want to suspend all the VMs on this host and shut the host down?'
}).then ->
xo.host.emergencyShutdownHost hostId
$scope.saveHost = ($data) ->
{host} = $scope
{name_label, name_description, enabled} = $data
@@ -227,9 +249,10 @@ module.exports = angular.module 'xoWebApp.host', [
title: 'Log deletion'
message: 'Are you sure you want to delete all the logs?'
}).then ->
for log in $scope.host.messages
forEach $scope.host.messages, (log) ->
console.log "Remove log #{log.id}"
xo.log.delete log.id
return
$scope.deleteLog = (id) ->
console.log "Remove log #{id}"
@@ -308,6 +331,11 @@ module.exports = angular.module 'xoWebApp.host', [
$scope.creatingNetwork = false
$scope.createNetworkWaiting = false
$scope.isPoolPatch = (patch) ->
return false if $scope.poolPatches is undefined
return $scope.poolPatches.hasOwnProperty(patch.uuid)
$scope.isPoolPatchApplied = (patch) ->
return true if patch.applied
hostPatch = intersection(patch.$host_patches, $scope.host.patches)
@@ -328,25 +356,63 @@ module.exports = angular.module 'xoWebApp.host', [
}
xo.host.installPatch id, patchUid
$scope.installAllPatches = (id) ->
modal.confirm({
title: 'Install all the missing patches'
message: 'Are you sure you want to install all the missing patches on this host? This could take a while...'
}).then ->
console.log('Installing all patches on host ' + id)
xo.host.installAllPatches id
$scope.refreshStats = (id) ->
return xo.host.refreshStats id
.then (result) ->
result.cpuSeries = []
forEach result.cpus, (v,k) ->
result.cpuSeries.push 'CPU ' + k
result.stats.cpuSeries = []
if result.stats.cpus.length >= 12
nValues = result.stats.cpus[0].length
nCpus = result.stats.cpus.length
cpuAVG = (0 for [1..nValues])
forEach result.stats.cpus, (cpu) ->
forEach cpu, (stat, index) ->
cpuAVG[index] += stat
return
return
forEach cpuAVG, (cpu, index) ->
cpuAVG[index] /= nCpus
return
result.stats.cpus = [cpuAVG]
result.stats.cpuSeries.push 'CPU AVG'
else
forEach result.stats.cpus, (v,k) ->
result.stats.cpuSeries.push 'CPU ' + k
return
result.stats.pifSeries = []
pifsArray = []
forEach result.stats.pifs.rx, (v,k) ->
return unless v
result.stats.pifSeries.push '#' + k + ' in'
result.stats.pifSeries.push '#' + k + ' out'
pifsArray.push (v || [])
pifsArray.push (result.stats.pifs.tx[k] || [])
return
result.pifSeries = []
forEach result.pifs, (v,k) ->
result.pifSeries.push '#' + Math.floor(k/2) + ' ' + if k % 2 then 'out' else 'in'
return
forEach result.date, (v,k) ->
result.date[k] = new Date(v*1000).toLocaleTimeString()
forEach result.memoryUsed, (v, k) ->
result.memoryUsed[k] = v*1024
forEach result.memory, (v, k) ->
result.memory[k] = v*1024
$scope.stats = result
result.stats.pifs = pifsArray
forEach result.stats.memoryUsed, (v, k) ->
result.stats.memoryUsed[k] = v*1024
forEach result.stats.memory, (v, k) ->
result.stats.memory[k] = v*1024
result.stats.date = []
timestamp = result.endTimestamp
for i in [result.stats.memory.length-1..0] by -1
result.stats.date.unshift new Date(timestamp*1000).toLocaleTimeString()
timestamp -= 5
$scope.stats = result.stats
$scope.statView = {
cpuOnly: false,
@@ -354,5 +420,23 @@ module.exports = angular.module 'xoWebApp.host', [
netOnly: false,
loadOnly: false
}
$scope.canAdmin = (id = undefined) ->
if id == undefined
id = $scope.host && $scope.host.id
return id && xoApi.canInteract(id, 'administrate') || false
$scope.canOperate = (id = undefined) ->
if id == undefined
id = $scope.host && $scope.host.id
return id && xoApi.canInteract(id, 'operate') || false
$scope.canView = (id = undefined) ->
if id == undefined
id = $scope.host && $scope.host.id
return id && xoApi.canInteract(id, 'view') || false
# A module exports its name.
.name

View File

@@ -1,4 +1,4 @@
.grid
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-host(class="xo-color-{{host.power_state | lowercase}}")
@@ -8,13 +8,15 @@
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
| )
p.center {{host.bios_strings["system-manufacturer"]}} {{host.bios_strings["system-product-name"]}}
.grid
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs(style="color: #e25440;")
i.fa.fa-cogs
| General
span.quick-edit(tooltip="Edit General settings", ng-click="hostSettings.$show()")
span.quick-edit(tooltip="Edit General settings", ng-click="hostSettings.$show()", ng-if = '!hostSettings.$visible && canAdmin()')
i.fa.fa-edit.fa-fw
span.quick-edit(ng-if="hostSettings.$visible", tooltip="Cancel Edition", ng-click="hostSettings.$cancel()")
i.fa.fa-undo.fa-fw
.panel-body
form(editable-form="", name="hostSettings", onbeforesave="saveHost($data)")
dl.dl-horizontal
@@ -31,11 +33,8 @@
span(editable-select="host.enabled", e-ng-options="ap.v as ap.t for ap in [{v: true, t:'Yes'}, {v: false, t:'No'}]", e-name="enabled", e-form="hostSettings")
| {{host.enabled ? 'Yes' : 'No'}}
dt Tags
dd(ng-if="host.tags.length")
span(ng-repeat="tag in host.tags")
span.label.label-primary {{tag}}
dd(ng-if="!host.tags.length")
em No tags.
dd
xo-tag(ng-if = 'host', object = 'host')
dt CPUs
dd {{host.CPUs["cpu_count"]}}x {{host.CPUs["modelname"]}}
dt Hostname
@@ -62,12 +61,12 @@
| Save
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-stats(style="color: #e25440;")
i.xo-icon-stats
| Stats
.panel-body(ng-if="refreshStatControl.running && stats")
div(ng-if="statView.cpuOnly", ng-click="statView.cpuOnly = false")
p.stat-name
i.fa.fa-tachometer
i.xo-icon-cpu
| &nbsp; CPU usage
canvas.chart.chart-line.chart-stat-full(
id="bigCpu"
@@ -80,8 +79,9 @@
)
div(ng-if="statView.ramOnly", ng-click="statView.ramOnly = false")
p.stat-name
i.xo-icon-memory
//- i.fa.fa-bar-chart
i.fa.fa-tasks
//- i.fa.fa-tasks
//- i.fa.fa-server
| &nbsp; RAM usage
canvas.chart.chart-line.chart-stat-full(
@@ -95,7 +95,7 @@
)
div(ng-if="statView.netOnly", ng-click="statView.netOnly = false")
p.stat-name
i.fa.fa-sitemap
i.xo-icon-network
| &nbsp; Network I/O
canvas.chart.chart-line.chart-stat-full(
id="bigNet"
@@ -121,9 +121,9 @@
)
div(ng-if="!statView.netOnly && !statView.loadOnly && !statView.cpuOnly && !statView.ramOnly")
.row
.col-sm-6(ng-click="statView.cpuOnly=true")
.col-md-6(ng-click="statView.cpuOnly=true")
p.stat-name
i.fa.fa-tachometer
i.xo-icon-cpu
| &nbsp; CPU usage
canvas.chart.chart-line.chart-stat-preview(
id="smallCpu"
@@ -133,10 +133,11 @@
colours="['#0000ff', '#9999ff', '#000099', '#5555ff', '#000055']"
options='{responsive: true, maintainAspectRatio: false, showTooltips: false, pointDot: false, showScale: false, animation: false, datasetStrokeWidth: 0.8, scaleOverride: true, scaleSteps: 100, scaleStartValue: 0, scaleStepWidth: 1}'
)
.col-sm-6(ng-click="statView.ramOnly=true")
.col-md-6(ng-click="statView.ramOnly=true")
p.stat-name
i.xo-icon-memory
//- i.fa.fa-bar-chart
i.fa.fa-tasks
//- i.fa.fa-tasks
//- i.fa.fa-server
| &nbsp; RAM usage
canvas.chart.chart-line.chart-stat-preview(
@@ -148,9 +149,9 @@
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.row
.col-sm-6(ng-click="statView.netOnly=true")
.col-md-6(ng-click="statView.netOnly=true")
p.stat-name
i.fa.fa-sitemap
i.xo-icon-network
| &nbsp; Network I/O
canvas.chart.chart-line.chart-stat-preview(
id="smallNet"
@@ -160,7 +161,7 @@
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.col-sm-6(ng-click="statView.loadOnly=true")
.col-md-6(ng-click="statView.loadOnly=true")
p.stat-name
i.fa.fa-cogs
| &nbsp; Load Average
@@ -174,80 +175,101 @@
)
.panel-body(ng-if="!refreshStatControl.running || !stats")
.row
.col-sm-6.col-lg-4
.col-sm-4
p.stat-name CPU usage:
p.center.mid-stat {{vCPUs}}/{{host.CPUs['cpu_count']}}
.col-sm-6.col-lg-4
.col-sm-4
p.stat-name RAM used:
p.center.mid-stat {{host.memory.usage | bytesToSize}}
.col-sm-4.visible-lg
.col-sm-4
p.stat-name Running VMs:
p.center.mid-stat {{vms | count}}
.row
p.center(ng-if="refreshStatControl.running")
i.fa.fa-circle-o-notch.fa-spin.fa-2x
| &nbsp; Fetching stats...
.row.hidden-lg
.col-sm-12
br
p.stat-name {{vms | count}} running VMs
p.center(ng-if="refreshStatControl.running")
i.xo-icon-loading
| &nbsp; Fetching stats...
//- Action panel
.grid
.grid-sm(ng-if = 'canOperate()')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flash(style="color: #e25440;")
i.fa.fa-flash
| Actions
.panel-body.text-center
.grid
.grid-cell.btn-group
button.btn(tooltip="Add SR", type="button", style="width: 90%", xo-sref="SRs_new({container: host.id})")
i.xo-icon-sr.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Add VM", type="button", style="width: 90%", xo-sref="VMs_new({container: host.id})")
i.xo-icon-vm.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Reboot host", type="button", style="width: 90%", xo-click="rebootHost(host.id)")
i.fa.fa-refresh.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Shutdown host", type="button", style="width: 90%", xo-click="shutdownHost(host.id)")
i.fa.fa-power-off.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="host.enabled")
button.btn(tooltip="Disable host", type="button", style="width: 90%", xo-click="disableHost(host.id)")
i.fa.fa-times-circle.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="!host.enabled")
button.btn(tooltip="Enable host", type="button", style="width: 90%", xo-click="enableHost(host.id)")
i.fa.fa-check-circle.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Restart toolstack", type="button", style="width: 90%", xo-click="restartToolStack(host.id)")
i.fa.fa-retweet.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="pool.name_label")
button.btn(tooltip="Remove from pool", style="width: 90%", type="button", xo-click="pool_removeHost(host.id)")
i.fa.fa-cloud-upload.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="!pool.name_label")
button.btn(tooltip="Add to pool", style="width: 90%", type="button", xo-click="pool_addHost(host.id)")
i.fa.fa-cloud-download.fa-2x.fa-fw
.grid-cell.btn-group(style="margin-bottom: 0.5em")
button.btn(
tooltip="Import VM"
type="button"
style="width: 90%"
ng-file-select = 'importVm($files, host.id)'
.grid-sm.grid--gutters
.grid.grid-cell
.grid-cell.btn-group
button.btn(tooltip="Add SR", tooltip-placement="top", type="button", style="width: 90%", xo-sref="SRs_new({container: host.id})", ng-if = 'canAdmin()')
i.xo-icon-sr.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Add VM", tooltip-placement="top", type="button", style="width: 90%", xo-sref="VMs_new({container: host.id})", ng-if = 'canAdmin()')
i.xo-icon-vm.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Reboot host", tooltip-placement="top", type="button", style="width: 90%", xo-click="rebootHost(host.id)", ng-if = 'canOperate()')
i.fa.fa-refresh.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Shutdown host", tooltip-placement="top", type="button", style="width: 90%", xo-click="shutdownHost(host.id)", ng-if = 'canOperate()')
i.fa.fa-power-off.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Suspend all VMs and shutdown host", tooltip-placement="top", type="button", style="width: 90%", xo-click="emergencyShutdownHost(host.id)", ng-if = 'canOperate()')
i.fa.fa-exclamation-triangle.fa-2x.fa-fw
.grid.grid-cell
.grid-cell.btn-group(ng-if="host.enabled")
button.btn(tooltip="Disable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="disableHost(host.id)", ng-if = 'canAdmin()')
i.fa.fa-times-circle.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="!host.enabled")
button.btn(tooltip="Enable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="enableHost(host.id)", ng-if = 'canAdmin()')
i.fa.fa-check-circle.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Restart toolstack", tooltip-placement="top", type="button", style="width: 90%", xo-click="restartToolStack(host.id)", ng-if = 'canAdmin()')
i.fa.fa-retweet.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="pool.name_label && (hostsByPool[pool.id] | count)>1")
button.btn(tooltip="Remove from pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_removeHost(host.id)", ng-if = 'canAdmin()')
i.fa.fa-cloud-upload.fa-2x.fa-fw
.grid-cell.btn-group.dropdown(
ng-if="pool.name_label && (hostsByPool[pool.id] | count)==1"
dropdown
)
i.fa.fa-upload.fa-2x.fa-fw
.grid-cell.btn-group(style="margin-bottom: 0.5em")
button.btn(
tooltip="Host console"
type="button"
style="width: 90%"
xo-sref="consoles_view({id: controller.id})"
)
i.xo-icon-console.fa-2x.fa-fw
button.btn.dropdown-toggle(
ng-if = 'canAdmin()'
dropdown-toggle
tooltip="Move host to another pool"
tooltip-placement="top"
type="button"
style="width: 90%"
)
i.fa.fa-cloud-download.fa-2x.fa-fw
span.caret
ul.dropdown-menu.left(role="menu")
li(ng-repeat="p in pools.all | map | orderBy:natural('name_label') track by p.id" ng-if="p!=pool")
a(xo-click="pool_moveHost(p)")
i.xo-icon-host.fa-fw
| To {{p.name_label}}
.grid-cell.btn-group(ng-if="!pool.name_label")
button.btn(tooltip="Add to pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_addHost(host.id)", ng-if = 'canAdmin()')
i.fa.fa-cloud-download.fa-2x.fa-fw
.grid-cell.btn-group(style="margin-bottom: 0.5em")
button.btn(
ng-if = 'canAdmin()'
tooltip="Import VM"
tooltip-placement="top"
type="button"
style="width: 90%"
ng-file-select = 'importVm($files, host.id)'
)
i.fa.fa-upload.fa-2x.fa-fw
.grid-cell.btn-group(style="margin-bottom: 0.5em")
button.btn(
tooltip="Host console"
tooltip-placement="top"
type="button"
style="width: 90%"
xo-sref="consoles_view({id: controller.id})"
)
i.xo-icon-console.fa-2x.fa-fw
//- TODO: Memory panel
.grid
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory(style="color: #e25440;")
i.xo-icon-memory
| Memory
.panel-body.text-center
.progress
@@ -260,10 +282,10 @@
li Currently used: {{host.memory.usage | bytesToSize}}
li Available: {{host.memory.size-host.memory.usage | bytesToSize}}
//- SR panel
.grid
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr(style="color: #e25440;")
i.xo-icon-sr
| Storage
.panel-body
table.table.table-hover
@@ -271,7 +293,7 @@
th Name
th Format
th Size
th Physical/Allocated usage
th Physical usage
th Type
th Status
//- TODO: display PBD status for each SR of this host (connected or not)
@@ -283,21 +305,20 @@
td {{SR.size | bytesToSize}}
td
.progress-condensed
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | percentage}}", tooltip="Allocated: {{[(SR.usage), SR.size] | percentage}}")
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
td
span.label.label-primary Shared
td(ng-if="SRsToPBDs[SR.id].attached")
span.label.label-success Connected
span.pull-right.btn-group.quick-buttons
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)")
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
i.fa.fa-unlink.fa-lg
td(ng-if="!SRsToPBDs[SR.id].attached")
span.label.label-default Disconnected
span.pull-right.btn-group.quick-buttons
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)")
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
i.fa.fa-link.fa-lg
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)")
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
i.fa.fa-ban.fa-lg
//- Local SR
//- TODO: migrate to SRs and not PBDs when implemented in xo-server spec
@@ -308,27 +329,26 @@
td {{SR.size | bytesToSize}}
td
.progress-condensed
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | percentage}}", tooltip="Allocated: {{[(SR.usage), SR.size] | percentage}}")
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
td
span.label.label-info Local
td(ng-if="SRsToPBDs[SR.id].attached")
span.label.label-success Connected
span.pull-right.btn-group.quick-buttons
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)")
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
i.fa.fa-unlink.fa-lg
td(ng-if="!SRsToPBDs[SR.id].attached")
span.label.label-default Disconnected
span.pull-right.btn-group.quick-buttons
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)")
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
i.fa.fa-link.fa-lg
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)")
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
i.fa.fa-ban.fa-lg
//- Networks/Interfaces panel
.grid
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-network(style="color: #e25440;")
i.xo-icon-network
| Interfaces
.panel-body
table.table.table-hover
@@ -340,7 +360,7 @@
th.col-md-1 Link status
tr(ng-repeat="PIF in host.$PIFs | resolve | orderBy:natural('name_label') track by PIF.id")
td
| {{PIF.device}}
| {{PIF.device}}&nbsp;
span.label.label-primary(ng-if="PIF.management") XAPI
td
span(ng-if="PIF.vlan > -1")
@@ -353,23 +373,23 @@
td(ng-if="PIF.attached")
span.label.label-success Connected
span.pull-right.btn-group.quick-buttons
a(tooltip="Disconnect this interface", xo-click="disconnectPIF(PIF.id)")
a(tooltip="Disconnect this interface", xo-click="disconnectPIF(PIF.id)", ng-if = 'canAdmin()')
i.fa.fa-unlink.fa-lg
td(ng-if="!PIF.attached")
span.label.label-default Disconnected
span.pull-right.btn-group.quick-buttons
a(tooltip="Connect this interface", xo-click="connectPIF(PIF.id)")
a(tooltip="Connect this interface", xo-click="connectPIF(PIF.id)", ng-if = 'canAdmin()')
i.fa.fa-link.fa-lg
a(tooltip="Remove this interface", xo-click="removePIF(PIF.id)")
a(tooltip="Remove this interface", xo-click="removePIF(PIF.id)", ng-if = 'canAdmin()')
i.fa.fa-trash-o.fa-lg
.text-right
button.btn(type="button", ng-class = '{"btn-success": creatingNetwork, "btn-primary": !creatingNetwork}', ng-click="creatingNetwork = !creatingNetwork")
button.btn(type="button", ng-class = '{"btn-success": creatingNetwork, "btn-primary": !creatingNetwork}', ng-click="creatingNetwork = !creatingNetwork", ng-hide = '!canAdmin()', ng-disabled = '!canAdmin()')
i.fa.fa-plus(ng-if = '!creatingNetwork')
i.fa.fa-minus(ng-if = 'creatingNetwork')
| Create Network
br
form.form-inline.text-right#createNetworkForm(ng-if = 'creatingNetwork', name = 'createNetworkForm', ng-submit = 'createNetwork(newNetworkName, newNetworkDescription, newNetworkPIF, newNetworkMTU, newNetworkVlan)')
fieldset(ng-attr-disabled = '{{ createNetworkWaiting ? true : undefined }}')
fieldset(ng-disabled = 'createNetworkWaiting || !canAdmin()')
.form-group
label(for = 'newNetworkPIF') Interface&nbsp;
select.form-control(ng-model = 'newNetworkPIF', ng-change = 'updateMTU(newNetworkPIF)', ng-options='(PIF | resolve).device for PIF in host.$PIFs')
@@ -397,17 +417,17 @@
| Create
span(ng-if = 'createNetworkWaiting')
| &nbsp;
i.fa.fa-spin.fa-circle-o-notch
i.xo-icon-loading-sm
br
//- CPU and Logs panels
.grid
.grid-sm
//- Task panel
.panel.panel-default
.panel-heading.panel-title(ng-if="tasks | isNotEmpty")
i.fa.fa-spinner.fa-pulse(style="color: #e25440;")
i.fa.fa-spinner.fa-pulse
| Pending tasks
.panel-heading.panel-title(ng-if="tasks | isEmpty")
i.fa.fa-spinner(style="color: #e25440;")
i.fa.fa-spinner
| Pending tasks
.panel-body
p.center(ng-if="tasks | isEmpty") No recent tasks
@@ -424,7 +444,7 @@
| {{task.progress*100 | number:1}}%
td.oneliner
| {{task.name_label}}
span.pull-right.btn-group.quick-buttons
span.pull-right.btn-group.quick-buttons(ng-if = 'canAdmin()')
a(xo-click="cancelTask(task.id)")
i.fa.fa-times.fa-lg(tooltip="Cancel this task")
a(xo-click="destroyTask(task.id)")
@@ -434,9 +454,9 @@
//- Logs panel
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-comments(style="color: #e25440;")
i.fa.fa-comments
| Logs
span.quick-edit(ng-if="host.messages | isNotEmpty", tooltip="Remove all logs", ng-click="deleteAllLog()")
span.quick-edit(ng-if="(host.messages | isNotEmpty) && canAdmin()", tooltip="Remove all logs", ng-click="deleteAllLog()")
i.fa.fa-trash-o.fa-fw
.panel-body
p.center(ng-if="host.messages | isEmpty") No recent logs
@@ -447,19 +467,21 @@
td {{message.time*1e3 | date:"medium"}}
td
| {{message.name}}
span.pull-right.btn-group.quick-buttons
span.pull-right.btn-group.quick-buttons(ng-if = 'canAdmin()')
a(xo-click="deleteLog(message.id)")
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
.center(ng-if = '(host.messages | count) > 5')
.center(ng-if = '(host.messages | count) > 5 || currentLogPage > 1')
pagination(boundary-links="true", total-items="host.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.grid
.grid-sm
//- Patches panel
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-file-code-o(style="color: #e25440;")
i.fa.fa-file-code-o
| Patches
span.quick-edit(ng-click="listMissingPatches(host.id)", tooltip="Check for updates")
i.fa.fa-question-circle
span.quick-edit(ng-click="installAllPatches(host.id)", tooltip="Install all the missing patches", style="margin-right:5px", ng-if = 'canAdmin()')
i.fa.fa-download
.panel-body
table.table.table-hover(ng-if="poolPatches || updates")
th.col-sm-2 Name
@@ -467,36 +489,39 @@
th.col-sm-3 Applied/Released date
th.col-sm-1 Size
th.col-sm-1 Status
tr(ng-repeat="patch in updates")
tr(
ng-repeat="patch in updates"
ng-if="!isPoolPatch(patch)"
)
td.oneliner {{patch.name}}
td.oneliner
a(href="{{patch.documentationUrl}}", target="_blank") {{patch.description}}
td.oneliner {{patch.date | date:"medium"}}
td -
td
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to install the patch on this host")
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to install the patch on this host", ng-if = 'canAdmin()')
span.label.label-danger Missing
span.label.label-danger(ng-if = '!canAdmin()') Missing
tr(ng-repeat="patch in poolPatches | map | slice:(5*(currentPatchPage-1)):(5*currentPatchPage)")
td.oneliner {{patch.name_label}}
td.oneliner {{patch.name_description}}
td.oneliner {{patch.name}}
td.oneliner {{patch.description}}
//- TODO: use a proper function for patch date, like poolPatchToHostPatch
td.oneliner {{((patch.$host_patches[0]) | resolve).time*1e3 | date:"medium"}}
td {{patch.size | bytesToSize}}
td
span(ng-if="isPoolPatchApplied(patch)")
span.label.label-success Applied
span(ng-click="installPatch(host.id, patch.id)", ng-if="!isPoolPatchApplied(patch)", tooltip="Click to apply the patch on this host")
span.label.label-warning Not applied
//- span.pull-right.btn-group.quick-buttons
//- a(xo-click="deletePatch(patch.id)")
//- i.fa.fa-trash-o.fa-lg(tooltip="Remove this patch")
.center(ng-if = '(poolPatches | count) > 5')
span(ng-if="!isPoolPatchApplied(patch)")
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to apply the patch on this host", ng-if = 'canAdmin()')
span.label.label-warning Not applied
span.label.label-warning(ng-if = '!canAdmin()') Not applied
.center(ng-if = '(poolPatches | count) > 5 || currentPatchPage > 1')
pagination(boundary-links="true", total-items="poolPatches | count", ng-model="$parent.currentPatchPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.grid
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-plug(style="color: #e25440;")
i.fa.fa-plug
| PCI Devices
.panel-body
p.center(ng-if="!host.$PCIs") No PCI devices available
@@ -510,7 +535,7 @@
pagination(boundary-links="true", total-items="(host.$PCIs | resolve).length", ng-model="$parent.currentPCIPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-desktop(style="color: #e25440;")
i.fa.fa-desktop
| GPUs
.panel-body
p.center(ng-if="host.$PGPUs.length === 0") No GPUs available

View File

@@ -1,5 +1,7 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import xoTag from 'tag'
import includes from 'lodash.includes'
import xoApi from 'xo-api'
@@ -9,7 +11,8 @@ import view from './view'
export default angular.module('xoWebApp.list', [
uiRouter,
xoApi
xoApi,
xoTag
])
.config(function ($stateProvider) {
$stateProvider.state('list', {
@@ -18,7 +21,7 @@ export default angular.module('xoWebApp.list', [
template: view
})
})
.controller('ListCtrl', function (xoApi) {
.controller('ListCtrl', function (xoApi, $scope, $rootScope) {
this.hosts = xoApi.getView('host')
this.pools = xoApi.getView('pool')
this.SRs = xoApi.getView('SR')
@@ -27,7 +30,84 @@ export default angular.module('xoWebApp.list', [
this.hostsByPool = xoApi.getIndex('hostsByPool')
this.runningHostsByPool = xoApi.getIndex('runningHostsByPool')
this.vmsByContainer = xoApi.getIndex('vmsByContainer')
})
$scope.canView = function (id) {
return xoApi.canInteract(id, 'view')
}
$scope.shouldAppear = (obj) => {
// States
const powerState = obj.power_state
// If there is a search option on the power state (running or halted),
// then objects that do not have a power_state (eg: SRs) are not displayed
if (($scope.states['running'] !== 2 || $scope.states['halted'] !== 2) && !powerState) return false
if (powerState) {
if ($scope.states[powerState.toLowerCase()] === 0) return false
if (($scope.states['running'] === 1 || $scope.states['halted'] === 1) &&
$scope.states[powerState.toLowerCase()] !== 1) return false
}
if ($scope.states['disconnected'] !== 2 && !obj.$PBDs) return false
let disconnected = false
if (obj.$PBDs) {
for (const id of obj.$PBDs) {
const pbd = xoApi.get(id)
disconnected |= !pbd.attached
}
if ($scope.states['disconnected'] === 0 && disconnected) return false
if ($scope.states['disconnected'] === 1 && !disconnected) return false
}
// Types
if ($scope.types[obj.type.toLowerCase()] === 0) return false
if ($scope.types[obj.type.toLowerCase()] === 2 && includes($scope.types, 1)) return false
return true
}
const _initOptions = () => {
$scope.types = {
'host': 2,
'pool': 2,
'sr': 2,
'vm': 2
}
$scope.states = {
'running': 2,
'halted': 2,
'disconnected': 2
}
}
_initOptions()
$scope.parsedListFilter = $scope.listFilter
$rootScope.searchParse = () => {
let keyWords = []
const words = $scope.listFilter ? $scope.listFilter.split(' ') : ['']
_initOptions()
for (const word of words) {
let isOption = word.charAt(0) === '*'
const isNegation = word.charAt(0) === '!'
// as long as there is a '!', it is an option. ie !vm <=> !*vm
isOption = isOption || isNegation
let option = (isNegation ? word.substring(1, word.length) : word).toLowerCase()
option = option.charAt(0) === '*' ? option.substring(1, option.length) : option
if (!isOption) {
if (option !== '') keyWords.push(option)
} else {
if ($scope.types.hasOwnProperty(option)) {
$scope.types[option] = isNegation ? 0 : 1
} else if ($scope.states.hasOwnProperty(option)) {
$scope.states[option] = isNegation ? 0 : 1
}
}
}
$scope.parsedListFilter = keyWords.join(' ')
}
$scope.onClick = (type) => {
$rootScope.options[type.toLowerCase()] = !$rootScope.options[type.toLowerCase()]
$rootScope.updateListFilter(type.toLowerCase())
}
})
// A module exports its name.
.name

View File

@@ -1,164 +1,201 @@
.sub-bar
.grid(style="margin-left:1em")
.btn-group.dropdown(dropdown)
a.btn.navbar-btn.dropdown-toggle.filter(dropdown-toggle)
| Types
i.fa.fa-caret-down
ul.dropdown-menu.inverse(role="menu" style="color:white")
li(
ng-repeat = "type in ['VM', 'SR', 'Host', 'Pool']"
ng-click='onClick(type)'
)
| &nbsp
label(ng-click)
i.fa.fa-square-o(ng-if='!options[type.toLowerCase()]')
i.fa.fa-check-square-o(ng-if='options[type.toLowerCase()]')
| {{type}}
.btn-group.dropdown(dropdown)
a.btn.navbar-btn.dropdown-toggle.filter(dropdown-toggle)
| States
i.fa.fa-caret-down
ul.dropdown-menu.inverse(role="menu" style="color:white")
li(
ng-repeat = "state in ['Running', 'Halted', 'Disconnected']"
ng-click='onClick(state)'
)
| &nbsp
label(ng-click)
i.fa.fa-square-o(ng-if='!options[state.toLowerCase()]')
i.fa.fa-check-square-o(ng-if='options[state.toLowerCase()]')
| {{state}}
//- TODO: print a message when no entries.
//- FIXME: Ugly trick to force the results to be under the sub bar.
div(style="margin-top: 50px; visibility: hidden; height: 0") .
//- If it's a (named) pool.
.grid.flat-object(ng-repeat="pool in list.pools.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by pool.id", ng-if="pool.name_label", xo-sref="pools_view({id: pool.id})")
.grid.flat-object(
ng-repeat="pool in list.pools.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by pool.id"
ng-if="pool.name_label && shouldAppear(pool)"
xo-sref="pools_view({id: pool.id})"
)
//- Icon.
.grid-cell.flat-cell.flat-cell-type
i.xo-icon-pool
//- Properties & tags.
.grid-cell
//- Properties.
.grid
.grid-cell
.grid
.grid-cell.flat-cell.flat-cell-name
| {{pool.name_label}}
.grid-cell.flat-cell.flat-cell-description
i {{pool.name_description}}
.grid-cell.flat-cell(ng-init="default_SR = (pool.default_SR | resolve)")
div(ng-if="default_SR")
| Default SR:
a(ui-sref="SRs_view({id: default_SR.id})") {{default_SR.name_label}}
div(ng-if="!default_SR")
em No default SR.
.grid-cell.flat-cell(ng-init="master = (pool.master | resolve)")
div(ng-if="master")
| Master:
a(ui-sref="hosts_view({id: master.id})") {{master.name_label}}
div(ng-if="!master")
em Unknown master.
.grid-cell.flat-cell
div(ng-if="pool.HA_enabled")
| HA enabled
div(ng-if="!pool.HA_enabled")
| HA disabled
.grid-cell.flat-cell
| {{list.runningHostsByPool[pool.id] | count}}/{{list.hostsByPool[pool.id] | count}} hosts
.grid-sm
.grid-cell.flat-cell.flat-cell-name
| {{pool.name_label}}
.grid-cell.flat-cell.flat-cell-description
i {{pool.name_description}}
.grid-cell.flat-cell(ng-init="default_SR = (pool.default_SR | resolve)")
div(ng-if="default_SR")
| Default SR:
a(ui-sref="SRs_view({id: default_SR.id})") {{default_SR.name_label}}
div(ng-if="!default_SR")
em No default SR.
.grid-cell.flat-cell(ng-init="master = (pool.master | resolve)")
div(ng-if="master")
| Master:
a(ui-sref="hosts_view({id: master.id})") {{master.name_label}}
div(ng-if="!master")
em Unknown master.
.grid-cell.flat-cell
div(ng-if="pool.HA_enabled")
| HA enabled
div(ng-if="!pool.HA_enabled")
| HA disabled
.grid-cell.flat-cell
| {{list.runningHostsByPool[pool.id] | count}}/{{list.hostsByPool[pool.id] | count}} hosts
//- /Properties.
//- Tags.
.grid
.grid-cell
.grid-cell.flat-cell-tag
i.fa.fa-tag
span(ng-repeat="tag in pool.tags")
span.label.label-primary {{tag}}
i.fa.fa-tag &nbsp;
xo-tag(object = 'pool')
//- /Tags.
//- /Properties & tags.
//- /Pool.
//- If it's a host.
.grid.flat-object(ng-repeat="host in list.hosts.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by host.id", xo-sref="hosts_view({id: host.id})")
.grid.flat-object(
ng-repeat="host in list.hosts.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by host.id"
ng-if="shouldAppear(host)"
xo-sref="hosts_view({id: host.id})"
)
//- Icon.
.grid-cell.flat-cell.flat-cell-type
i.xo-icon-host(class="xo-color-{{host.power_state | lowercase}}")
//- Properties & tags.
.grid-cell
//- Properties.
.grid
.grid-cell
.grid
.grid-cell.flat-cell.flat-cell-name
| {{host.name_label}}
.grid-cell.flat-cell.flat-cell-description
i {{host.name_description}}
.grid-cell.flat-cell
| Address: {{host.address}}
//- .grid-cell.flat-cell
//- | {{host.$vCPUs}} vCPUs used on {{host.CPUs["cpu_count"]}} cores
.grid-cell.flat-cell
.progress-condensed
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{100*host.memory.usage/host.memory.size}}", aria-valuemax="100", style="width: {{[host.memory.usage, host.memory.size] | percentage}}", tooltip="RAM: {{[host.memory.usage, host.memory.size] | percentage}} allocated")
| {{[host.memory.usage, host.memory.size] | percentage}}
.grid-cell.flat-cell
| {{list.vmsByContainer[host.id] | count}} VMs running
.grid-sm
.grid-cell.flat-cell.flat-cell-name
| {{host.name_label}}
.grid-cell.flat-cell.flat-cell-description
i {{host.name_description}}
.grid-cell.flat-cell
| Address: {{host.address}}
//- .grid-cell.flat-cell
//- | {{host.$vCPUs}} vCPUs used on {{host.CPUs["cpu_count"]}} cores
.grid-cell.flat-cell
.progress-condensed
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{100*host.memory.usage/host.memory.size}}", aria-valuemax="100", style="width: {{[host.memory.usage, host.memory.size] | percentage}}", tooltip="RAM: {{[host.memory.usage, host.memory.size] | percentage}} allocated")
| {{[host.memory.usage, host.memory.size] | percentage}}
.grid-cell.flat-cell
| {{list.vmsByContainer[host.id] | count}} VMs running
//- /Properties.
//- Tags.
.grid
.grid-cell
.grid-cell.flat-cell-tag
i.fa.fa-tag
span(ng-repeat="tag in host.tags")
span.label.label-primary {{tag}}
i.fa.fa-tag &nbsp;
xo-tag(object = 'host')
//- /Tags.
//- /Properties & tags.
//- /Host.
//- If it's a VM.
.grid.flat-object(ng-repeat="VM in list.VMs.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by VM.id", xo-sref="VMs_view({id: VM.id})")
.grid.flat-object(
ng-repeat="VM in list.VMs.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by VM.id"
ng-if="shouldAppear(VM)"
xo-sref="VMs_view({id: VM.id})"
)
//- Icon.
.grid-cell.flat-cell.flat-cell-type
i.xo-icon-vm(class="xo-color-{{VM.power_state | lowercase}}")
//- Properties & tags.
.grid-cell
//- Properties.
.grid
.grid-cell
.grid
.grid-cell.flat-cell.flat-cell-name
| {{VM.name_label}}
.grid-cell.flat-cell.flat-cell-description
i {{VM.name_description}}
.grid-cell.flat-cell
| Address: {{VM.addresses["0/ip"]}}
.grid-cell.flat-cell
| {{VM.CPUs.number}} vCPUs
.grid-cell.flat-cell
| {{VM.memory.size | bytesToSize}} RAM
.grid-cell.flat-cell(ng-init="container = (VM.$container | resolve)")
div(ng-if="'pool' === container.type")
| Resident on:&nbsp;
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
div(ng-if="'host' === container.type", ng-init="pool = (container.$poolId | resolve)")
| Resident on:&nbsp;
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
small(ng-if="pool.name_label")
| (
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
| )
.grid-sm
.grid-cell.flat-cell.flat-cell-name
| {{VM.name_label}}
.grid-cell.flat-cell.flat-cell-description
i {{VM.name_description}}
.grid-cell.flat-cell
| Address: {{VM.addresses["0/ip"]}}
.grid-cell.flat-cell
| {{VM.CPUs.number}} vCPUs
.grid-cell.flat-cell
| {{VM.memory.size | bytesToSize}} RAM
.grid-cell.flat-cell(ng-init="container = (VM.$container | resolve)", ng-if="canView((VM.$container | resolve).id)")
div(ng-if="'pool' === container.type")
| Resident on:
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
div(ng-if="'host' === container.type", ng-init="pool = (container.$poolId | resolve)")
| Resident on:
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
small(ng-if="pool.name_label && canView(pool.id)")
| (
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
| )
//- /Properties.
//- Tags.
.grid
.grid-cell
.grid-cell.flat-cell-tag
i.fa.fa-tag
span(ng-repeat="tag in VM.tags")
span.label.label-primary {{tag}}
i.fa.fa-tag &nbsp;
xo-tag(object = 'VM')
//- /Tags.
//- /Properties & tags.
//- /VM.
//- If it's a SR.
.grid.flat-object(ng-repeat="SR in list.SRs.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by SR.id", xo-sref="SRs_view({id: SR.id})")
.grid.flat-object(
ng-repeat="SR in list.SRs.all | xoHideUnauthorized | filter:parsedListFilter | orderBy:natural('name_label') track by SR.id"
ng-if="shouldAppear(SR)"
xo-sref="SRs_view({id: SR.id})"
)
//- Icon.
.grid-cell.flat-cell.flat-cell-type
i.xo-icon-sr
//- Properties & tags.
.grid-cell
//- Properties.
.grid
.grid-cell
.grid
.grid-cell.flat-cell.flat-cell-name
| {{SR.name_label}}
.grid-cell.flat-cell.flat-cell-description
i {{SR.name_description}}
.grid-cell.flat-cell
| Usage: {{[SR.usage, SR.size] | percentage}} ({{SR.usage | bytesToSize}}/{{SR.size | bytesToSize}})
.grid-cell.flat-cell
| Type: {{SR.SR_type}}
.grid-cell.flat-cell(ng-init="container = (SR.$container | resolve)")
div(ng-if="'pool' === container.type")
strong
| Shared on
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
div(ng-if="'host' === container.type")
| Connected to&nbsp;
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
.grid-sm
.grid-cell.flat-cell.flat-cell-name
| {{SR.name_label}}
.grid-cell.flat-cell.flat-cell-description
i {{SR.name_description}}
.grid-cell.flat-cell
span(ng-if="SR.content_type !== 'disk'") Usage: {{[SR.physical_usage, SR.size] | percentage}} ({{SR.physical_usage | bytesToSize}}/{{SR.size | bytesToSize}})
.grid-cell.flat-cell
| Type: {{SR.SR_type}}
.grid-cell.flat-cell(ng-init="container = (SR.$container | resolve)")
div(ng-if="'pool' === container.type")
strong
| Shared on:
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
div(ng-if="'host' === container.type")
| Connected to:
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
//- /Properties.
//- Tags.
.grid
.grid-cell
.grid-cell.flat-cell-tag
i.fa.fa-tag
span(ng-repeat="tag in SR.tags")
span.label.label-primary {{tag}}
i.fa.fa-tag &nbsp;
xo-tag(object = 'SR')
//- /Tags.
//- /Properties & tags.
//- /SR.

View File

@@ -1,65 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.login', [
uiRouter
])
.config(function ($stateProvider) {
$stateProvider.state('login', {
url: '/login',
controller: 'LoginCtrl',
template: view
})
})
.controller('LoginCtrl', function ($scope, $state, $rootScope, xoApi, notify) {
const {toState, toStateParams} = (function (login) {
if (login) {
delete $rootScope._login
return {
toState: login.state.name,
toStateParams: login.stateParams
}
}
return { toState: 'index' }
})($rootScope._login)
$scope.$watch(() => xoApi.user, function (user) {
// When the user is logged in, go the wanted view, fallbacks on
// the index view if necessary.
if (user) {
$state.go(toState, toStateParams).catch(function () {
$state.go('index')
})
}
})
Object.defineProperties($scope, {
user: {
get () {
return xoApi.user
}
},
status: {
get () {
return xoApi.status
}
}
})
$scope.logIn = (...args) => {
xoApi.logIn(...args).catch(error => {
notify.warning({
title: 'Authentication failed',
message: error.message
})
})
}
})
// A module exports its name.
.name

View File

@@ -1,54 +0,0 @@
//- Hide the navbar for this view.
style.
.navbar {
display: none;
}
div.container
div.row-login
div.page-header
img(src = 'images/logo_small.png')
h2 Xen Orchestra
form.form-horizontal(
ng-submit = '$broadcast("fixAutofill"); logIn(email, password, true)'
)
fieldset
legend.login: h3 Sign in
div.form-group
div.col-sm-12
.input-group
span.input-group-addon: i.fa.fa-user.fa-fw
input.form-control.input-sm(
name = 'email'
type = 'text'
placeholder = 'Username'
ng-model = 'email'
required
fix-autofill
)
div.form-group
div.col-sm-12
.input-group
span.input-group-addon: i.fa.fa-key.fa-fw
input.form-control.input-sm(
name = 'password'
type = 'password'
placeholder = 'Password'
ng-model = 'password'
required
fix-autofill
)
div.form-group
div.col-sm-12
button.btn.btn-login.btn-block.btn-success(
id = 'login'
name = 'login'
)
i.fa.fa-sign-in
| Login
p.status(ng-if = '"disconnected" === status')
i.xo-icon-error.fa-2x(tooltip = 'You are not connected to XO-Server')
p.status(ng-if = '"connecting" === status')
i.fa.fa-refresh.fa-spin.fa-2x(tooltip = 'Connecting to XO-Server')
p.status(ng-if = '"connected" === status')
i.xo-icon-success.fa-2x(tooltip = 'You are connected to XO-Server')

View File

@@ -0,0 +1,75 @@
import angular from 'angular'
import forEach from 'lodash.foreach'
import uiBootstrap from 'angular-ui-bootstrap'
import xoServices from 'xo-services'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.migrateVm', [
uiBootstrap,
xoServices
])
.controller('MigrateVmCtrl', function (
$scope,
$modalInstance,
xoApi,
VDIs,
srsOnTargetPool,
srsOnTargetHost,
VIFs,
networks,
defaults,
intraPoolMigration
) {
$scope.VDIs = VDIs
$scope.SRs = srsOnTargetPool.concat(srsOnTargetHost)
$scope.VIFs = VIFs
$scope.networks = networks
$scope.intraPoolMigration = intraPoolMigration
$scope.selected = {}
$scope.selected.migrationNetwork = defaults.network
$scope.selected.vdi = {}
forEach($scope.VDIs, (vdi) => {
$scope.selected.vdi[vdi.id] = defaults.sr
})
if (!intraPoolMigration) {
$scope.selected.vif = {}
forEach($scope.VIFs, (vif) => {
$scope.selected.vif[vif.id] = defaults.network
})
}
$scope.migrate = function () {
$modalInstance.close($scope.selected)
}
})
.service('migrateVmModal', function ($modal, xo, xoApi) {
return function (state, id, hostId, VDIs, srsOnTargetPool, srsOnTargetHost, VIFs, networks, defaults, intraPoolMigration) {
return $modal.open({
controller: 'MigrateVmCtrl',
template: view,
resolve: {
VDIs: () => VDIs,
srsOnTargetPool: () => srsOnTargetPool,
srsOnTargetHost: () => srsOnTargetHost,
VIFs: () => VIFs,
networks: () => networks,
defaults: () => defaults,
intraPoolMigration: () => intraPoolMigration
}
}).result.then(function (selected) {
let isAdmin = xoApi.user && (xoApi.user.permission === 'admin')
state.go(isAdmin ? 'tree' : 'list')
return xo.vm.migrate(id, hostId, selected.vdi, intraPoolMigration ? undefined : selected.vif, selected.migrationNetwork)
})
}
})
// A module exports its name.
.name

View File

@@ -0,0 +1,50 @@
form(ng-submit="migrate()")
.modal-header
h3 VM migration
.modal-body
.form-inline
label Choose a network to migrate the VM:&nbsp
select.form-control(
ng-options="network.id as network.name_label for network in networks"
ng-model="selected.migrationNetwork"
)
p &nbsp
p
strong For each VDI, choose an SR:
table.table
tr
th.col-sm-5 Name
th.col-sm-7 SRs
tbody
tr(ng-repeat="vdi in VDIs")
td {{ vdi.name_label }}
td
table.table
tbody
tr
select.form-control(
ng-options="sr.id as sr.name_label for sr in SRs"
ng-model="selected.vdi[vdi.id]"
)
p(ng-if="!intraPoolMigration")
strong For each VIF, choose a network:
table.table(ng-if="!intraPoolMigration")
tr
th.col-sm-5 Device
th.col-sm-7 Networks
tbody
tr(ng-repeat="vif in VIFs")
td VIF \#{{ vif.device }} ({{vif.MAC}})
td
table.table
tbody
tr
select.form-control(
ng-options="network.id as network.name_label for network in networks"
ng-model="selected.vif[vif.id]"
)
.modal-footer
button.btn.btn-primary(type="submit")
| Migrate
button.btn.btn-warning(type="button", ng-click="$dismiss()")
| Cancel

View File

@@ -3,6 +3,7 @@ import uiRouter from 'angular-ui-router'
import updater from '../updater'
import xoServices from 'xo-services'
import includes from 'lodash.includes'
import view from './view'
@@ -14,7 +15,7 @@ export default angular.module('xoWebApp.navbar', [
updater,
xoServices
])
.controller('NavbarCtrl', function ($state, xoApi, xo, $scope, updater) {
.controller('NavbarCtrl', function ($state, xoApi, xo, $scope, updater, $rootScope) {
this.updater = updater
// TODO: It would make sense to inject xoApi in the scope.
Object.defineProperties(this, {
@@ -28,13 +29,85 @@ export default angular.module('xoWebApp.navbar', [
this.logIn = xoApi.logIn
this.logOut = function () {
xoApi.logOut()
$state.go('login')
}
// When a searched is entered, we must switch to the list view if
// necessary.
this.ensureListView = function () {
$state.go('list')
// necessary. When the text field is empty again, we must swith
// to tree view
let timeout
$scope.ensureListView = function (listFilter) {
clearTimeout(timeout)
timeout = window.setTimeout(function () {
$state.go('list').then(() =>
$rootScope.searchParse(),
$scope.updateOptions()
)
}, 400)
}
const _isOption = function (word, option) {
if (word === '*' + option || word === '!' + option || word === '!*' + option) {
return true
}
return false
}
const _removeOption = function (option) {
if (!$scope.$root.listFilter) {
return
}
const words = $scope.$root.listFilter.split(' ')
$scope.$root.listFilter = ''
for (const word of words) {
if (!_isOption(word, option) && word !== '') {
$scope.$root.listFilter += word + ' '
}
}
}
const _addOption = function (option) {
if (!$scope.$root.listFilter) {
$scope.$root.listFilter = '*' + option + ' '
return
}
const words = $scope.$root.listFilter.split(' ')
if (!includes(words, '*' + option) && !includes(words, '!' + option) && !includes(words, '!*' + option)) {
if ($scope.$root.listFilter.charAt($scope.$root.listFilter.length - 1) !== ' ') {
$scope.$root.listFilter += ' '
}
$scope.$root.listFilter += '*' + option + ' '
}
}
$rootScope.options = {
'vm': false,
'sr': false,
'host': false,
'pool': false,
'running': false,
'halted': false,
'disconnected': false
}
// Checkboxes --> Text
// Update text field after a checkbox has been clicked
$rootScope.updateListFilter = function (option) {
if ($rootScope.options[option]) {
_addOption(option)
} else {
_removeOption(option)
}
$scope.ensureListView($scope.$root.listFilter)
}
// Text --> Checkboxes
// Update checkboxes after the text field has been changed
$scope.updateOptions = function () {
const words = $scope.$root.listFilter ? $scope.$root.listFilter.split(' ') : ['']
for (const opt in $rootScope.options) {
$rootScope.options[opt] = false
for (let word of words) {
if (_isOption(word, opt)) {
$rootScope.options[opt] = true
}
}
}
}
this.tasks = xoApi.getView('runningTasks')

View File

@@ -21,12 +21,12 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
type = 'text'
placeholder = ''
ng-model = '$root.listFilter'
ng-change = 'navbar.ensureListView()'
ng-change = 'ensureListView($root.listFilter)'
)
span.input-group-btn
button.btn.btn-search(
type = 'button'
ng-click = 'navbar.ensureListView()'
ng-click = 'ensureListView($root.listFilter)'
)
i.fa.fa-search
//- /Search form.
@@ -85,15 +85,23 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
a(ui-sref="list")
i.fa.fa-align-justify
| Flat view
//- li.disabled(ui-sref-active="active")
//- a(ui-sref="graph")
//- i.fa.fa-sitemap
//- | Graphs view
li(
ui-sref-active="active"
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
)
a(ui-sref="dashboard.index")
i.fa.fa-dashboard
| Dashboard
li.divider
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
a(ui-sref = 'backup.index')
i.fa.fa-archive
| Backup
li
a(ui-sref = 'taskscheduler.index')
i.fa.fa-cogs
| Job Manager
li.divider
//- li.disabled
//- a
//- i.fa.fa-clock-o
//- | Scheduler
li(
ui-sref-active = 'active'
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
@@ -118,7 +126,7 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
i.fa.fa-exclamation-triangle.text-danger(ng-if = 'navbar.updater.state == "error"', tooltip = 'Can\'t fetch update information')
li
a(ui-sref="settings.users", tooltip="{{navbar.user.email}}")
a(ng-if = '!navbar.user.provider', ui-sref="{{navbar.user.provider ? 'settings.users' : 'settings.user'}}", tooltip="{{navbar.user.email}}")
i.fa.fa-user
span.hidden-sm {{navbar.user.email}}
li

View File

@@ -19,16 +19,13 @@ export default angular.module('xoWebApp.newSr', [
})
})
.controller('NewSrCtrl', function ($scope, $state, $stateParams, xo, xoApi, notify, modal, bytesToSizeFilter) {
this.reset = function (data = {}) {
this.data = {}
delete this.lockCreation
this.lock = !(
(data.srType === 'Local') &&
(data.srPath && data.srPath.path)
)
}
this.resetLists = function () {
@@ -48,7 +45,6 @@ export default angular.module('xoWebApp.newSr', [
* Loads NFS paths and iScsi iqn`s
*/
this.populateSettings = function (type, server, auth, user, password) {
this.reset()
this.loading = true
@@ -82,7 +78,6 @@ export default angular.module('xoWebApp.newSr', [
xoApi.call('sr.probeIscsiIqns', params)
.then(response => {
if (response.length > 0) {
this.data.iqns = response
} else {
@@ -91,7 +86,6 @@ export default angular.module('xoWebApp.newSr', [
message: 'No IQNs found'
})
}
})
.catch(error => notify.warning({
title: 'iSCSI Detection',
@@ -101,7 +95,6 @@ export default angular.module('xoWebApp.newSr', [
} else {
this.loading = false
}
}
/*
@@ -124,7 +117,6 @@ export default angular.module('xoWebApp.newSr', [
xoApi.call('sr.probeIscsiLuns', params)
.then(response => {
forEach(response, item => {
item.display = 'LUN ' + item.id + ': ' +
item.serial + ' ' + bytesToSizeFilter(item.size) +
@@ -225,9 +217,9 @@ export default angular.module('xoWebApp.newSr', [
let server = this._parseAddress(data.srServer || '')
let path = (
data.srType === 'NFS_ISO' ?
server.host + ':' :
''
data.srType === 'NFS_ISO'
? server.host + ':'
: ''
) + data.srPath.path
operationToPromise = xoApi.call('sr.createIso', {
@@ -334,13 +326,11 @@ export default angular.module('xoWebApp.newSr', [
xoApi.call('sr.probeIscsiExists', params)
.then(response => {
if (response.length > 0) {
this.data.scsiList = this._processSRList(response)
}
this.lock = !Boolean(data.srIScsiId)
})
.catch(error => {
notify.error({
@@ -362,13 +352,11 @@ export default angular.module('xoWebApp.newSr', [
serverPath: data.srPath.path
})
.then(response => {
if (response.length > 0) {
this.data.scsiList = this._processSRList(response)
}
this.lock = !Boolean(data.srPath.path)
})
.catch(error => {
notify.error({
@@ -413,10 +401,9 @@ export default angular.module('xoWebApp.newSr', [
$state.go('SRs_view', {id})
})
.catch(error => notify.error({
title: 'reattach',
message: error.message
})
)
title: 'reattach',
message: error.message
}))
.finally(() => {
this.lock = false
this.attaching = false

View File

@@ -12,7 +12,7 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
//- Choose SR type panel
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-info-circle(style="color: #e25440;")
i.fa.fa-info-circle
| General
.panel-body
.form-group
@@ -38,7 +38,7 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
//- Choose SR details
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs(style="color: #e25440;")
i.fa.fa-cogs
| Settings
.panel-body
.form-group(ng-if = 'formData.srType === "NFS" || formData.srType === "iSCSI" || formData.srType === "NFS_ISO"')
@@ -109,12 +109,12 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
select.form-control(name = 'srIScsiId', ng-change = 'newSr.loadScsiList(formData)', ng-model = 'formData.srIScsiId', ng-options = 'item.display for item in newSr.data.iScsiIds', required)
option(value = '', disabled) -- Choose LUN --
.form-group.text-center(ng-if = 'newSr.loading')
i.fa.fa-circle-o-notch.fa-spin.fa-2x
i.xo-icon-loading
.grid(ng-if = 'newSr.data.nfsList && newSr.data.nfsList.length > 0')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-eye(style="color: #e25440;")
i.fa.fa-eye
| NFS storage use
.panel-body
table.table.table-condensed
@@ -130,12 +130,12 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
i.fa.fa-eye
| In use
p.text-center(ng-if = 'newSr.attaching')
i.fa.fa-circle-o-notch.fa-spin.fa-2x
i.xo-icon-loading
.grid(ng-if = 'newSr.data.scsiList && newSr.data.scsiList.length > 0')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-eye(style="color: #e25440;")
i.fa.fa-eye
| iSCSI storage use
.panel-body
table.table.table-condensed
@@ -151,13 +151,13 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
i.fa.fa-eye
| In use
p.text-center(ng-if = 'newSr.attaching')
i.fa.fa-circle-o-notch.fa-spin.fa-2x
i.xo-icon-loading
//- Summary
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flag-checkered(style="color: #e25440;")
i.fa.fa-flag-checkered
| Summary
.panel-body
.grid
@@ -180,4 +180,4 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
button.btn.btn-lg.btn-primary(type="submit", ng-disabled = 'newSr.lock || newSr.lockCreation')
i.fa.fa-play
| &nbsp;Create SR&nbsp;
i.fa.fa-circle-o-notch.fa-spin(ng-if = 'newSr.creating')
i.xo-icon-loading-sm(ng-if = 'newSr.creating')

View File

@@ -2,6 +2,7 @@ angular = require 'angular'
cloneDeep = require 'lodash.clonedeep'
filter = require 'lodash.filter'
forEach = require 'lodash.foreach'
trim = require 'lodash.trim'
#=====================================================================
@@ -11,7 +12,7 @@ module.exports = angular.module 'xoWebApp.newVm', [
.config ($stateProvider) ->
$stateProvider.state 'VMs_new',
url: '/vms/new/:container'
controller: 'NewVmsCtrl'
controller: 'NewVmsCtrl as ctrl'
template: require './view'
.controller 'NewVmsCtrl', (
$scope, $stateParams, $state
@@ -19,8 +20,53 @@ module.exports = angular.module 'xoWebApp.newVm', [
bytesToSizeFilter, sizeToBytesFilter
notify
) ->
{get} = xoApi
$scope.multipleVmsActive = false
$scope.vmsNames = ['VM1', 'VM2']
$scope.numberOfVms = 2
$scope.newNumberOfVms = 2
$scope.checkNumberOfVms = ->
if $scope.newNumberOfVms && Number.isInteger($scope.newNumberOfVms)
$scope.newNumberOfVms = $scope.numberOfVms = Math.min(100,Math.max(2,$scope.newNumberOfVms))
else
$scope.newNumberOfVms = $scope.numberOfVms = 2
$scope.refreshNames = ->
$scope.defaultName = 'VM'
$scope.defaultName = $scope.name_label if $scope.name_label
forEach($scope.vmsNames, (name, index) ->
$scope.vmsNames[index] = $scope.defaultName + (index+1)
)
$scope.toggleBootAfterCreate = ->
$scope.bootAfterCreate = false if $scope.multipleVmsActive
$scope.configDriveActive = false
existingDisks = {}
$scope.saveChange = (position, propertyName, value) ->
if not existingDisks[position]?
existingDisks[position] = {}
existingDisks[position][propertyName] = value
$scope.updateVdiSize = (position) ->
$scope.saveChange(position, 'size', bytesToSizeFilter(sizeToBytesFilter($scope.existingDiskSizeValues[position] + ' ' + $scope.existingDiskSizeUnits[position])))
$scope.initExistingValues = (template) ->
$scope.name_label = template.name_label
sizes = {}
$scope.existingDiskSizeValues = {}
$scope.existingDiskSizeUnits = {}
forEach xoApi.get(template.$VBDs), (VBD) ->
sizes[VBD.position] = bytesToSizeFilter xoApi.get(VBD.VDI).size
$scope.existingDiskSizeValues[VBD.position] = parseInt(sizes[VBD.position].split(' ')[0], 10)
$scope.existingDiskSizeUnits[VBD.position] = sizes[VBD.position].split(' ')[1]
$scope.VIFs.length = 0
if template.VIFs.length
forEach xoApi.get(template.VIFs), (VIF) ->
network = xoApi.get(VIF.$network)
$scope.addVIF(network)
return
else $scope.addVIF()
{get} = xoApi
removeItems = do ->
splice = Array::splice.call.bind Array::splice
(array, index, n) -> splice array, index, n ? 1
@@ -103,28 +149,35 @@ module.exports = angular.module 'xoWebApp.newVm', [
default_SR = get pool.default_SR
default_SR = if default_SR then default_SR.id else ''
)
$scope.availableMethods = {}
$scope.CPUs = ''
$scope.pv_args = ''
$scope.installation_cdrom = ''
$scope.installation_method = ''
$scope.installation_network = ''
$scope.memory = ''
$scope.name_description = ''
$scope.memoryValue = null
$scope.units = ['MiB', 'GiB', 'TiB']
$scope.memoryUnit = $scope.units[0]
$scope.name_description = 'Created by XO'
$scope.name_label = ''
$scope.template = ''
$scope.firstSR = ''
$scope.VDIs = []
$scope.VIFs = []
$scope.isDiskTemplate = false
$scope.cloudConfigSshKey = ''
$scope.bootAfterCreate = true
$scope.updateMemoryUnit = (memoryUnit) ->
$scope.memoryUnit = memoryUnit
$scope.addVIF = do ->
id = 0
->
(network = '') ->
$scope.VIFs.push {
id: id++
network: ''
network
}
$scope.addVIF()
$scope.removeVIF = (index) -> removeItems $scope.VIFs, index
@@ -142,6 +195,8 @@ module.exports = angular.module 'xoWebApp.newVm', [
id: VDI_id++
bootable: false
size: ''
sizeValue: null
sizeUnit: $scope.units[0]
SR: default_SR
type: 'system'
}
@@ -149,7 +204,11 @@ module.exports = angular.module 'xoWebApp.newVm', [
# When the selected template changes, updates other variables.
$scope.$watch 'template', (template) ->
return unless template
# After each template change, initialize coreOsCloudConfig to empty
$scope.coreOsCloudConfig = ''
# Fetch the PV args
$scope.pv_args = template.PV_args
{install_methods} = template.template_info
availableMethods = $scope.availableMethods = Object.create null
for method in install_methods
@@ -159,44 +218,84 @@ module.exports = angular.module 'xoWebApp.newVm', [
else
delete $scope.installation_method
delete $scope.installation_method
delete $scope.installation_network
# if the template already have a configured install repository
installRepository = template.template_info.install_repository
if installRepository
if installRepository is 'cdrom'
$scope.installation_method = 'cdrom'
else
$scope.installation_network = template.template_info.install_repository
$scope.installation_method = 'network'
VDIs = $scope.VDIs = cloneDeep template.template_info.disks
# if the template has no config disk
# nor it's Other install media (specific case)
# then do NOT display disk and network panel
if VDIs.length is 0 and template.name_label isnt 'Other install media'
$scope.isDiskTemplate = true
$scope.VIFs.length = 0
else $scope.isDiskTemplate = false
for VDI in VDIs
VDI.id = VDI_id++
VDI.size = bytesToSizeFilter VDI.size
VDI.SR or= default_SR
VDI.size = bytesToSizeFilter VDI.size
VDI.sizeValue = if VDI.size then parseInt(VDI.size.split(' ')[0], 10) else null
VDI.sizeUnit = VDI.size.split(' ')[1]
# if the template is labeled CoreOS
# we'll use config drive setup
if template.name_label == 'CoreOS'
return xo.vm.getCloudInitConfig template.id
.then (result) ->
$scope.coreOsCloudConfig = result
$scope.createVM = ->
$scope.createVMs = ->
if !$scope.multipleVmsActive
$scope.createVM($scope.name_label)
return
forEach($scope.vmsNames, (name) ->
$scope.createVM(name)
)
# Send the client on the tree view
$state.go 'tree'
xenDefaultWeight = 256
$scope.weightMap = {
'Quarter (1/4)': xenDefaultWeight / 4,
'Half (1/2)': xenDefaultWeight / 2,
'Normal': xenDefaultWeight,
'Double (x2)': xenDefaultWeight * 2
}
$scope.createVM = (name_label) ->
{
CPUs
cpuWeight
pv_args
installation_cdrom
installation_method
installation_network
memory
memoryValue
memoryUnit
name_description
name_label
template
VDIs
VIFs
} = $scope
forEach VDIs, (vdi) ->
vdi.size = bytesToSizeFilter(sizeToBytesFilter(vdi.sizeValue + ' ' + vdi.sizeUnit))
# Does not edit the displayed data directly.
VDIs = cloneDeep VDIs
for VDI, index in VDIs
# store the first VDI's SR for later use (e.g: coreOsCloudConfig)
if VDI.id == 0
$scope.firstSR = VDI.SR or default_SR
# Removes the dummy identifier used for AngularJS.
delete VDI.id
# Adds the device number based on the index.
VDI.device = "#{index}"
# Transforms the size from human readable format to bytes.
VDI.size = sizeToBytesFilter VDI.size
# TODO: handles invalid values.
# Does not edit the displayed data directly.
@@ -205,10 +304,13 @@ module.exports = angular.module 'xoWebApp.newVm', [
# Removes the dummy identifier used for AngularJS.
delete VIF.id
# Removes the MAC address if empty.
if 'MAC' of VIF
VIF.MAC = VIF.MAC.trim()
delete VIF.MAC unless VIF.MAC
# xo-server expects a network id, not the whole object
VIF.network = VIF.network.id
# Removes the mac address if empty.
if 'mac' of VIF
VIF.mac = trim(VIF.mac)
delete VIF.mac unless VIF.mac
if installation_method is 'cdrom'
@@ -223,17 +325,22 @@ module.exports = angular.module 'xoWebApp.newVm', [
method: matches[1].toLowerCase()
repository: installation_network
}
else if installation_method is 'pxe'
installation = {
method: 'network'
repository: 'pxe'
}
else
installation = undefined
data = {
installation
pv_args
name_label
template: template.id
VDIs
VIFs
existingDisks
}
# TODO:
# - disable the form during creation
# - indicate the progress of the operation
@@ -241,32 +348,67 @@ module.exports = angular.module 'xoWebApp.newVm', [
title: 'VM creation'
message: 'VM creation started'
}
$scope.creatingVM = true
id = null
xoApi.call('vm.create', data)
.then (id_) ->
id = id_
xoApi.call('vm.create', data).then (id) ->
# If nothing to sets, just stops.
return id unless CPUs or name_description or memory
return unless CPUs or name_description or memoryValue
data = {
id
}
data.CPUs = +CPUs if CPUs
if cpuWeight
data.cpuWeight = cpuWeight
if name_description
data.name_description = name_description
if memory
memory = sizeToBytesFilter memory
# FIXME: handles invalid entries.
data.memory = memory
if pv_args
data.pv_args = pv_args
xoApi.call('vm.set', data).then -> id
.then (id) ->
$state.go 'VMs_view', { id }
if memoryValue
# FIXME: handles invalid entries.
data.memoryValue = memoryValue + ' ' + memoryUnit
return xoApi.call('vm.set', data)
.then () ->
# If a CloudConfig drive needs to be created
if $scope.coreOsCloudConfig
# Use the CoreOS specific Cloud Config creation
return xo.vm.createCloudInitConfigDrive(id, $scope.firstSR, $scope.coreOsCloudConfig, true).then ->
return xo.docker.register(id)
if $scope.configDriveActive
# User creation is less universal...
# $scope.cloudContent = '#cloud-config\nhostname: ' + name_label + '\nusers:\n - name: olivier\n sudo: ALL=(ALL) NOPASSWD:ALL\n groups: sudo\n shell: /bin/bash\n ssh_authorized_keys:\n - ' + $scope.cloudConfigSshKey + '\n'
# So keep it basic for now: hostname and ssh key
hostname = name_label
# Remove leading and trailing spaces.
.replace(/^\s+|\s+$/g, '')
# Replace spaces with '-'.
.replace(/\s+/g, '-')
$scope.cloudContent = '#cloud-config\nhostname: ' + hostname + '\nssh_authorized_keys:\n - ' + $scope.cloudConfigSshKey + '\n'
# The first SR for a template with an existing disk
$scope.firstSR = (get (get template.$VBDs[0]).VDI).$SR
# Use the generic CloudConfig creation
return xo.vm.createCloudInitConfigDrive(id, $scope.firstSR, $scope.cloudContent).then ->
# Boot directly on disk
return xo.vm.setBootOrder({vm: id, order: 'c'})
.then () ->
if $scope.bootAfterCreate
xo.vm.start id
if !$scope.multipleVmsActive
# Send the client on the VM view
$state.go 'VMs_view', { id }
.catch (error) ->
notify.error {
title: 'VM creation'
message: 'The creation failed'
}
$scope.creatingVM = false
console.log error

View File

@@ -2,62 +2,78 @@
.panel.panel-default
p.page-title
i.xo-icon-vm
| Create VM on&nbsp;
| Create VM on
a(ng-if="'pool' === container.type", ui-sref="pools_view({id: container.id})")
| {{container.name_label}}
| {{container.name_label}}
a(ng-if="'host' === container.type", ui-sref="hosts_view({id: container.id})")
| {{container.name_label}}
| {{container.name_label}}
//- Add server panel
form.form-horizontal(ng-submit="createVM()")
form.form-horizontal(ng-submit="createVMs()")
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-info-circle(style="color: #e25440;")
i.fa.fa-info-circle
| VM info
.panel-body
.form-group
label.col-sm-3.control-label Template
.col-sm-9
select.form-control(ng-model="template", ng-options="template.name_label for template in templates | orderBy:natural('name_label') track by template.id", required="")
select.form-control(ng-model="template", ng-options="template.name_label for template in templates | orderBy:natural('name_label') track by template.id", required="", ng-change = 'initExistingValues(template)')
.form-group
label.col-sm-3.control-label Name
.col-sm-9
input.form-control(type="text", placeholder="Name of your new VM", required="", ng-model="name_label")
input.form-control(type="text", placeholder="Name of your new VM", ng-required="!multipleVmsActive", ng-model="name_label")
.form-group
label.col-sm-3.control-label Description
.col-sm-9
input.form-control(type="text", placeholder="Optional description of you new VM", ng-model="name_description")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-dashboard(style="color: #e25440;")
i.fa.fa-dashboard
| Performances
.panel-body
.form-group
label.col-sm-3.control-label vCPUs
.col-sm-9
input.form-control(type="text", placeholder="{{template.CPUs.number}}", ng-model="CPUs")
.form-group
label.col-sm-3.control-label CPU Weight
.col-sm-9
select.form-control(ng-model = "cpuWeight", ng-options='value as key for (key, value) in weightMap track by value')
option(value = '') default
.form-group
label.col-sm-3.control-label RAM
.col-sm-9
input.form-control(type="text", placeholder="{{template.memory.size | bytesToSize}}", ng-model="memory")
.grid(ng-if="isDiskTemplate")
.panel.panel-default
.input-group
input.form-control(type='number' min="0" placeholder="{{ template.memory.size | bytesConvert:memoryUnit:'iB' }}" ng-model="memoryValue")
.input-group-btn
span.pull-right.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(type='button' dropdown-toggle)
| {{ memoryUnit }}
span.caret
ul.dropdown-menu(role="menu")
li(ng-repeat="memoryUnit in units")
a(ng-click="updateMemoryUnit(memoryUnit)") {{ memoryUnit }}
.grid
//- Cloud Config Panel, only for templates with existing disks
.panel.panel-default(ng-if="isDiskTemplate")
.panel-heading.panel-title
i.fa.fa-info-circle(style="color: #e25440;")
| Template info
i.fa.fa-cloud
| Config Drive
span.pull-right
label(style = 'cursor: pointer;')
input.hidden(type = 'checkbox', ng-model = '$parent.configDriveActive')
i.fa(ng-class = '{"fa-toggle-on": $parent.configDriveActive, "text-success": $parent.configDriveActive, "fa-toggle-off": !$parent.configDriveActive}', style = 'font-size: 1.5em;')
.panel-body
p.center This template will create automatically a VM with:
.col-md-6
ul(ng-repeat="VIF in template.VIFs | resolve | orderBy:natural('device') track by VIF.id")
li Interface \#{{VIF.device}} (MTU {{VIF.MTU}}) on {{(VIF.$network | resolve).name_label}}
.col-md-6
ul(ng-repeat = 'VBD in (template.$VBDs | resolve) track by VBD.id')
li Disk {{(VBD.VDI | resolve).name_label}} ({{(VBD.VDI | resolve).size | bytesToSize}}) on {{((VBD.VDI | resolve).$SR | resolve).name_label}}
.grid(ng-if="!isDiskTemplate")
//- Install panel
.panel.panel-default
fieldset(ng-disabled = '!$parent.configDriveActive')
.form-group
label.col-sm-3.control-label SSH Key
.col-sm-9
input.form-control(type="text", placeholder="ssh-rsa AAAA.... you@machine", ng-model="$parent.cloudConfigSshKey", name="cloudConfigSshKey", required)
//- Install panel, only if an installation method is needed
.panel.panel-default(ng-if="!isDiskTemplate")
.panel-heading.panel-title
i.fa.fa-download(style="color: #e25440;")
i.fa.fa-download
| Install settings
.panel-body
.form-group(ng-show="availableMethods.cdrom")
@@ -70,18 +86,18 @@ form.form-horizontal(ng-submit="createVM()")
name = 'installation_method'
ng-model = '$parent.installation_method'
value = 'cdrom'
required
)
select.form-control.disabled(
ng-disabled="'cdrom' !== installation_method"
ng-model="$parent.installation_cdrom"
required
)
option(value = '') Please select
optgroup(ng-repeat="SR in ISO_SRs | orderBy:natural('name_label') track by SR.id", ng-if="SR.VDIs.length", label="{{SR.name_label}}")
option(ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.id", ng-value="VDI.id")
| {{VDI.name_label}}
.form-group(ng-show="availableMethods.http || availableMethods.ftp || availableMethods.nfs")
.form-group(
ng-show = '(availableMethods.http || availableMethods.ftp || availableMethods.nfs)'
)
label.col-sm-3.control-label Network
.col-sm-9
.input-group
@@ -91,23 +107,26 @@ form.form-horizontal(ng-submit="createVM()")
name = 'installation_method'
ng-model = '$parent.installation_method'
value = 'network'
required
)
input.form-control(type="text", ng-disabled="'network' !== installation_method", placeholder="e.g: http://ftp.debian.org/debian", ng-model="$parent.installation_network")
//- <div class="form-group"> FIXME
//- <label class="col-sm-3 control-label">Home server</label>
//- <div class="col-sm-9">
//- <select class="form-control">
//- <option>Default (auto)</option>
//- </select>
//- </div>
//- </div>
.form-group(ng-show = 'template.virtualizationMode === "hvm"')
label.col-sm-3.control-label PXE
.col-sm-9
input(
type = 'radio'
name = 'installation_method'
ng-model = '$parent.installation_method'
value = 'pxe'
)
.form-group(ng-show='template.virtualizationMode === "pv"')
label.col-sm-3.control-label PV Args
.col-sm-9
input.form-control(type="text", placeholder="{{template.PV_args}}", ng-model="$parent.pv_args")
//- Interface panel
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-network(style="color: #e25440;")
i.xo-icon-network
| Interfaces
.panel-body
table.table.table-hover
@@ -118,10 +137,10 @@ form.form-horizontal(ng-submit="createVM()")
//- Buttons
tr(ng-repeat="VIF in VIFs track by VIF.id")
td
input.form-control(type="text", ng-model="VIF.MAC", ng-pattern="/^\s*[0-9a-f]{2}(:[0-9a-f]{2}){5}\s*$/i", placeholder="00:00:00:00:00")
input.form-control(type="text", ng-model="VIF.mac", ng-pattern="/^\s*[0-9a-f]{2}(:[0-9a-f]{2}){5}\s*$/i", placeholder="Auto-generated if empty")
td
select.form-control(
ng-options = 'network.id as network.name_label for network in networks | orderBy:natural("name_label") track by network.id'
ng-options = 'network as network.name_label for network in networks | orderBy:natural("name_label") track by network.id'
ng-model = 'VIF.network'
required
)
@@ -138,11 +157,59 @@ form.form-horizontal(ng-submit="createVM()")
i.fa.fa-plus
| Add interface
//- end of misc and interface panel
//- Disk panel
.grid(ng-if="!isDiskTemplate")
//- Cloud config panel
.grid(ng-if = 'coreOsCloudConfig')
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr(style="color: #e25440;")
i.fa.fa-cloud
| Cloud config
.pull-right.small
button.btn.btn-default(type = 'button', ng-click = 'isExpanded = !isExpanded'): i.fa(ng-class = '{"fa-plus": !isExpanded, "fa-minus": isExpanded}')
.panel-body
textarea.form-control(rows="20", collapse= '!isExpanded', ng-model='$parent.coreOsCloudConfig', name='coreOsCloudConfig')
| {{coreOsCloudConfig}}
//- Multiple VMs panel
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-clone
| Multiple VMs
span.pull-right
label(style = 'cursor: pointer;')
input.hidden(type = 'checkbox', ng-model = 'multipleVmsActive', ng-click='refreshNames()', ng-change="toggleBootAfterCreate()")
i.fa(ng-class = '{"fa-toggle-on": multipleVmsActive, "text-success": multipleVmsActive, "fa-toggle-off": !multipleVmsActive}', style = 'font-size: 1.5em;')
.panel-body(ng-if="multipleVmsActive")
.form-group
label.col-md-offset-4.col-sm-2.control-label
i.fa.fa-refresh(ng-click = "$parent.refreshNames()" tooltip="Set VMs to default names")
| Number of VMs
.col-sm-2
.input-group(style="width:10em")
input.form-control(type="number" ng-model="$parent.newNumberOfVms")
span.input-group-btn
button.btn.btn-default(type="button" ng-click="checkNumberOfVms()")
i.fa.fa-arrow-right
.col-sm-6(ng-repeat="offset in [0, 1]")
.form-group(
ng-repeat = "n in [].constructor($parent.numberOfVms).slice(0, ($parent.numberOfVms+1)/2) track by $index"
ng-if = "2*$index+offset < $parent.numberOfVms"
)
label.col-sm-2.control-label VM \#{{ 2*$index+1+offset }}
.col-sm-10
input.form-control(
type = "text"
required
placeholder = "Name of new VM \#{{ (2*$index+1+offset) }}"
ng-model = "$parent.vmsNames[2*$index+offset]"
ng-init = "$parent.vmsNames[2*$index+offset] = $parent.defaultName + (2*$index+1+offset)"
)
//- Disk panel
.grid
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-disk
| Disks
.panel-body
table.table.table-hover
@@ -154,13 +221,50 @@ form.form-horizontal(ng-submit="createVM()")
th.col-md-4 Description
th.col-md-1 &#160;
//- Buttons
tr(ng-repeat="VBD in (template.$VBDs | resolve) track by VBD.id", ng-if="isDiskTemplate")
td
select.form-control(ng-model="(VBD.VDI | resolve).$SR", ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.physical_usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))", ng-change = 'saveChange(VBD.position, "$SR", (VBD.VDI | resolve).$SR)')
td.text-center
i.fa.fa-check(ng-if = 'VBD.bootable')
td
.input-group
input.form-control(
type='number'
min="0"
placeholder="Size of this virtual disk"
ng-model="existingDiskSizeValues[VBD.position]"
ng-readonly='!configDriveActive'
ng-change = 'updateVdiSize(VBD.position)'
)
.input-group-btn
span.pull-right.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(type='button' dropdown-toggle ng-disabled='!configDriveActive')
| {{ existingDiskSizeUnits[VBD.position] }}
span.caret
ul.dropdown-menu(role="menu")
li(ng-repeat="unit in $parent.units")
a(ng-click="existingDiskSizeUnits[VBD.position] = unit; updateVdiSize(VBD.position)") {{ unit }}
td
input.form-control(type="text", placeholder="Name of this virtual disk", ng-model="(VBD.VDI | resolve).name_label", ng-change = 'saveChange(VBD.position, "name_label", (VBD.VDI | resolve).name_label)')
td
input.form-control(type="text", placeholder="Description of this virtual disk", ng-model="(VBD.VDI | resolve).name_description", ng-change = 'saveChange(VBD.position, "name_description", (VBD.VDI | resolve).name_description)')
td
tr(ng-repeat="VDI in VDIs track by VDI.id")
td
select.form-control(ng-model="VDI.SR", ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))")
select.form-control(ng-model="VDI.SR", ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.physical_usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))")
td.text-center
input(type="checkbox", ng-model="VDI.bootable")
td
input.form-control(type="text", ng-model="VDI.size", required="")
.input-group
input.form-control(type='number' min="0" placeholder="Size of this virtual disk" ng-model="VDI.sizeValue")
.input-group-btn
span.pull-right.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(type='button' dropdown-toggle)
| {{ VDI.sizeUnit }}
span.caret
ul.dropdown-menu(role="menu")
li(ng-repeat="unit in units")
a(ng-click="VDI.sizeUnit = unit") {{ unit }}
td
input.form-control(type="text", placeholder="Name of this virtual disk", ng-model="VDI.name_label")
td
@@ -185,12 +289,14 @@ form.form-horizontal(ng-submit="createVM()")
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flag-checkered(style="color: #e25440;")
i.fa.fa-flag-checkered
| Summary
.panel-body
.grid
.grid-cell
p.center.big {{name_label}}
p.center.big
span(ng-if="!multipleVmsActive") {{name_label}}
span(ng-if="multipleVmsActive") {{numberOfVms}} new VMs
| &nbsp;
span.small(ng-if="template.name_label") ({{template.name_label}})
.grid
@@ -202,19 +308,31 @@ form.form-horizontal(ng-submit="createVM()")
.grid-cell
//- p.stat-name RAM
p.center.big(tooltip="RAM")
| {{(memory) || (template.memory.size | bytesToSize)}}&nbsp;
span(ng-if="memory") {{memory}} {{memoryUnit}}
span(ng-if="!memory") {{template.memory.size | bytesToSize}}
| &nbsp;
i.xo-icon-memory
.grid-cell
//- p.stat-name Disks
p.center.big(tooltip="Disks")
| {{(VDIs.length) || (template.$VBDs.length) || 0}}x&nbsp;
i.fa.fa-hdd-o
i.xo-icon-disk
.grid-cell
//- p.stat-name Interfaces
p.center.big(tooltip="Network interfaces")
| {{(VIFs.length) || (template.VIFs.length) || 0}}x&nbsp;
i.xo-icon-network
p.center(ng-if="isDiskTemplate")
| Cloud configuration is&nbsp;
strong.text-success(ng-if = 'configDriveActive') enabled.
strong.text-danger(ng-if = '!configDriveActive') disabled.
p.center
button.btn.btn-lg.btn-primary(type="submit")
i.fa.fa-play
label
input(type='checkbox', ng-model = 'bootAfterCreate')
span(ng-if='!multipleVmsActive') Boot VM after creation
span(ng-if='multipleVmsActive') Boot {{numberOfVms}} VMs after creation
p.center
button.btn.btn-lg.btn-primary(type="submit", ng-disabled = 'creatingVM')
i.fa.fa-play(ng-if = '!creatingVM')
i.fa.fa-circle-o-notch.fa-spin(ng-if = 'creatingVM')
| Create VM

View File

@@ -2,12 +2,15 @@ import angular from 'angular'
import forEach from 'lodash.foreach'
import uiRouter from 'angular-ui-router'
import xoTag from 'tag'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.pool', [
uiRouter
uiRouter,
xoTag
])
.config(function ($stateProvider) {
$stateProvider.state('pools_view', {
@@ -72,6 +75,16 @@ export default angular.module('xoWebApp.pool', [
})
}
$scope.setDefaultSr = function (id) {
let {pool} = $scope
return modal.confirm({
title: 'Set default SR',
message: 'Are you sure you want to set this SR as default?'
}).then(function () {
return xo.pool.setDefaultSr(pool.id, id)
})
}
$scope.deleteLog = function (id) {
console.log('Remove log', id)
return xo.log.delete(id)
@@ -100,7 +113,6 @@ export default angular.module('xoWebApp.pool', [
// notify.info
// title: 'Upload patch'
// message: 'Success'
})
// A module exports its name.

View File

@@ -1,13 +1,13 @@
//- TODO: lots of stuff.
.grid
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-pool
| {{pool.name_label}}
.grid
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs(style="color: #e25440;")
i.fa.fa-cogs
| General
span.quick-edit(tooltip="Edit General settings", ng-click="poolSettings.$show()")
i.fa.fa-edit.fa-fw
@@ -28,8 +28,7 @@
| {{master.name_label}}
dt Tags
dd
span(ng-repeat="tag in pool.tags")
span.label.label-primary {{tag}}
xo-tag(ng-if = 'pool', object = 'pool')
dt(ng-if="pool.default_SR") Default SR
dd(ng-if="pool.default_SR", ng-init="default_SR = (pool.default_SR | resolve)")
a(ui-sref="SRs_view({id: default_SR.id})") {{default_SR.name_label}}
@@ -49,46 +48,46 @@
| Save
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-stats(style="color: #e25440;")
i.xo-icon-stats
| Stats
.row
.col-sm-6.col-lg-4
.col-xs-6
p.stat-name Hosts:
p.center.big-stat {{hosts | count}}
.col-sm-6.col-lg-4
.col-xs-6
p.stat-name Running:
p.center.big-stat {{runningHosts | count}}
//- Action panel
.grid
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flash(style="color: #e25440;")
i.fa.fa-flash
| Actions
.panel-body
.grid-cell.text-center
.grid
.grid-cell.btn-group
button.btn(tooltip="Add SR", type="button", style="width: 90%", disabled)
button.btn(tooltip="Add SR", tooltip-placement="top", type="button", style="width: 90%", disabled)
i.xo-icon-sr.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Add VM", type="button", style="width: 90%", xo-sref="VMs_new({container: pool.id})")
button.btn(tooltip="Add VM", tooltip-placement="top", type="button", style="width: 90%", xo-sref="VMs_new({container: pool.id})")
i.xo-icon-vm.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Patch the pool", type="button", style="width: 90%", ng-file-select = "patchPool($files, pool.id)")
button.btn(tooltip="Patch the pool", tooltip-placement="top", type="button", style="width: 90%", ng-file-select = "patchPool($files, pool.id)")
i.fa.fa-file-code-o.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Add Host", type="button", style="width: 90%")
button.btn(tooltip="Add Host", tooltip-placement="top", type="button", style="width: 90%")
i.xo-icon-host.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Disconnect", type="button", style="width: 90%; margin-bottom: 0.5em")
button.btn(tooltip="Disconnect", tooltip-placement="top", type="button", style="width: 90%; margin-bottom: 0.5em")
i.fa.fa-unlink.fa-2x.fa-fw
//- Hosts panel
.grid
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-host(style="color: #e25440;")
i.xo-icon-host
| Hosts
.panel-body
table.table.table-hover.table-condensed
@@ -100,13 +99,20 @@
td.oneliner {{host.name_description}}
td
.progress-condensed
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{host.memory.usage}}", aria-valuemax="{{host.memory.size}}", style="width: {{[host.memory.usage, host.memory.size] | percentage}}")
.progress-bar(
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{host.memory.usage}}",
aria-valuemax="{{host.memory.size}}",
style="width: {{[host.memory.usage, host.memory.size] | percentage}}",
tooltip="RAM: {{host.memory.usage | bytesToSize}}/{{host.memory.size | bytesToSize}} ({{[host.memory.usage, host.memory.size] | percentage}})"
)
//- Shared SR panel
.grid
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr(style="color: #e25440;")
i.xo-icon-sr
| Shared SR
.panel-body
table.table.table-hover
@@ -115,24 +121,30 @@
th Type
th Size
th.col-md-4 Physical/Allocated usage
th.col-md-1 Action
tr(
ng-repeat="SR in srs | map | orderBy:natural('name_label') track by SR.id"
xo-sref="SRs_view({id: SR.id})"
)
td.oneliner {{SR.name_label}}
td.oneliner
| {{SR.name_label}}&nbsp;
span.label.label-primary(ng-if="SR.id === pool.default_SR") Default SR
td.oneliner {{SR.name_description}}
td {{SR.SR_type}}
td {{SR.size | bytesToSize}}
td
.progress-condensed
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | percentage}}", tooltip="Allocated: {{[(SR.usage), SR.size] | percentage}}")
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
td
span.pull-right.btn-group.quick-buttons
a(ng-if="SR.id !== pool.default_SR", xo-click="setDefaultSr(SR.id)")
i.fa.fa-hdd-o.fa-lg(tooltip="Set as default SR")
//- Logs panel
.grid
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-comments(style="color: #e25440;")
i.fa.fa-comments
| Logs
span.quick-edit(ng-if="pool.messages | isNotEmpty", tooltip="Remove all logs", xo-click="deleteAllLog()")
i.fa.fa-trash-o.fa-fw
@@ -148,5 +160,5 @@
span.pull-right.btn-group.quick-buttons
a(xo-click="deleteLog(message.id)")
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
.center(ng-if = '(pool.messages | count) > 5')
.center(ng-if = '(pool.messages | count) > 5 || currentLogPage > 1')
pagination(boundary-links="true", total-items="pool.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")

View File

@@ -1,15 +1,27 @@
import angular from 'angular'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import Bluebird from 'bluebird'
import filter from 'lodash.filter'
import forEach from 'lodash.foreach'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
const HIGH_LEVEL_OBJECTS = {
pool: true,
host: true,
VM: true,
SR: true,
network: true
}
export default angular.module('settings.acls', [
uiBootstrap,
uiRouter,
uiSelect,
@@ -21,9 +33,6 @@ export default angular.module('settings.acls', [
controller: 'SettingsAcls as ctrl',
url: '/acls',
resolve: {
acls (xo) {
return xo.acl.get()
},
users (xo) {
return xo.user.getAll()
},
@@ -37,8 +46,17 @@ export default angular.module('settings.acls', [
template: view
})
})
.controller('SettingsAcls', function ($scope, acls, users, groups, roles, xoApi, xo) {
this.acls = acls
.controller('SettingsAcls', function ($scope, users, groups, roles, xoApi, xo, selectHighLevelFilter, filterFilter) {
const refreshAcls = () => {
xo.acl.get().then(acls => {
forEach(acls, acl => acl.newRole = acl.action)
this.acls = acls
})
}
refreshAcls()
this.types = Object.keys(HIGH_LEVEL_OBJECTS)
this.selectedTypes = {}
this.users = users
this.roles = roles
@@ -62,12 +80,6 @@ export default angular.module('settings.acls', [
this.objects = xoApi.all
let refreshAcls = () => {
xo.acl.get().then(acls => {
this.acls = acls
})
}
this.getUser = (id) => {
for (let user of this.users) {
if (user.id === id) {
@@ -77,22 +89,43 @@ export default angular.module('settings.acls', [
}
this.addAcl = () => {
xo.acl.add(this.subject.id, this.object.id, this.role.id).then(refreshAcls)
this.subject = this.object = this.role = null
const promises = []
forEach(this.selectedObjects, object => promises.push(xo.acl.add(this.subject.id, object.id, this.role.id)))
this.subject = this.selectedObjects = this.role = null
Bluebird.all(promises).then(refreshAcls)
}
this.removeAcl = (subject, object, role) => {
xo.acl.remove(subject, object, role).then(refreshAcls)
}
this.editAcl = (subject, object, role, newRole) => {
console.log(subject, object, role, newRole)
xo.acl.remove(subject, object, role)
.then(xo.acl.add(subject, object, newRole))
.then(refreshAcls)
}
this.toggleType = (toggle, type) => {
const selectedObjects = this.selectedObjects && this.selectedObjects.slice() || []
if (toggle) {
const objects = filterFilter(selectHighLevelFilter(this.objects), {type})
forEach(objects, object => { selectedObjects.indexOf(object) === -1 && selectedObjects.push(object) })
this.selectedObjects = selectedObjects
} else {
const keptObjects = []
for (let index in this.selectedObjects) {
const object = this.selectedObjects[index]
if (object.type !== type) {
keptObjects.push(object)
}
}
this.selectedObjects = keptObjects
}
}
})
.filter('selectHighLevel', () => {
const HIGH_LEVEL_OBJECTS = {
pool: true,
host: true,
VM: true,
SR: true
}
let isHighLevel = (object) => HIGH_LEVEL_OBJECTS[object.type]
return (objects) => filter(objects, isHighLevel)
})
.name

View File

@@ -1,82 +1,75 @@
.grid
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-key(style="color: #e25440;")
| ACLs
.grid
.grid-lg
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-plus-circle(style="color: #e25440;")
i.fa.fa-plus-circle
| Create
.panel-body.text-center
form(
ng-submit = 'ctrl.addAcl()'
)
.panel-body
form(ng-submit = 'ctrl.addAcl()')
.form-group
ui-select(
ng-model = 'ctrl.subject'
)
ui-select-match(
placeholder = 'Choose a user or group'
)
ui-select(ng-model = 'ctrl.subject')
ui-select-match(placeholder = 'Choose a user or group')
div
span(ng-if = '$select.selected.email')
i.fa.fa-fw.fa-user
i.xo-icon-user.fa-fw
| {{$select.selected.email}}
span(ng-if = '$select.selected.name')
i.fa.fa-fw.fa-users
i.xo-icon-group.fa-fw
| {{$select.selected.name}}
ui-select-choices(
repeat = 'entity in ctrl.entities | filter:{ permission: "!admin" } | filter:$select.search'
)
ui-select-choices(repeat = 'entity in ctrl.entities | filter:{ permission: "!admin" } | filter:$select.search')
div
span(ng-if = 'entity.email')
i.fa.fa-fw.fa-user
i.xo-icon-user.fa-fw
| {{entity.email}}
span(ng-if = 'entity.name')
i.fa.fa-fw.fa-users
i.xo-icon-group.fa-fw
| {{entity.name}}
.form-group
ui-select(
ng-model = 'ctrl.object'
)
ui-select-match(
placeholder = 'Choose an object'
)
div
i(class = 'xo-icon-{{$select.selected.type | lowercase}}')
| {{$select.selected.name_label}}
ui-select-choices(
repeat = 'object in ctrl.objects | selectHighLevel | filter:$select.search | orderBy:["type", "name_label"]'
)
ui-select(ng-model = 'ctrl.selectedObjects', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose an object')
i(class = 'xo-icon-{{$item.type | lowercase}}')
| {{$item.name_label}}
span(ng-if="($item.type === 'SR' || $item.type === 'VM') && $item.$container")
| ({{($item.$container | resolve).name_label}})
span(ng-if="$item.type === 'network'")
| ({{($item.$poolId | resolve).name_label}})
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:$select.search | orderBy:["type", "name_label"]')
div
i(class = 'xo-icon-{{object.type | lowercase}}')
| {{object.name_label}}
span(ng-if="(object.type === 'SR' || object.type === 'VM') && object.$container")
| ({{(object.$container | resolve).name_label}})
span(ng-if="object.type === 'network'")
| ({{(object.$poolId | resolve).name_label}})
.text-center
span(ng-repeat = 'type in ctrl.types')
label(tooltip = 'select/deselect all {{type}}s', style = 'cursor: pointer')
input.hidden(type = 'checkbox', ng-model = 'ctrl.selectedTypes[type]', ng-change = 'ctrl.toggleType(ctrl.selectedTypes[type], type)')
span.fa-stack
i(class = 'xo-icon-{{type | lowercase}}').fa-stack-1x
i.fa.fa-square-o.fa-stack-2x.text-info(ng-if = 'ctrl.selectedTypes[type]')
.form-group
ui-select(
ng-model = 'ctrl.role'
)
ui-select-match(
placeholder = 'Choose a role'
)
ui-select(ng-model = 'ctrl.role')
ui-select-match(placeholder = 'Choose a role')
div
i(class = 'xo-icon-{{$select.selected.type | lowercase}}')
| {{$select.selected.name}}
ui-select-choices(
repeat = 'role in ctrl.roles | filter:$select.search | orderBy:"name"'
)
ui-select-choices(repeat = 'role in ctrl.roles | filter:$select.search | orderBy:"name"')
div
i(class = 'xo-icon-{{role.type | lowercase}}')
| {{role.name}}
button.btn.btn-success
i.fa.fa-plus
| Create
.text-center
button.btn.btn-success
i.fa.fa-plus
| Create
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-street-view(style="color: #e25440;")
i.fa.fa-street-view
| Manage
.panel-body
table.table.table-hover
@@ -85,11 +78,11 @@
th Object
th Role
th
tr(ng-repeat = 'acl in ctrl.acls')
tr(ng-repeat = 'acl in ctrl.acls | orderBy:["subject", "object"] track by acl.id')
td {{ ctrl.usersById[acl.subject].email || ctrl.groupsById[acl.subject].name }}
td {{(acl.object | resolve).name_label}}
td {{ ctrl.rolesById[acl.action].name }}
td
select.form-control(ng-options = 'role.id as role.name for role in ctrl.roles | orderBy:"name"', ng-model = 'acl.newRole', ng-change = 'ctrl.editAcl(acl.subject, acl.object, acl.action, acl.newRole)')
td
button.btn.btn-danger(ng-click = 'ctrl.removeAcl(acl.subject, acl.object, acl.action)')
i.fa.fa-trash

View File

@@ -1,14 +1,14 @@
.grid
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-users(style="color: #e25440;")
i.xo-icon-group(style="color: #e25440;")
| {{ ctrl.group.name }}&nbsp;
a.btn.btn-default(ui-sref = 'settings.groups')
i.fa.fa-level-up
.grid
i.fa.fa-level-up
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-street-view(style="color: #e25440;")
i.fa.fa-street-view
| Members&nbsp;
span(ng-if = 'ctrl.isModified()') (*)
.panel-body
@@ -39,10 +39,10 @@
button.btn.btn-default(ng-if = 'ctrl.isModified()', type="button", ng-click = 'ctrl.cancelEdition()')
i.fa.fa-times
| Cancel
.grid
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-key(style="color: #e25440;")
i.fa.fa-key
| ACLs&nbsp;
.panel-body
table.table.table-hover
@@ -58,7 +58,7 @@
i.fa.fa-trash
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-eye(style="color: #e25440;")
i.fa.fa-eye
| Accessible objects&nbsp;
.panel-body
p(ng-repeat = 'object in ctrl.objects | selectHighLevel | canAccess:ctrl.group:ctrl.acls | orderBy:["type", "name_label"]')

View File

@@ -1,9 +1,9 @@
.grid
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-users(style="color: #e25440;")
i.xo-icon-group(style="color: #e25440;")
| Groups
.grid
.grid-sm
.panel.panel-default
form(ng-submit="ctrl.saveGroups()", autocomplete="off").panel-body
table.table.table-hover

View File

@@ -4,8 +4,10 @@ import uiRouter from 'angular-ui-router'
import acls from './acls'
import group from './group'
import groups from './groups'
import plugins from './plugins'
import servers from './servers'
import update from './update'
import user from './user'
import users from './users'
import view from './view'
@@ -16,13 +18,18 @@ export default angular.module('settings', [
acls,
group,
groups,
plugins,
servers,
update,
user,
users
])
.config(function ($stateProvider) {
$stateProvider.state('settings', {
abstract: true,
data: {
requireAdmin: true
},
template: view,
url: '/settings'
})
@@ -36,4 +43,3 @@ export default angular.module('settings', [
})
})
.name

View File

@@ -0,0 +1,274 @@
import angular from 'angular'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import marked from 'marked'
import trim from 'lodash.trim'
import uiRouter from 'angular-ui-router'
import remove from 'lodash.remove'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
import multiStringView from './multi-string-view'
import objectInputView from './object-input-view'
function isRequired (key, schema) {
return find(schema.required, item => item === key) || false
}
function isPassword (key) {
return key.search(/password|secret/i) !== -1
}
function loadDefaults (schema, configuration) {
if (!schema || !configuration) {
return
}
forEach(schema.properties, (item, key) => {
if (item.type === 'boolean' && !(key in configuration)) { // String default values are used as placeholders in view
configuration[key] = Boolean(item && item.default)
}
})
}
function cleanUpConfiguration (schema, configuration, dump = {}) {
if (!schema || !configuration) {
return
}
function sanitizeItem (item) {
if (typeof item === 'string') {
item = trim(item)
}
return item
}
function keepItem (item) {
return !(item == null || item === '' || (Array.isArray(item) && item.length === 0))
}
forEach(configuration, (item, key) => {
item = sanitizeItem(item)
configuration[key] = item
dump[key] = item
if (!keepItem(item) || !schema.properties || !(key in schema.properties)) {
delete dump[key]
} else if (schema.properties && schema.properties[key]) {
const type = schema.properties[key].type
if (type === 'integer' || type === 'number') {
dump[key] = +dump[key]
} else if (type === 'object') {
dump[key] = {}
cleanUpConfiguration(schema.properties[key], item, dump[key])
}
}
})
}
export default angular.module('settings.plugins', [
uiRouter,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.plugins', {
controller: 'SettingsPlugins as ctrl',
url: '/plugins',
data: {
requireAdmin: true
},
resolve: {
},
template: view
})
})
.controller('SettingsPlugins', function (xo, notify, modal) {
this.disabled = {}
const preparePluginForView = plugin => {
plugin._loaded = plugin.loaded
plugin._autoload = plugin.autoload
if (!plugin.configuration) {
plugin.configuration = {}
}
loadDefaults(plugin.configurationSchema, plugin.configuration)
}
const refreshPlugin = id => {
xo.plugin.get()
.then(plugins => {
const plugin = find(plugins, plugin => plugin.id === id)
if (plugin) {
preparePluginForView(plugin)
remove(this.plugins, plugin => plugin.id === id)
this.plugins.push(plugin)
}
})
}
const refreshPlugins = () => xo.plugin.get().then(plugins => {
forEach(plugins, preparePluginForView)
this.plugins = plugins
})
refreshPlugins()
const _execPluginMethod = (id, method, ...args) => {
this.disabled[id] = true
return xo.plugin[method](...args)
.finally(() => {
refreshPlugin(id)
this.disabled[id] = false
})
}
this.isRequired = isRequired
this.isPassword = isPassword
this.configure = (plugin) => {
const newConfiguration = {}
plugin.errors = []
cleanUpConfiguration(plugin.configurationSchema, plugin.configuration, newConfiguration)
_execPluginMethod(plugin.id, 'configure', plugin.id, newConfiguration)
.then(() => notify.info({
title: 'Plugin configuration',
message: 'Successfully saved'
}))
.catch(err => {
forEach(err.data, data => {
const fieldPath = data.field.split('.').slice(1)
const fieldPathTitles = []
let groupObject = plugin.configurationSchema
forEach(fieldPath, groupName => {
groupObject = groupObject.properties[groupName]
fieldPathTitles.push(groupObject.title || groupName)
})
plugin.errors.push(`${fieldPathTitles.join(' > ')} ${data.message}`)
})
})
}
this.purgeConfiguration = (plugin) => {
modal.confirm({
title: 'Purge configuration',
message: 'Are you sure you want to purge this configuration ?'
}).then(() => {
_execPluginMethod(plugin.id, 'purgeConfiguration', plugin.id).then(() => {
notify.info({
title: 'Purge configuration',
message: 'This plugin config is now purged.'
})
})
})
}
this.toggleAutoload = (plugin) => {
let method
if (!plugin._autoload && plugin.autoload) {
method = 'disableAutoload'
} else if (plugin._autoload && !plugin.autoload) {
method = 'enableAutoload'
}
if (method) {
_execPluginMethod(plugin.id, method, plugin.id)
}
}
this.toggleLoad = (plugin) => {
let method
if (!plugin._loaded && plugin.loaded && plugin.unloadable !== false) {
method = 'unload'
} else if (plugin._loaded && !plugin.loaded) {
method = 'load'
}
if (method) {
_execPluginMethod(plugin.id, method, plugin.id)
}
}
})
.directive('multiStringInput', () => {
return {
restrict: 'E',
template: multiStringView,
scope: {
model: '='
},
controller: 'MultiString as ctrl',
bindToController: true
}
})
.controller('MultiString', function ($scope, xo, xoApi) {
const checkModel = () => {
if (this.model === undefined || this.model === null) {
this.model = []
}
if (!Array.isArray(this.model)) {
throw new Error('multiString directive model must be an array')
}
}
checkModel()
$scope.$watch(() => this.model, checkModel)
this.add = (string) => {
string = trim(string)
if (string === '') {
return
}
this.model.push(string)
}
this.remove = (index) => {
this.model.splice(index, 1)
}
})
.directive('confObjectInput', () => {
return {
restrict: 'E',
template: objectInputView,
scope: {
model: '=',
schema: '=',
required: '='
},
controller: 'ConfObjectInput as ctrl',
bindToController: true
}
})
.controller('ConfObjectInput', function ($scope, xo, xoApi) {
const prepareModel = () => {
if (this.model === undefined || this.model === null) {
this.model = {
__use: this.required
}
} else {
if (typeof this.model !== 'object' || Array.isArray(this.model)) {
throw new Error('objectInput directive model must be a plain object')
}
if (!('__use' in this.model)) {
this.model.__use = true
}
}
loadDefaults(this.schema, this.model)
}
prepareModel()
$scope.$watch(() => this.model, prepareModel)
this.isRequired = isRequired
this.isPassword = isPassword
})
.filter('md2html', function ($sce) {
return function (input) {
return $sce.trustAsHtml(marked(input || ''))
}
})
.name

View File

@@ -0,0 +1,10 @@
ul(style = 'padding-left: 0;')
li.list-group-item.clearfix(ng-repeat = 'item in ctrl.model track by $index')
| {{item}}
button.btn.btn-default.btn-sm.pull-right(type = 'button', ng-click = 'ctrl.remove($index)')
i.fa.fa-times
form(ng-submit = 'ctrl.add(newItem); newItem = ""')
.input-group
input.form-control.input-sm(type = 'text', ng-model = 'newItem')
span.input-group-btn
button.btn.btn-primary.btn-sm(type = 'submit') Add

View File

@@ -0,0 +1,29 @@
.checkbox(ng-if = '!ctrl.required')
label
input(type = 'checkbox', ng-model = 'ctrl.model.__use')
| &nbsp;Configure (optional)
fieldset(ng-disabled = '!ctrl.required && !ctrl.model.__use', ng-hide = '!ctrl.required && !ctrl.model.__use')
ul(style = 'padding-left: 0;')
li.list-group-item(ng-repeat = '(key, value) in ctrl.schema.properties track by key')
.input-group(ng-if = 'value.type != "boolean"')
span.input-group-addon
| {{value.title || key}}
span.text-warning(ng-if = 'ctrl.isRequired(key, ctrl.schema)') *
input.form-control.input-sm(
ng-if = 'value.type != "number" && value.type != "integer"',
type = '{{ctrl.isPassword(key) ? "password" : "text"}}',
ng-model = 'ctrl.model[key]',
ng-required = 'ctrl.isRequired(key, ctrl.schema)'
)
input.form-control.input-sm(
ng-if = 'value.type == "number" || value.type == "integer"',
type = 'number',
ng-model = 'ctrl.model[key]',
ng-required = 'ctrl.isRequired(key, ctrl.schema)'
)
.form-inline(ng-if = 'value.type == "boolean"')
.checkbox.small('style="color: #31708F;"') {{value.title || key}}&nbsp;:&nbsp;
label('style="color: #A7AFB0;"')
i.fa.fa-2x(ng-class = '{"fa-toggle-on": ctrl.model[key], "fa-toggle-off": !ctrl.model[key]}')
input.hidden(type = 'checkbox', ng-model = 'ctrl.model[key]')
.help-block(ng-bind-html = 'ctrl.schema.properties[key].description | md2html')

View File

@@ -0,0 +1,57 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-plugin(style="color: #e25440;")
| Plugins
.grid-sm
.panel.panel-default
.panel-body
p.text-center(ng-if = '!ctrl.plugins || !ctrl.plugins.length') No plugins found
div(ng-repeat = 'plugin in ctrl.plugins | orderBy:"name" track by plugin.id')
h3.form-inline.clearfix
span.text-info {{ plugin.name }}&nbsp;
.checkbox.small
label
i.fa.fa-2x(ng-class = '{"fa-toggle-on": plugin.loaded, "fa-toggle-off": !plugin.loaded, "text-success": plugin.loaded}')
span(ng-if = 'plugin.loaded && plugin.unloadable === false')
| &nbsp;
i.fa.fa-2x.fa-lock(tooltip = 'This plugin cannot be unloaded without a server restart')
input.hidden(type = 'checkbox', ng-model = 'plugin._loaded', ng-change = 'ctrl.toggleLoad(plugin)', ng-disabled = 'plugin.unloadable === false && plugin.loaded || ctrl.disabled[plugin.id]')
| &nbsp;
.checkbox.small
label
| Auto-load at server start&nbsp;
input(type = 'checkbox', ng-model = 'plugin._autoload', ng-change = 'ctrl.toggleAutoload(plugin)', ng-disabled = 'ctrl.disabled[plugin.id]')
.form-group.pull-right.small
button.btn.btn-default(type = 'button', ng-click = 'isExpanded = !isExpanded'): i.fa(ng-class = '{"fa-plus": !isExpanded, "fa-minus": isExpanded}')
hr
div(collapse = '!isExpanded')
p(ng-if = '!plugin.configurationSchema') This plugin has no specific configuration
form.form-horizontal(ng-if = 'plugin.configurationSchema', ng-submit = 'ctrl.configure(plugin)')
fieldset(ng-disabled = 'ctrl.disabled[plugin.id]')
.form-group(ng-repeat = '(key, prop) in plugin.configurationSchema.properties')
label.col-md-2.control-label
| {{prop.title || key}}
span.text-warning(ng-if = 'ctrl.isRequired(key, plugin.configurationSchema)') *
.col-md-5
input.form-control(ng-if = 'prop.type === "integer" || prop.type === "number"', type = 'number', ng-model = 'plugin.configuration[key]', ng-required = 'ctrl.isRequired(key, plugin.configurationSchema)', placeholder = '{{ plugin.configurationSchema.properties[key].default }}')
input.form-control(ng-if = 'prop.type === "string"', type = '{{ ctrl.isPassword(key) ? "password" : "text" }}', ng-model = 'plugin.configuration[key]', ng-required = 'ctrl.isRequired(key, plugin.configurationSchema)')
multi-string-input(ng-if = 'prop.type === "array" && prop.items.type === "string"', model = 'plugin.configuration[key]')
.checkbox(ng-if = 'prop.type === "boolean"'): label: input(type = 'checkbox', ng-model = 'plugin.configuration[key]')
conf-object-input(ng-if = 'prop.type === "object"', model = 'plugin.configuration[key]', schema = 'prop', required = 'ctrl.isRequired(key, plugin.configurationSchema)')
.col-md-5
span.help-block(ng-bind-html = 'prop.description | md2html')
.form-group
.col-md-offset-2.col-md-10.text-danger(ng-repeat = "err in plugin.errors")
| {{ err }}
.form-group
.col-md-offset-2.col-md-10
.btn-toolbar
.btn-group
button.btn.btn-primary(type = 'submit')
| Save configuration&nbsp;
i.fa.fa-floppy-o
.btn-group
button.btn.btn-danger(type = 'button' ng-click = 'ctrl.purgeConfiguration(plugin)')
| Purge configuration&nbsp;
i.fa.fa-trash-o

View File

@@ -1,4 +1,6 @@
import angular from 'angular'
import forEach from 'lodash.foreach'
import includes from 'lodash.includes'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
@@ -26,26 +28,36 @@ export default angular.module('settings.servers', [
template: view
})
})
.controller('SettingsServers', function ($scope, $interval, servers, xoApi, xo, notify) {
this.servers = servers
.controller('SettingsServers', function ($scope, $rootScope, $interval, $filter, servers, xoApi, xo, notify) {
const orderBy = $filter('orderBy')
this.servers = orderBy(servers, $rootScope.natural('host'))
$scope.readOnly = {}
forEach(this.servers, (server) => {
$scope.readOnly[server.id] = Boolean(server.readOnly)
})
const selected = this.selectedServers = {}
const newServers = this.newServers = []
const refreshServers = () => {
xo.server.getAll().then(servers => {
this.servers = servers
this.servers = orderBy(servers, $rootScope.natural('host'))
})
}
const refreshServersIfUnfocused = () => {
if (!$scope.isFocused) {
refreshServers()
}
}
const interval = $interval(refreshServers, 10e3)
const interval = $interval(refreshServersIfUnfocused, 10e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.connectServer = (id) => {
notify.info({
title: 'Server connect',
message: 'Connecting the server...'
title: 'Server connect',
message: 'Connecting the server...'
})
xo.server.connect(id).catch(error => {
notify.error({
@@ -57,8 +69,8 @@ export default angular.module('settings.servers', [
this.disconnectServer = (id) => {
notify.info({
title: 'Server disconnect',
message: 'Disconnecting the server...'
title: 'Server disconnect',
message: 'Disconnecting the server...'
})
xo.server.disconnect(id)
}
@@ -73,6 +85,9 @@ export default angular.module('settings.servers', [
this.addServer()
this.saveServers = () => {
const addresses = []
forEach(xoApi.getView('host').all, host => addresses.push(host.address))
const newServers = this.newServers
const servers = this.servers
const updateServers = []
@@ -87,6 +102,7 @@ export default angular.module('settings.servers', [
if (!server.password) {
delete server.password
}
server.readOnly = $scope.readOnly[id]
xo.server.set(server)
delete server.password
updateServers.push(server)
@@ -94,17 +110,26 @@ export default angular.module('settings.servers', [
}
for (let i = 0, len = newServers.length; i < len; i++) {
const server = newServers[i]
const {host, username, password} = server
const {host, username, password, readOnly} = server
if (!host) {
continue
}
if (includes(addresses, host)) {
notify.warning({
title: 'Server already connected',
message: `You are already connected to ${host}`
})
continue
}
xo.server.add({
host,
username,
password,
readOnly,
autoConnect: false
}).then(function (id) {
server.id = id
$scope.readOnly[id] = Boolean(readOnly)
xo.server.connect(id).catch(error => {
notify.error({
title: 'Server connection error',

View File

@@ -1,36 +1,50 @@
.grid
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-cloud(style="color: #e25440;")
| Servers
.grid
.grid-sm
.panel.panel-default
//- .panel-heading.panel-title
//- i.fa.fa-cloud(style="color: #e25440;")
//- | Connections
form(ng-submit="ctrl.saveServers()", autocomplete="off").panel-body
table.table.table-hover
tr
th.col-md-5 Host
th.col-md-2 User
th.col-md-3 Password
th.col-md-2 Password
th.col-md-1.text.center Actions
th.col-md-1.text.center Read only
th.col-md-1.text-center
i.fa.fa-trash-o.fa-lg(tooltip="Forget server")
tr(ng-repeat="server in ctrl.servers | orderBy:natural('host') track by server.id")
tr(ng-repeat="server in ctrl.servers track by server.id")
td
.input-group
span.input-group-addon(ng-if="server.status === 'connected'")
i.fa.fa-check-circle.fa-lg.text-success(tooltip="Connected")
span.input-group-addon(ng-if="server.status === 'disconnected'")
i.fa.fa-times-circle.fa-lg.text-danger(tooltip="Disconnected")
span.input-group-addon(ng-if="server.status === 'connecting'")
span.input-group-addon.hidden-xs(ng-if="server.status === 'connected'")
i.xo-icon-success.fa-lg(tooltip="Connected")
span.input-group-addon.hidden-xs(ng-if="server.status === 'disconnected'")
i.xo-icon-failure.fa-lg(tooltip="Disconnected")
span.input-group-addon.hidden-xs(ng-if="server.status === 'connecting'")
i.fa.fa-cog.fa-lg.fa-spin(tooltip="Connecting...")
input.form-control(type="text", ng-model="server.host")
input.form-control(
type="text",
ng-model="server.host",
ng-focus="$parent.isFocused = true",
ng-blur="$parent.isFocused = false"
)
td
input.form-control(type="text", ng-model="server.username")
input.form-control(
type="text",
ng-model="server.username",
ng-focus="$parent.isFocused = true",
ng-blur="$parent.isFocused = false"
)
td
input.form-control(type="password", ng-model="server.password", placeholder="Fill to change the password")
input.form-control(
type="password",
ng-model="server.password",
placeholder="Fill to change the password",
ng-focus="$parent.isFocused = true",
ng-blur="$parent.isFocused = false"
)
td.text-center
button.btn.btn-default(
ng-if="server.status === 'disconnected'",
@@ -46,6 +60,8 @@
tooltip="Disconnect this server"
)
i.fa.fa-unlink
td.text-center
input(type="checkbox", ng-model="readOnly[server.id]")
td.text-center
input(type="checkbox", ng-model="ctrl.selectedServers[server.id]")
tr(ng-repeat="server in ctrl.newServers")
@@ -70,6 +86,8 @@
placeholder="password"
)
td &#160;
td.text-center
input( type="checkbox", ng-model="server.readOnly")
td &#160;
p.text-center
button.btn.btn-primary(type="submit")

View File

@@ -36,25 +36,24 @@ export default angular.module('settings.update', [
this.updater = updater
this.updater.isRegistered()
.then(() => this.updater.on('end', () => {
if (this.updater.state === 'registerNeeded' && this.updater.registerState !== 'unregistered' && this.updater.registerState !== 'error') {
this.updater.isRegistered()
}
}))
.then(() => this.updater.on('end', () => this.updater.isRegistered()))
.catch(err => console.error(err))
this.updater.getConfiguration()
.then(configuration => this.configuration = _assign({}, configuration))
.then(() => this.withAuth = Boolean(this.configuration.proxyUser))
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message
}))
this.registerXoa = (email, password) => {
this.registerXoa = (email, password, renewRegister) => {
this.regPwd = ''
this.updater.register(email, password)
this.updater.register(email, password, renewRegister)
.tap(() => this.renewRegister = false)
.then(() => this.updater.update())
.catch(AuthenticationFailed, () => {})
.catch(err => console.error(err))
}
this.update = () => {
@@ -73,12 +72,19 @@ export default angular.module('settings.update', [
}))
}
this.configure = (host, port) => {
this.configure = (host, port, username, password) => {
const config = {}
if (!this.withAuth) {
username = null
password = null
}
config.proxyHost = host && host.trim() || null
config.proxyPort = port && port.trim() || null
config.proxyUser = username || null
config.proxyPassword = password || null
return this.updater.configure(config)
.then(configuration => this.configuration = _assign({}, configuration))
.then(() => this.withAuth = Boolean(this.configuration.proxyUser))
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message

View File

@@ -1,12 +1,12 @@
.grid
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-refresh(style="color: #e25440;")
| Update
.grid
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-globe(style="color: #e25440;")
i.fa.fa-globe
| Status
.panel-body
p(ng-if = '!ctrl.updater.state')
@@ -35,10 +35,10 @@
p(ng-repeat = 'entry in ctrl.updater._log')
span(ng-class = '{"text-danger": entry.level === "error", "text-muted": entry.level === "info", "text-warning": entry.level === "warning", "text-success": entry.level === "success"}') {{ entry.date }}
| &nbsp;:&nbsp;
span(ng-bind-html = 'entry.message | ansitohtml')
span(style = 'word-wrap: break-word;', ng-bind-html = 'entry.message | ansitohtml')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-pencil(style="color: #e25440;")
i.fa.fa-pencil
| Registration
.panel-body.text-center
.text-warning(ng-if = '!ctrl.updater.state || ctrl.updater.registerState === "unknown"')
@@ -57,8 +57,11 @@
button.btn.btn-default(type = 'button', ng-click = 'ctrl.updater.isRegistered()')
i.fa.fa-refresh
| Refresh
form(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "unregistered"', ng-submit = 'ctrl.registerXoa(ctrl.regEmail, ctrl.regPwd)')
p.form-static-control XOA is not registered.
form(ng-if = 'ctrl.updater.state && (ctrl.renewRegister || ctrl.updater.registerState === "unregistered")', ng-submit = 'ctrl.registerXoa(ctrl.regEmail, ctrl.regPwd, ctrl.renewRegister)')
p.form-static-control(ng-if = '!ctrl.renewRegister') XOA is not registered.
p.form-static-control(ng-if = 'ctrl.renewRegister')
| Forget previous registration ?&nbsp;
button.btn.btn-default(type = 'button', ng-click = 'ctrl.renewRegister = false') Cancel
p.small Your xen-orchestra.com email and password
.form-group
.input-group
@@ -75,38 +78,51 @@
i.fa.fa-check
| Register
p.form-static-control.text-danger {{ ctrl.updater.registerError }}
p(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "registered"')
p(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "registered" && !ctrl.renewRegister')
| Your Xen Orchestra appliance is registered to
span.text-success {{ ctrl.updater.token.registrationEmail }}
| .
br
span.big-stat
i.fa.fa-check-circle.text-success
.grid
.grid-cell(ng-if = 'ctrl.updater.state && ctrl.configuration')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs(style="color: #e25440;")
| Settings
.panel-body
form.form-inline(ng-submit = 'ctrl.configure(ctrl.configuration.proxyHost, ctrl.configuration.proxyPort)')
fieldset
h4
i.fa.fa-globe
| Proxy settings
p If you need a proxy to access the Internet
br
i.fa.fa-check-circle.fa-3x.text-success
br
br
button.btn.btn-default(type = 'button', ng-click = 'ctrl.renewRegister = true') Register to someone else ?
.grid-sm(ng-if = 'ctrl.updater.state && ctrl.configuration')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| Settings
.panel-body
form(ng-submit = 'ctrl.configure(ctrl.configuration.proxyHost, ctrl.configuration.proxyPort, ctrl.configuration.proxyUser, ctrl.configuration.proxyPassword)')
h4
i.fa.fa-globe
| Proxy settings
p
| If you need a proxy to access the Internet&ensp;
label
input(type = 'checkbox', ng-model = 'ctrl.withAuth')
| with authentication
fieldset.form-inline
.form-group
//- label.control-label Host:&nbsp;
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyHost', placeholder = 'Host (myproxy.example.org)')
| &nbsp;
.form-group
//- label.control-label Port:&nbsp;
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyPort', placeholder = 'Port (3128 ?...)')
br
div(ng-hide = '!ctrl.withAuth')
fieldset.form-inline(ng-disabled = '!ctrl.withAuth')
.form-group
label.control-label Host:&nbsp;
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyHost', placeholder = 'myproxy.example.org')
| &nbsp;
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyUser', placeholder = 'User name', required)
| &nbsp;
.form-group
label.control-label Port:&nbsp;
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyPort', placeholder = '3128')
| &nbsp;
| &nbsp;
| &nbsp;
.form-group
button.btn.btn-primary(type = 'submit')
i.fa.fa-floppy-o
| Save
input.form-control(type = 'password', ng-model = 'ctrl.configuration.proxyPassword', placeholder = 'Password')
br
fieldset
.form-group
button.btn.btn-primary(type = 'submit')
i.fa.fa-floppy-o
| Save

View File

@@ -0,0 +1,48 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
export default angular.module('settings.user', [
uiRouter,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.user', {
controller: 'SettingsUser as ctrl',
url: '/user',
data: {
requireAdmin: false
},
resolve: {
},
template: view
})
})
.controller('SettingsUser', function (xo, notify) {
this.changePassword = function (oldPassword, newPassword) {
this.working = true
xo.user.changePassword(oldPassword, newPassword)
.then(() => {
this.oldPassword = ''
this.newPassword = ''
this.confirmPassword = ''
notify.info({
title: 'Change password',
message: 'Password has been successfully change'
})
})
.catch(error => notify.error({
title: 'Change password',
message: error.message
}))
.finally(() => this.working = false)
}
})
.name

View File

@@ -0,0 +1,21 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-user(style="color: #e25440;")
| Profile
.grid-sm
.panel.panel-default
.panel-body
.row
.col-sm-6
form(ng-submit = 'ctrl.changePassword(ctrl.oldPassword, ctrl.newPassword)')
fieldset(ng-disabled = 'ctrl.working')
legend Change password
.form-group
input.form-control(type = 'password', ng-model = 'ctrl.oldPassword', placeholder = 'Current password', required)
.form-group
input.form-control(type = 'password', ng-model = 'ctrl.newPassword', placeholder = 'New password', required)
.form-group(ng-class = '{"has-error": ctrl.confirmPassword && ctrl.newPassword && (ctrl.confirmPassword !== ctrl.newPassword)}')
input.form-control(type = 'password', ng-model = 'ctrl.confirmPassword', placeholder = 'Confirm password', required)
.form-group
button.btn.btn-primary(type = 'submit', ng-disabled = '!ctrl.oldPassword || !ctrl.newPassword || ctrl.newPassword !== ctrl.confirmPassword') Save password

View File

@@ -1,4 +1,5 @@
import angular from 'angular'
import passwordGenerator from 'password-generator'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import uiEvent from 'angular-ui-event'
@@ -121,6 +122,11 @@ export default angular.module('settings.users', [
this.editingUser = editing => {
this._editingUser = editing
}
this.generatePassword = (user) => {
// Generate password of 8 letters/numbers/underscore
user.password = passwordGenerator(8, false)
}
})
.name

View File

@@ -1,58 +1,111 @@
.grid
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-user(style="color: #e25440;")
i.xo-icon-user(style="color: #e25440;")
| Users
.grid
.grid-sm
.panel.panel-default
//- .panel-heading.panel-title
//- i.fa.fa-users(style="color: #e25440;")
//- | Users
form(ng-submit="ctrl.saveUsers()", autocomplete="off").panel-body
table.table.table-hover
tr
th.col-md-4 Email
th.col-md-4 Permissions
th.col-md-3 Password
th.col-md-1.text-center
i.fa.fa-trash-o.fa-lg(tooltip="Remove user")
tr(ng-repeat="user in ctrl.users | orderBy:natural('id') track by user.id")
td
input.form-control(type="text", ng-model="user.email", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
td
select.form-control(ng-options="p.value as p.label for p in ctrl.permissions", ng-model="user.permission", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
td
input.form-control(type="password", ng-model="user.password", placeholder="Fill to change the password", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
td.text-center
input(type="checkbox", ng-model="ctrl.selectedUsers[user.id]", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
tr(ng-repeat="user in ctrl.newUsers")
td
input.form-control(
type = "text"
ng-model = "user.email"
placeholder = "email"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td
select.form-control(
ng-options = "p.value as p.label for p in ctrl.permissions"
ng-model = "user.permission"
ng-required = "user.email"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td
input.form-control(
type = "password"
ng-model = "user.password"
ng-required = "user.email"
placeholder = "password"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td &#160;
p.text-center
button.btn.btn-primary(type="submit")
i.fa.fa-save
| Save
| &nbsp;
button.btn.btn-success(type="button", ng-click="ctrl.addUser()")
i.fa.fa-plus
.panel-body
form(ng-submit="ctrl.saveUsers()", autocomplete="off")
table.table.table-hover
tr
th.col-md-4 Email
th.col-md-4 Permissions
th.col-md-3 Password
th.col-md-1.text-center
i.fa.fa-trash-o.fa-lg(tooltip="Remove user")
tr(ng-repeat="user in ctrl.users | orderBy:natural('id') track by user.id")
td
input.form-control(
type="text"
ng-model="user.email"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td
select.form-control(
ng-options="p.value as p.label for p in ctrl.permissions"
ng-model="user.permission"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td
div.input-group
span.input-group-btn
button.btn.btn-default(
type = "button"
tooltip = "Generate random password"
ng-click = "ctrl.generatePassword(user); showPassword = true"
)
i.fa.fa-key
input.form-control(
type = "{{ showPassword ? 'text' : 'password' }}"
ng-model = "user.password"
placeholder = "Fill to change the password"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
span.input-group-btn
button.btn.btn-default(
type = "button"
tooltip = "Reveal password"
ng-show = "user.password.length > 0"
ng-mousedown = "showPassword = true"
ng-mouseup = "showPassword = false"
ng-mouseleave = "showPassword = false"
)
i.fa.fa-eye(ng-if = "showPassword")
i.fa.fa-eye-slash(ng-if = "!showPassword")
td.text-center
input(
type="checkbox"
ng-model="ctrl.selectedUsers[user.id]"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
tr(ng-repeat="user in ctrl.newUsers")
td
input.form-control(
type = "text"
ng-model = "user.email"
placeholder = "email"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td
select.form-control(
ng-options = "p.value as p.label for p in ctrl.permissions"
ng-model = "user.permission"
ng-required = "user.email"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td
div.input-group
span.input-group-btn
button.btn.btn-default(
type = "button"
tooltip = "Generate random password"
ng-click = "ctrl.generatePassword(user); showPassword = true"
)
i.fa.fa-key
input.form-control(
type = "{{ showPassword ? 'text' : 'password' }}"
ng-model = "user.password"
ng-required = "user.email"
placeholder = "password"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
span.input-group-btn
button.btn.btn-default(
type = "button"
tooltip = "Reveal password"
ng-show = "user.password.length > 0"
ng-mousedown = "showPassword = true"
ng-mouseup = "showPassword = false"
ng-mouseleave = "showPassword = false"
)
i.fa.fa-eye(ng-if = "showPassword")
i.fa.fa-eye-slash(ng-if = "!showPassword")
td &#160;
p.text-center
button.btn.btn-primary(type="submit")
i.fa.fa-save
| Save
| &nbsp;
button.btn.btn-success(type="button", ng-click="ctrl.addUser()")
i.fa.fa-plus

View File

@@ -1,23 +1,28 @@
.grid(style = 'height: 100%')
//- Side menu
.settings-menu
.menu-grid
.side-menu
ul.nav
li
a(ui-sref = '.servers', ui-sref-active = 'active')
i.fa.fa-fw.fa-cloud.fa-menu
span.menu-entry Servers
li
a(ui-sref = '.users')
i.fa.fa-fw.fa-user.fa-menu
i.xo-icon-user.fa-fw.fa-menu
span.menu-entry Users
li
a(ui-sref = '.groups')
i.fa.fa-fw.fa-users.fa-menu
i.xo-icon-group.fa-fw.fa-menu
span.menu-entry Groups
li
a(ui-sref = '.acls')
i.fa.fa-fw.fa-key.fa-menu
span.menu-entry ACLs
li
a(ui-sref = '.plugins')
i.xo-icon-plugin.fa-fw.fa-menu
span.menu-entry Plugins
li
a(ui-sref = '.update')
i.fa.fa-fw.fa-refresh.fa-menu
span.menu-entry Update
//- Content
div.settings-content(ui-view = '')
.side-content(ui-view = '')

View File

@@ -1,16 +1,22 @@
import angular from 'angular'
import escapeRegExp from 'lodash.escaperegexp'
import filter from 'lodash.filter'
import forEach from 'lodash.foreach'
import isEmpty from 'lodash.isempty'
import trim from 'lodash.trim'
import uiRouter from 'angular-ui-router'
import Bluebird from 'bluebird'
import xoTag from 'tag'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.sr', [
uiRouter
uiRouter,
xoTag
])
.config(function ($stateProvider) {
$stateProvider.state('SRs_view', {
@@ -19,18 +25,52 @@ export default angular.module('xoWebApp.sr', [
template: view
})
})
.controller('SrCtrl', function ($scope, $stateParams, $state, $q, notify, xoApi, xo, modal, $window, bytesToSizeFilter) {
.filter('vdiFilter', (xoApi, filterFilter) => {
return (input, search) => {
search && (search = trim(search).toLowerCase())
return filter(input, vdi => {
let vbd, vm
let vmName = vdi.$VBDs && vdi.$VBDs[0] && (vbd = xoApi.get(vdi.$VBDs[0])) && (vm = xoApi.get(vbd.VM)) && vm.name_label
vmName && (vmName = vmName.toLowerCase())
return !search || (vmName && (vmName.search(escapeRegExp(search)) !== -1) || filterFilter([vdi], search).length)
})
}
})
.controller('SrCtrl', function ($scope, $stateParams, $state, $q, notify, xoApi, xo, modal, $window, bytesToSizeFilter, sizeToBytesFilter) {
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
$scope.units = ['MiB', 'GiB', 'TiB']
$scope.currentLogPage = 1
$scope.currentVDIPage = 1
let {get} = xoApi
$scope.$watch(() => xoApi.get($stateParams.id), function (SR) {
const VDIs = []
if (SR) {
forEach(SR.VDIs, vdi => {
vdi = xoApi.get(vdi)
if (vdi) {
const size = bytesToSizeFilter(vdi.size)
VDIs.push({...vdi, size, sizeValue: size.split(' ')[0], sizeUnit: size.split(' ')[1]})
}
})
}
$scope.SR = SR
$scope.VDIs = VDIs
})
$scope.selectedForDelete = {}
$scope.deleteSelectedVdis = function () {
return modal.confirm({
title: 'VDI deletion',
message: 'Are you sure you want to delete all selected VDIs? This operation is irreversible.'
}).then(function () {
forEach($scope.selectedForDelete, (selected, id) => selected && xo.vdi.delete(id))
$scope.selectedForDelete = {}
})
}
$scope.saveSR = function ($data) {
let {SR} = $scope
let {name_label, name_description} = $data
@@ -62,7 +102,12 @@ export default angular.module('xoWebApp.sr', [
$scope.disconnectVBD = function (id) {
console.log('Disconnect VBD', id)
return xoApi.call('vbd.disconnect', {id: id})
return modal.confirm({
title: 'VDI disconnection',
message: 'Are you sure you want to disconnect this VDI?'
}).then(function () {
return xoApi.call('vbd.disconnect', {id: id})
})
}
$scope.connectPBD = function (id) {
@@ -155,6 +200,7 @@ export default angular.module('xoWebApp.sr', [
$scope.saveDisks = function (data) {
// Group data by disk.
let disks = {}
let sizeChanges = false
forEach(data, function (value, key) {
let i = key.indexOf('/')
@@ -164,27 +210,52 @@ export default angular.module('xoWebApp.sr', [
;(disks[id] || (disks[id] = {}))[prop] = value
})
let promises = []
forEach(disks, function (attributes, id) {
// Keep only changed attributes.
let disk = get(id)
forEach(attributes, function (value, name) {
if (value === disk[name]) {
delete attributes[name]
}
})
if (!isEmpty(attributes)) {
// Inject id.
attributes.id = id
// Ask the server to update the object.
promises.push(xoApi.call('vdi.set', attributes))
attributes.size = bytesToSizeFilter(sizeToBytesFilter(attributes.sizeValue + ' ' + attributes.sizeUnit))
if (attributes.size !== bytesToSizeFilter(disk.size)) { // /!\ attributes are provided by a modified copy of disk
sizeChanges = true
return false
}
})
return $q.all(promises)
let promises = []
const preCheck = sizeChanges ? modal.confirm({
title: 'Disk resizing',
message: 'Growing the size of a disk is not reversible'
}) : $q.resolve()
return preCheck
.then(() => {
forEach(disks, function (attributes, id) {
let disk = get(id)
// Resize disks
attributes.size = bytesToSizeFilter(sizeToBytesFilter(attributes.sizeValue + ' ' + attributes.sizeUnit))
if (attributes.size !== bytesToSizeFilter(disk.size)) { // /!\ attributes are provided by a modified copy of disk
promises.push(xo.disk.resize(id, attributes.size))
}
delete attributes.size
// Keep only changed attributes.
forEach(attributes, function (value, name) {
if (value === disk[name]) {
delete attributes[name]
}
})
if (!isEmpty(attributes)) {
// Inject id.
attributes.id = id
// Ask the server to update the object.
promises.push(xoApi.call('vdi.set', attributes))
}
})
return $q.all(promises)
})
}
})

View File

@@ -6,10 +6,12 @@
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs(style="color: #e25440;")
i.fa.fa-cogs
| General
span.quick-edit(tooltip="Edit General settings", ng-click="srSettings.$show()")
span.quick-edit(tooltip="Edit General settings", ng-click="srSettings.$show()", ng-if = '!srSettings.$visible')
i.fa.fa-edit.fa-fw
span.quick-edit(tooltip="Cancel Edition", ng-click="srSettings.$cancel()", ng-if = 'srSettings.$visible')
i.fa.fa-undo.fa-fw
.panel-body
form(editable-form="", name="srSettings", onbeforesave="saveSR($data)")
dl.dl-horizontal
@@ -24,11 +26,8 @@
dt Content type:
dd {{SR.SR_type}}
dt Tags
dd(ng-if="SR.tags.length")
span(ng-repeat="tag in SR.tags")
span.label.label-primary {{tag}}
dd(ng-if="!SR.tags.length")
em No tags.
dd
xo-tag(ng-if = 'SR', object = 'SR')
dt Shared
div(ng-repeat="container in [SR.$container] | resolve")
dd(ng-if="'pool' === container.type")
@@ -51,86 +50,104 @@
| Save
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-stats(style="color: #e25440;")
i.xo-icon-stats
| Stats
.panel-body
.row
.col-sm-6.col-lg-4
p.stat-name Physical Alloc:
.col-sm-6
p.stat-name Physical usage:
canvas.stat-simple(id="doughnut", class="chart chart-doughnut", data="[(SR.physical_usage), (SR.size - SR.physical_usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
.col-sm-6.col-lg-4
p.stat-name Virtual Alloc:
canvas.stat-simple(id="doughnut", class="chart chart-doughnut", data="[(SR.usage), (SR.size - SR.usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
.col-sm-4.visible-lg
.col-sm-6
p.stat-name VDIs:
p.center.big-stat {{SR.VDIs.length}}
.row.hidden-lg
.col-sm-12
br
p.stat-name {{SR.VDIs.length}} VDIs
//- Action panel
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flash(style="color: #e25440;")
i.fa.fa-flash
| Actions
.panel-body.text-center
.grid
.grid-cell.btn-group
button.btn(tooltip="Rescan all the VDI", type="button", style="width: 90%", ng-click="rescanSr(SR.id)")
button.btn(tooltip="Rescan all the VDI", tooltip-placement="top", type="button", style="width: 90%", ng-click="rescanSr(SR.id)")
i.fa.fa-refresh.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Reconnect all hosts", type="button", style="width: 90%", ng-click="reconnectAllHosts()")
button.btn(tooltip="Reconnect all hosts", tooltip-placement="top", type="button", style="width: 90%", ng-click="reconnectAllHosts()")
i.fa.fa-retweet.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Disconnect all hosts", type="button", style="width: 90%", xo-click="disconnectAllHosts()")
button.btn(tooltip="Disconnect all hosts", tooltip-placement="top", type="button", style="width: 90%", xo-click="disconnectAllHosts()")
i.fa.fa-power-off.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Forget SR", type="button", style="width: 90%", xo-click="forgetSR(SR.id)")
button.btn(tooltip="Forget SR", tooltip-placement="top", type="button", style="width: 90%", xo-click="forgetSR(SR.id)")
i.fa.fa-2x.fa-fw.fa-ban
.grid-cell.btn-group
button.btn(tooltip="Remove SR", type="button", style="width: 90%", xo-click="removeSR(SR.id)")
button.btn(tooltip="Remove SR", tooltip-placement="top", type="button", style="width: 90%", xo-click="removeSR(SR.id)")
i.fa.fa-2x.fa-trash-o
//- TODO: Space panel
.grid
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory(style="color: #e25440;")
i.xo-icon-memory
| VDI Map
.panel-body
.progress
.progress-bar.progress-bar-vm(ng-if="((VDI.size/SR.size)*100) > 0.5", ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.id", role="progressbar", aria-valuemin="0", aria-valuenow="{{VDI.size}}", aria-valuemax="{{SR.size}}", style="width: {{[VDI.size, SR.size] | percentage}}", tooltip="{{VDI.name_label}} ({{[VDI.size, SR.size] | percentage}})")
.progress-bar.progress-bar-vm(
ng-if="((VDI.usage/SR.size)*100) > 0.5",
ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.id",
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{VDI.usage}}",
aria-valuemax="{{SR.size}}",
style="width: {{[VDI.usage, SR.size] | percentage}}",
tooltip="{{VDI.name_label}} ({{VDI.usage | bytesToSize}}) {{[VDI.usage, SR.size] | percentage}}"
)
//- display the name only if it fits in its progress bar
span(ng-if="VDI.name_label.length < ((VDI.size/SR.size)*100)") {{VDI.name_label}}
span(ng-if="VDI.name_label.length < ((VDI.usage/SR.size)*100)") {{VDI.name_label}}
ul.list-inline.text-center
li Total: {{SR.size | bytesToSize}}
li Currently used: {{SR.usage | bytesToSize}}
li Available: {{SR.size-SR.usage | bytesToSize}}
li Currently used: {{SR.physical_usage | bytesToSize}}
li Available: {{SR.size-SR.physical_usage | bytesToSize}}
//- TODO: VDIs.
.grid
form(name = "disksForm" editable-form = '' onbeforesave = 'saveDisks($data)').panel.panel-default
.panel-heading.panel-title
i.fa.fa-hdd-o(style="color: #e25440;")
i.xo-icon-disk
| Virtual disks
span.quick-edit(tooltip="Edit disks", ng-click="disksForm.$show()")
span.quick-edit(
ng-if="!disksForm.$visible"
tooltip="Edit disks"
ng-click="disksForm.$show()"
)
i.fa.fa-edit.fa-fw
span.quick-edit(
ng-if="disksForm.$visible"
tooltip="Cancel Edition"
ng-click="disksForm.$cancel()"
)
i.fa.fa-undo.fa-fw
span.quick-edit(tooltip="Rescan", ng-click="rescanSr(SR.id)")
i.fa.fa-refresh.fa-fw
.panel-body
table.table.table-hover
tr
th Name
th Description
th Size
th Virtual Machine:
tr(ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') | slice:(10*(currentVDIPage-1)):(10*currentVDIPage)")
th.col-sm-2 Name
th.col-sm-2 Description
th.col-sm-1 Tags
th.col-sm-1 Size
th.col-sm-1(ng-show="disksForm.$visible")
th.col-sm-2
| Virtual Machine:&nbsp;
span.pull-right: button.btn.btn-danger(type = 'button', xo-click = 'deleteSelectedVdis()', tooltip = 'Delete selected disks'): i.fa.fa-trash
tr(ng-repeat="VDI in VDIs | vdiFilter:vdiSearch | orderBy:natural('name_label') | slice:(10*(currentVDIPage-1)):(10*currentVDIPage)")
td.oneliner
span(
editable-text="VDI.name_label"
e-name = '{{VDI.id}}/name_label'
)
| {{VDI.name_label}} &nbsp;
span.label.label-info(ng-if="VDI.$snapshot_of") snapshot
span(ng-if="VDI.type === 'VDI-snapshot'")
span.label.label-info(ng-if="VDI.$snapshot_of") snapshot
span.label.label-warning(ng-if="!VDI.$snapshot_of") orphaned snapshot
td.oneliner
span(
editable-text="VDI.name_description"
@@ -138,18 +155,34 @@
)
| {{VDI.name_description}}
td
//- FIXME: should be editable, but the server needs first
//- to accept a human readable string.
| {{VDI.size | bytesToSize}}
td.oneliner {{((VDI.$VBD | resolve).VM | resolve).name_label}}
span.pull-right.btn-group.quick-buttons
a(ng-if="(VDI.$VBD | resolve).attached", xo-click="disconnectVBD(VBD.id)")
i.fa.fa-unlink.fa-lg(tooltip="Disconnect this disk")
a(ng-if="!(VDI.$VBD | resolve).attached", xo-click="deleteVDI(VDI.id)")
i.fa.fa-trash-o.fa-lg(tooltip="Destroy this disk")
xo-tag(object = 'VDI')
td
span(
editable-text="VDI.sizeValue"
e-name = '{{VDI.id}}/sizeValue'
)
| {{VDI.sizeValue}} {{VDI.sizeUnit}}
td(ng-show="disksForm.$visible")
span(
editable-select="VDI.sizeUnit"
e-ng-options="unit for unit in units"
e-name="{{VDI.id}}/sizeUnit"
)
td.oneliner {{((VDI.$VBDs[0] | resolve).VM | resolve).name_label}}
span.pull-right
.btn-group.quick-buttons
a(ng-if="(VDI.$VBDs[0] | resolve).attached", xo-click="disconnectVBD(VDI.$VBDs[0])")
i.fa.fa-unlink.fa-lg(tooltip="Disconnect this disk")
a(ng-if="!(VDI.$VBDs[0] | resolve).attached", xo-click="deleteVDI(VDI.id)")
i.fa.fa-trash-o.fa-lg(tooltip="Destroy this disk")
input(ng-if = '!(VDI.$VBDs[0] | resolve).attached', type = 'checkbox', ng-model = 'selectedForDelete[VDI.id]', tooltip = 'select for deletion')
//- TODO: Ability to create new VDIs.
.center(ng-if = '(SR.VDIs | resolve).length > 10')
pagination(boundary-links="true", total-items="(SR.VDIs | resolve).length", ng-model="$parent.currentVDIPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.form-inline
.input-group
.input-group-addon: i.fa.fa-filter
input.form-control(type = 'text', ng-model = 'vdiSearch', placeholder = 'Enter your search here')
.center(ng-if = '(VDIs | vdiFilter:vdiSearch).length > 10 || currentVDIPage > 1')
pagination(boundary-links="true", total-items="(VDIs | vdiFilter:vdiSearch).length", ng-model="$parent.currentVDIPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.btn-form(ng-show="disksForm.$visible")
p.center
button.btn.btn-default(
@@ -171,7 +204,7 @@
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-link(style="color: #e25440;")
i.fa.fa-link
| Connected hosts
span.quick-edit(tooltip="Reconnect all hosts", ng-click="reconnectAllHosts()")
i.fa.fa-plus-square.fa-fw
@@ -185,7 +218,7 @@
span.label.label-success Connected
span.pull-right.btn-group.quick-buttons
a(xo-click="disconnectPBD(PBD.id)")
i.fa.fa-unlink.fa-lg(tooltip="Disconnect to this host")
i.fa.fa-unlink.fa-lg(tooltip="Disconnect from this host")
td(ng-if="!PBD.attached")
span.label.label-default Disconnected
span.pull-right.btn-group.quick-buttons
@@ -193,7 +226,7 @@
i.fa.fa-link.fa-lg(tooltip="Reconnect to this host")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-comments(style="color: #e25440;")
i.fa.fa-comments
| Logs
.panel-body
p.center(ng-if="SR.messages | isEmpty") No recent logs
@@ -206,6 +239,6 @@
| {{message.name}}
a.quick-remove(tooltip="Remove log")
i.fa.fa-trash-o.fa-fw
.center(ng-if = '(SR.messages | count) > 5')
.center(ng-if = '(SR.messages | count) > 5 || currentLogPage > 1')
pagination(boundary-links="true", total-items="SR.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
//- /Hosts.

View File

@@ -0,0 +1,41 @@
import angular from 'angular'
import later from 'later'
import scheduler from 'scheduler'
import uiRouter from 'angular-ui-router'
later.date.localTime()
import job from './job'
import overview from './overview'
import schedule from './schedule'
import view from './view'
export default angular.module('taskScheduler', [
uiRouter,
scheduler,
job,
overview,
schedule
])
.config(function ($stateProvider) {
$stateProvider.state('taskscheduler', {
abstract: true,
data: {
requireAdmin: true
},
template: view,
url: '/taskscheduler'
})
// Redirect to default sub-state.
$stateProvider.state('taskscheduler.index', {
url: '',
controller: function ($state) {
$state.go('taskscheduler.overview')
}
})
})
.name

View File

@@ -0,0 +1,19 @@
.form-group(ng-if = 'ctrl.active()')
label.col-md-2.control-label(ng-if = 'ctrl.key')
| {{ ctrl.key }}
span.text-warning(ng-if = 'ctrl.isRequired()') &nbsp;*
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
ul(style = 'padding-left: 0;')
li.list-group-item.clearfix(ng-repeat = 'item in ctrl.model track by $index')
| {{item}}
a.pull-right(ng-click = 'ctrl.remove($index)'): i.fa.fa-times
form(ng-submit = 'ctrl.add(ctrl.newItem); ctrl.newItem = ""')
.input-group
input.form-control.input-sm(ng-if = 'ctrl.getType(ctrl.property.items) === "string"', type = 'text', ng-model = 'ctrl.newItem', ng-required = '!param.optional')
input.form-control.input-sm(ng-if = 'ctrl.getType(ctrl.property.items) === "integer"', type = 'number', step = '1', ng-model = 'ctrl.newItem', ng-required = '!param.optional')
input.form-control.input-sm(ng-if = 'ctrl.getType(ctrl.property.items) === "number"', type = 'number', step = 'any', ng-model = 'ctrl.newItem', ng-required = '!param.optional')
span.input-group-addon(ng-if = 'ctrl.getType(ctrl.property.items) === "boolean"')
input(type = 'checkbox', ng-model = 'ctrl.newItem')
input.form-control.input-sm(ng-if = 'ctrl.getType(ctrl.property.items) === "boolean"', disabled)
span.input-group-btn
button.btn.btn-primary.btn-sm(type = 'submit') Add

View File

@@ -0,0 +1,6 @@
.form-group(ng-if = 'ctrl.active()')
label.col-md-2.control-label(ng-if = 'ctrl.key')
| {{ ctrl.key }}
span.text-warning(ng-if = 'ctrl.isRequired()') &nbsp;*
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
input(form = '{{ ctrl.form }}', type = 'checkbox', ng-model = 'ctrl.model')

View File

@@ -0,0 +1,15 @@
.form-group(ng-if = 'ctrl.active()')
label.col-md-2.control-label(ng-if = 'ctrl.key')
| Hosts
span.text-warning(ng-if = 'ctrl.isRequired()') &nbsp;*
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
ui-select(form = 'ctrl.form', ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
ui-select-match(placeholder = 'Choose hosts')
i(class = 'xo-icon-{{$item.type | lowercase}}')
| {{$item.name_label}}
span(ng-if = '$item.$container') &nbsp;({{ ($item.$container | resolve).name_label }})
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:{type: "host"} | filter:$select.search | orderBy:["$container", "name_label"] track by object.id')
div
i(class = 'xo-icon-{{object.type | lowercase}}')
| &nbsp;{{ object.name_label }}
span(ng-if = 'object.$container') &nbsp;({{ (object.$container | resolve).name_label || ((object.$container | resolve).master | resolve).name_label }})

View File

@@ -0,0 +1,827 @@
import angular from 'angular'
import assign from 'lodash.assign'
import cloneDeep from 'lodash.clonedeep'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import includes from 'lodash.includes'
import map from 'lodash.map'
import mapValues from 'lodash.mapvalues'
import remove from 'lodash.remove'
import trim from 'lodash.trim'
import uiRouter from 'angular-ui-router'
import uiBootstrap from 'angular-ui-bootstrap'
import Bluebird from 'bluebird'
Bluebird.longStackTraces()
import arrayInputView from './array-input-view'
import booleanInputView from './boolean-input-view'
import hostInputView from './host-input-view'
import integerInputView from './integer-input-view'
import numberInputView from './number-input-view'
import objectInputView from './object-input-view'
import poolInputView from './pool-input-view'
import srInputView from './sr-input-view'
import stringInputView from './string-input-view'
import view from './view'
import vmInputView from './vm-input-view'
import xoEntityInputView from './xo-entity-input-view'
import xoObjectInputView from './xo-object-input-view'
import xoRoleInputView from './xo-role-input-view'
// ====================================================================
const JOB_KEY = 'genericTask'
const jobCompliantMethods = [
'acl.add',
'acl.remove',
'host.detach',
'host.disable',
'host.enable',
'host.installAllPatches',
'host.restart',
'host.restartAgent',
'host.set',
'host.start',
'host.stop',
'job.runSequence',
'vm.attachDisk',
'vm.backup',
'vm.clone',
'vm.convert',
'vm.copy',
'vm.creatInterface',
'vm.delete',
'vm.migrate',
'vm.migrate',
'vm.restart',
'vm.resume',
'vm.revert',
'vm.rollingBackup',
'vm.rollingDrCopy',
'vm.rollingSnapshot',
'vm.set',
'vm.setBootOrder',
'vm.snapshot',
'vm.start',
'vm.stop',
'vm.suspend'
]
const getType = function (param) {
if (!param) {
return
}
if (Array.isArray(param.type)) {
if (includes(param.type, 'integer')) {
return 'integer'
} else if (includes(param.type, 'number')) {
return 'number'
} else {
return 'string'
}
}
return param.type
}
const isRequired = function (param) {
if (!param) {
return
}
return (!param.optional && !(includes(['boolean', 'array'], getType(param))))
}
/**
* Takes care of unfilled not-required data and unwanted white-spaces
*/
const cleanUpData = function (data) {
if (!data) {
return
}
function sanitizeItem (item) {
if (typeof item === 'string') {
item = trim(item)
}
return item
}
function keepItem (item) {
if ((item === undefined) || (item === null) || (item === '') || (Array.isArray(item) && item.length === 0) || item.__use === false) {
return false
} else {
return true
}
}
forEach(data, (item, key) => {
item = sanitizeItem(item)
data[key] = item
if (!keepItem(item)) {
delete data[key]
} else if (typeof item === 'object') {
cleanUpData(item)
}
})
delete data.__use
return data
}
/**
* Tries extracting XO Object targeted property
*/
const reduceXoObject = function (value, propertyName = 'id') {
return value && value[propertyName] || value
}
/**
* Adapts all data "arrayed" by UI-multiple-selectors to job's cross-product trick
*/
const dataToParamVectorItems = function (params, data) {
const items = []
forEach(params, (param, name) => {
if (Array.isArray(data[name]) && getType(param) !== 'array') {
const values = []
if (data[name].length === 1) { // One value, no need to engage cross-product
data[name] = data[name].pop()
} else {
forEach(data[name], value => {
values.push({[name]: reduceXoObject(value, name)})
})
if (values.length) { // No values at all
items.push({
type: 'set',
values
})
}
delete data[name]
}
}
})
if (Object.keys(data).length) {
items.push({
type: 'set',
values: [mapValues(data, reduceXoObject)]
})
}
return items
}
const actionGroup = {
group: undefined,
get: function () {
return this.group
},
set: function (group) {
this.group = group
}
}
const _initXoObjectInput = function () {
if (this.model === undefined) {
this.model = []
}
if (!Array.isArray(this.model)) {
this.model = [this.model]
}
this.intraModel = map(this.model, value => find(this.objects, object => object.id === value) || value)
}
const _exportRemove = function (removedItem) {
remove(this.model, item => item === reduceXoObject(removedItem))
}
const _exportSelect = function (addedItem) {
const addOn = reduceXoObject(addedItem)
if (!find(this.model, item => item === addOn)) {
this.model.push(addOn)
}
}
export default angular.module('xoWebApp.taskscheduler.job', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('taskscheduler.job', {
url: '/job/:id',
controller: 'JobCtrl as ctrl',
template: view
})
})
.controller('JobCtrl', function ($scope, xo, xoApi, notify, $stateParams) {
this.scheduleApi = {}
this.formData = {}
this.running = {}
this.ready = false
let comesForEditing = $stateParams.id
this.resetData = () => {
this.formData = {}
}
this.resetForm = () => {
this.resetData()
this.editedJobId = undefined
this.jobName = undefined
this.selectedAction = undefined
}
this.resetForm()
$scope.$watch(() => this.selectedAction, newAction => actionGroup.set(newAction && newAction.group))
const loadActions = () => xoApi.call('system.getMethodsInfo')
.then(response => {
const actions = []
for (let method in response) {
if (includes(jobCompliantMethods, method)) {
let [group, command] = method.split('.')
response[method].properties = response[method].params
response[method].type = 'object'
delete response[method].params
actions.push({
method,
group,
command,
info: response[method]
})
}
}
this.actions = actions
this.ready = true
})
const loadJobs = () => xo.job.getAll().then(jobs => {
const j = {}
forEach(jobs, job => {
if (job.key === JOB_KEY) {
j[job.id] = job
}
})
this.jobs = j
})
const refresh = () => loadJobs()
const getReady = () => loadActions().then(refresh).then(() => this.ready = true)
getReady().then(() => {
if (comesForEditing) {
this.edit(comesForEditing)
comesForEditing = undefined
}
})
const saveNew = (name, action, data) => {
const job = {
type: 'call',
name,
key: JOB_KEY,
method: action.method,
paramsVector: {
type: 'crossProduct',
items: dataToParamVectorItems(action.info.properties, data)
}
}
return xo.job.create(job)
}
const save = (id, name, action, data) => {
const job = this.jobs[id]
job.name = name
job.method = action.method
job.paramsVector = {
type: 'crossProduct',
items: dataToParamVectorItems(action.info.properties, data)
}
return xo.job.set(job)
}
this.save = (id, name, action, data) => {
const dataClone = cleanUpData(cloneDeep(data))
const saved = (id !== undefined) ? save(id, name, action, dataClone) : saveNew(name, action, dataClone)
return saved
.then(() => this.resetForm())
.finally(refresh)
}
this.edit = id => {
this.resetForm()
try {
const job = this.jobs[id]
if (job) {
this.editedJobId = id
this.jobName = job.name
this.selectedAction = find(this.actions, action => action.method === job.method)
const data = {}
const paramsVector = job.paramsVector
if (paramsVector) {
if (paramsVector.type !== 'crossProduct') {
throw new Error(`Unknown parameter-vector type ${paramsVector.type}`)
}
forEach(paramsVector.items, item => {
if (item.type !== 'set') {
throw new Error(`Unknown parameter-vector item type ${item.type}`)
}
if (item.values.length === 1) {
assign(data, item.values[0])
} else {
forEach(item.values, valueItem => {
forEach(valueItem, (value, key) => {
if (data[key] === undefined) {
data[key] = []
}
data[key].push(value)
})
})
}
})
}
this.formData = data
}
} catch (error) {
this.resetForm()
notify.error({
title: 'Unhandled Job',
message: error.message
})
}
}
this.delete = id => xo.job.delete(id).then(refresh).then(() => {
if (id === this.editedJobId) {
this.resetForm()
}
})
this.run = id => {
this.running[id] = true
notify.info({
title: 'Run Job',
message: 'One shot running started. See overview for logs.'
})
return xo.job.runSequence([id]).finally(() => delete this.running[id])
}
})
.directive('stringInput', function () {
return {
restrict: 'E',
scope: {
model: '=',
form: '=',
key: '=',
property: '='
},
bindToController: true,
controller: function () {
this.isRequired = () => isRequired(this.property)
this.active = () => getType(this.property) === 'string' && !includes(['id', 'host', 'host_id', 'target_host_id', 'sr', 'target_sr_id', 'vm', 'pool', 'subject', 'object', 'action'], this.key)
},
controllerAs: 'ctrl',
template: stringInputView
}
})
.directive('booleanInput', function () {
return {
restrict: 'E',
scope: {
model: '=',
form: '=',
key: '=',
property: '='
},
bindToController: true,
controller: function () {
this.isRequired = () => isRequired(this.property)
this.active = () => getType(this.property) === 'boolean'
},
controllerAs: 'ctrl',
template: booleanInputView
}
})
.directive('integerInput', function () {
return {
restrict: 'E',
scope: {
model: '=',
form: '=',
key: '=',
property: '='
},
bindToController: true,
controller: function () {
this.isRequired = () => isRequired(this.property)
this.active = () => getType(this.property) === 'integer'
},
controllerAs: 'ctrl',
template: integerInputView
}
})
.directive('numberInput', function () {
return {
restrict: 'E',
scope: {
model: '=',
form: '=',
key: '=',
property: '='
},
bindToController: true,
controller: function () {
this.isRequired = () => isRequired(this.property)
this.active = () => getType(this.property) === 'number'
},
controllerAs: 'ctrl',
template: numberInputView
}
})
.directive('arrayInput', function ($compile) {
return {
restrict: 'E',
scope: {
model: '=',
form: '=',
key: '=',
property: '='
},
controller: 'ArrayInput as ctrl',
bindToController: true,
link: function (scope, element, attrs) {
const updateElement = () => {
if (scope.ctrl.property.items) {
element.append(arrayInputView)
}
$compile(element.contents())(scope)
}
updateElement()
}
}
})
.controller('ArrayInput', function ($scope) {
this.isRequired = () => false
this.getType = getType
this.active = () => getType(this.property) === 'array'
this.add = value => {
const type = getType(this.property.items)
switch (type) {
case 'boolean':
value = Boolean(value)
break
case 'string':
value = trim(value)
break
}
this.model.push(value)
}
this.remove = index => this.model.splice(index, 1)
const init = () => {
if (this.model === undefined || this.model === null) {
this.model = []
}
}
if (this.active()) {
init()
if (!Array.isArray(this.model)) {
throw new Error('arrayInput directive model must be an array')
}
$scope.$watch(() => this.model, init)
}
})
.directive('objectInput', function ($compile) {
return {
restrict: 'E',
scope: {
model: '=',
form: '=',
key: '=',
property: '='
},
controller: 'ObjectInput as ctrl',
bindToController: true,
link: function (scope, element, attrs) {
const updateElement = () => {
if (scope.ctrl.property.properties) {
element.append(objectInputView)
}
$compile(element.contents())(scope)
}
updateElement()
}
}
})
.controller('ObjectInput', function ($scope) {
this.isRequired = () => isRequired(this.property)
this.active = () => getType(this.property) === 'object' && (this.key !== 'object' || actionGroup.get() !== 'acl')
const init = () => {
if (this.model === undefined || this.model === null) {
this.model = {
__use: this.isRequired()
}
}
if (typeof this.model !== 'object' || Array.isArray(this.model)) {
throw new Error('objectInput directive model must be a plain object')
}
const use = this.model.__use
delete this.model.__use
this.model.__use = Object.keys(this.model).length > 0 || use
forEach(this.property.properties, (property, key) => {
if (getType(property) === 'boolean') {
this.model[key] = Boolean(this.model[key])
}
})
}
if (this.active()) {
init()
$scope.$watch(() => this.model, (newVal, oldVal) => {
if (newVal !== oldVal) {
init()
}
})
}
})
.directive('vmInput', function () {
return {
restrict: 'E',
scope: {
form: '=',
key: '=',
property: '=',
model: '='
},
controller: 'VmInput as ctrl',
bindToController: true,
template: vmInputView
}
})
.controller('VmInput', function ($scope, xoApi) {
this.objects = xoApi.all
this.isRequired = () => isRequired(this.property)
this.active = () => getType(this.property) === 'string' && (this.key === 'vm' || (actionGroup.get() === 'vm' && this.key === 'id'))
this.init = _initXoObjectInput
this.exportRemove = _exportRemove
this.exportSelect = _exportSelect
if (this.active()) {
this.init()
$scope.$watch(() => this.model, (newVal, oldVal) => {
if (newVal !== oldVal) {
this.init()
}
})
}
})
.directive('hostInput', function () {
return {
restrict: 'E',
scope: {
form: '=',
key: '=',
property: '=',
model: '='
},
controller: 'HostInput as ctrl',
bindToController: true,
template: hostInputView
}
})
.controller('HostInput', function ($scope, xoApi) {
this.objects = xoApi.all
this.isRequired = () => isRequired(this.property)
this.active = () => getType(this.property) === 'string' && (includes(['host', 'host_id', 'target_host_id'], this.key) || (actionGroup.get() === 'host' && this.key === 'id'))
this.init = _initXoObjectInput
this.exportRemove = _exportRemove
this.exportSelect = _exportSelect
if (this.active()) {
this.init()
$scope.$watch(() => this.model, (newVal, oldVal) => {
if (newVal !== oldVal) {
this.init()
}
})
}
})
.directive('srInput', function () {
return {
restrict: 'E',
scope: {
form: '=',
key: '=',
property: '=',
model: '='
},
controller: 'SrInput as ctrl',
bindToController: true,
template: srInputView
}
})
.controller('SrInput', function ($scope, xoApi) {
this.objects = xoApi.all
this.isRequired = () => isRequired(this.property)
this.active = () => getType(this.property) === 'string' && includes(['sr', 'sr_id', 'target_sr_id'], this.key)
this.init = _initXoObjectInput
this.exportRemove = _exportRemove
this.exportSelect = _exportSelect
if (this.active()) {
this.init()
$scope.$watch(() => this.model, (newVal, oldVal) => {
if (newVal !== oldVal) {
this.init()
}
})
}
})
.directive('poolInput', function () {
return {
restrict: 'E',
scope: {
form: '=',
key: '=',
property: '=',
model: '='
},
controller: 'PoolInput as ctrl',
bindToController: true,
template: poolInputView
}
})
.controller('PoolInput', function ($scope, xoApi) {
this.objects = xoApi.all
this.isRequired = () => isRequired(this.property)
this.active = () => getType(this.property) === 'string' && includes(['pool', 'pool_id', 'target_pool_id'], this.key)
this.init = _initXoObjectInput
this.exportRemove = _exportRemove
this.exportSelect = _exportSelect
if (this.active()) {
this.init()
$scope.$watch(() => this.model, (newVal, oldVal) => {
if (newVal !== oldVal) {
this.init()
}
})
}
})
.directive('xoEntityInput', function () {
return {
restrict: 'E',
scope: {
form: '=',
key: '=',
property: '=',
model: '='
},
controller: 'XoEntityInput as ctrl',
bindToController: true,
template: xoEntityInputView
}
})
.controller('XoEntityInput', function ($scope, xo) {
this.ready = false
this.isRequired = () => isRequired(this.property)
this.active = () => this.ready && getType(this.property) === 'string' && this.key === 'subject' && actionGroup.get() === 'acl'
this.init = _initXoObjectInput
this.exportRemove = _exportRemove
this.exportSelect = _exportSelect
Bluebird.props({
users: xo.user.getAll(),
groups: xo.group.getAll()
})
.then(p => {
this.objects = p.users.concat(p.groups)
this.ready = true
if (this.active()) {
this.init()
$scope.$watch(() => this.model, (newVal, oldVal) => {
if (newVal !== oldVal) {
this.init()
}
})
}
})
})
.directive('xoRoleInput', function () {
return {
restrict: 'E',
scope: {
form: '=',
key: '=',
property: '=',
model: '='
},
controller: 'XoRoleInput as ctrl',
bindToController: true,
template: xoRoleInputView
}
})
.controller('XoRoleInput', function ($scope, xo) {
this.ready = false
this.isRequired = () => isRequired(this.property)
this.active = () => this.ready && getType(this.property) === 'string' && this.key === 'action' && actionGroup.get() === 'acl'
this.init = _initXoObjectInput
this.exportRemove = _exportRemove
this.exportSelect = _exportSelect
xo.role.getAll()
.then(roles => {
this.objects = roles
this.ready = true
if (this.active()) {
this.init()
$scope.$watch(() => this.model, (newVal, oldVal) => {
if (newVal !== oldVal) {
this.init()
}
})
}
})
})
.directive('xoObjectInput', function () {
return {
restrict: 'E',
scope: {
form: '=',
key: '=',
property: '=',
model: '='
},
controller: 'XoObjectInput as ctrl',
bindToController: true,
template: xoObjectInputView
}
})
.controller('XoObjectInput', function ($scope, xoApi, filterFilter, selectHighLevelFilter) {
const HIGH_LEVEL_OBJECTS = {
pool: true,
host: true,
VM: true,
SR: true,
network: true
}
this.types = Object.keys(HIGH_LEVEL_OBJECTS)
this.objects = xoApi.all
this.isRequired = () => isRequired(this.property)
this.active = () => getType(this.property) === 'string' && this.key === 'object' && actionGroup.get() === 'acl'
this.toggleType = (toggle, type) => {
const selectedObjects = this.intraModel && this.intraModel.slice() || []
if (toggle) {
const objects = filterFilter(selectHighLevelFilter(this.objects), {type})
forEach(objects, object => { selectedObjects.indexOf(object) === -1 && selectedObjects.push(object) })
this.intraModel = selectedObjects
} else {
const keptObjects = []
for (let index in selectedObjects) {
const object = selectedObjects[index]
if (object.type !== type) {
keptObjects.push(object)
}
}
this.intraModel = keptObjects
}
this.model.length = 0
forEach(this.intraModel, item => this.model.push(reduceXoObject(item)))
}
this.init = _initXoObjectInput
this.exportRemove = _exportRemove
this.exportSelect = _exportSelect
if (this.active()) {
this.init()
$scope.$watch(() => this.model, (newVal, oldVal) => {
if (newVal !== oldVal) {
this.init()
}
})
}
})
// A module exports its name.
.name

View File

@@ -0,0 +1,6 @@
.form-group(ng-if = 'ctrl.active()')
label.col-md-2.control-label(ng-if = 'ctrl.key')
| {{ ctrl.key }}
span.text-warning(ng-if = 'ctrl.isRequired()') &nbsp;*
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
input.form-control(form = '{{ ctrl.form }}', type = 'number', step = '1', ng-model = 'ctrl.model', ng-required = 'ctrl.isRequired()')

View File

@@ -0,0 +1,6 @@
.form-group(ng-if = 'ctrl.active()')
label.col-md-2.control-label(ng-if = 'ctrl.key')
| {{ ctrl.key }}
span.text-warning(ng-if = 'ctrl.isRequired()') &nbsp;*
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
input.form-control(form = '{{ ctrl.form }}', type = 'number', step = 'any', ng-model = 'ctrl.model', ng-required = 'ctrl.isRequired()')

View File

@@ -0,0 +1,30 @@
.form-group(ng-if = 'ctrl.active()')
label.col-md-2.control-label(ng-if = 'ctrl.key')
| {{ ctrl.key }}
span.text-warning(ng-if = 'ctrl.isRequired()') &nbsp;*
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
.checkbox(ng-if = '!ctrl.isRequired()')
label
input(type = 'checkbox', ng-model = 'ctrl.model.__use')
| &nbsp;Fill informations (optional)
hr
.help-block(ng-if = 'ctrl.isRequired()')
span.text-warning Fill required informations
hr
fieldset.form-horizontal(ng-disabled = '!ctrl.isRequired() && !ctrl.model.__use', ng-hide = '!ctrl.isRequired() && !ctrl.model.__use')
.form-group(ng-if = '(ctrl.property.properties | count) < 1')
p.help-block No parameters
div(ng-repeat = '(key, property) in ctrl.property.properties')
array-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
boolean-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
host-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
integer-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
number-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
object-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
pool-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
sr-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
string-input(form = 'ctrl.form', model = 'ctrl.model[key]', key = 'key', property = 'property')
vm-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
xo-entity-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
xo-object-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')
xo-role-input(form = 'ctrl.form', key = 'key', property = 'property', model = 'ctrl.model[key]')

View File

@@ -0,0 +1,15 @@
.form-group(ng-if = 'ctrl.active()')
label.col-md-2.control-label(ng-if = 'ctrl.key')
| Pools
span.text-warning(ng-if = 'ctrl.isRequired()') &nbsp;*
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
ui-select(form = 'ctrl.form', ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
ui-select-match(placeholder = 'Choose pools')
i(class = 'xo-icon-{{$item.type | lowercase}}')
| {{$item.name_label}}
span(ng-if = '$item.$container') &nbsp;({{ ($item.$container | resolve).name_label }})
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:{type: "pool"} | filter:$select.search | orderBy:["$container", "name_label"] track by object.id')
div
i(class = 'xo-icon-{{object.type | lowercase}}')
| &nbsp;{{ object.name_label }}
span(ng-if = 'object.$container') &nbsp;({{ (object.$container | resolve).name_label || ((object.$container | resolve).master | resolve).name_label }})

View File

@@ -0,0 +1,15 @@
.form-group(ng-if = 'ctrl.active()')
label.col-md-2.control-label(ng-if = 'ctrl.key')
| SRs
span.text-warning(ng-if = 'ctrl.isRequired()') &nbsp;*
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
ui-select(form = 'ctrl.form', ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
ui-select-match(placeholder = 'Choose Storage Repositories')
i(class = 'xo-icon-{{$item.type | lowercase}}')
| {{$item.name_label}}
span(ng-if = '$item.$container') &nbsp;({{ ($item.$container | resolve).name_label }})
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:{type: "sr"} | filter:$select.search | orderBy:["$container", "name_label"] track by object.id')
div
i(class = 'xo-icon-{{object.type | lowercase}}')
| &nbsp;{{ object.name_label }}
span(ng-if = 'object.$container') &nbsp;({{ (object.$container | resolve).name_label || ((object.$container | resolve).master | resolve).name_label }})

View File

@@ -0,0 +1,6 @@
.form-group(ng-if = 'ctrl.active()')
label.col-md-2.control-label(ng-if = 'ctrl.key')
| {{ ctrl.key }}
span.text-warning(ng-if = 'ctrl.isRequired()') &nbsp;*
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
input.form-control(form = '{{ ctrl.form }}', type = 'text', ng-model = 'ctrl.model', ng-required = 'ctrl.isRequired()')

View File

@@ -0,0 +1,68 @@
.grid
.panel.panel-default
p.page-title
i.fa.fa-cogs
| Jobs
form#jobform(ng-submit = 'ctrl.save(ctrl.editedJobId, ctrl.jobName, ctrl.selectedAction, ctrl.formData)')
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-wrench
| {{ ctrl.editedJobId ? "Edit" : "Create" }}
.panel-body
.alert.alert-warning(ng-if = 'ctrl.editedJobId') Editing Job ID: {{ ctrl.editedJobId }}
fieldset.form-horizontal(ng-disabled = '!ctrl.ready')
.form-group
label.col-sm-2.control-label Job Name
.col-sm-10
input.form-control(form = 'jobform', type = 'text', ng-model = 'ctrl.jobName', required, placeholder = 'An explicit name for your job')
.form-group
label.col-sm-2.control-label {{ ctrl.selectedAction ? (ctrl.selectedAction.group + ".") : "Action" }}
.col-sm-10
select.form-control(form = 'jobform', ng-model = 'ctrl.selectedAction', ng-options = 'action.command group by action.group for action in ctrl.actions', ng-change = 'ctrl.resetData()', required)
option(value = '') -- Choose an action --
p.help-block(ng-if = 'ctrl.selectedAction.info.description') {{ ctrl.selectedAction.info.description }}
.form-group(ng-if = 'ctrl.selectedAction.info.permission')
label.col-sm-2.control-label Permission
.col-sm-10: p.form-control-static {{ ctrl.selectedAction.info.permission }}
fieldset.form-horizontal(ng-if = 'ctrl.selectedAction', ng-disabled = '!ctrl.ready')
legend Parameters
object-input(form = '"jobform"', property = 'ctrl.selectedAction.info', model = 'ctrl.formData')
.grid-sm
.panel.panel-default
.panel-body
fieldset.center(ng-disabled = '!ctrl.ready')
button.btn.btn-lg.btn-primary(form = 'jobform', type = 'submit')
i.fa.fa-clock-o
| &nbsp;
i.fa.fa-arrow-right
| &nbsp;
i.fa.fa-database
| &nbsp;Save&nbsp;
| &nbsp;
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetForm()')
| &nbsp;Reset&nbsp;
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-list-ul
| Jobs
.panel-body
.text-center(ng-if = '!(ctrl.jobs | count)') No jobs found
table.table(ng-if = 'ctrl.jobs | count')
tr
th Name
th Action
th
tr(ng-repeat = 'job in ctrl.jobs | map | orderBy:"name" track by job.id')
td
| {{ job.name }}&ensp;
span.text-muted.hidden-xs ({{ job.id }})
td {{ job.method }}
td
span.pull-left
button.btn.btn-warning(type = 'button', ng-click = 'ctrl.run(job.id)', ng-disabled = 'ctrl.running[job.id]'): i.fa.fa-play
span.pull-right
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(job.id)'): i.fa.fa-pencil
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(job.id)'): i.fa.fa-trash

View File

@@ -0,0 +1,17 @@
.form-group(ng-if = 'ctrl.active()')
label.col-md-2.control-label(ng-if = 'ctrl.key')
| VMs
span.text-warning(ng-if = 'ctrl.isRequired()') &nbsp;*
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
ui-select(form = 'ctrl.form', ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
ui-select-match(placeholder = 'Choose VMs')
i.xo-icon-working(ng-if = 'isVMWorking($item)')
i(ng-class = '"xo-icon-" + ($item.power_state | lowercase)', ng-if = '!isVMWorking($item)')
| &nbsp;{{ $item.name_label }}
span(ng-if = '$item.$container') &nbsp;({{ ($item.$container | resolve).name_label }})
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "vm"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
div
i.xo-icon-working(ng-if = 'isVMWorking(vm)', tooltip = '{{ vm.power_state }} and {{ (vm.current_operations | map)[0] }}')
i(ng-class = '"xo-icon-" + (vm.power_state | lowercase)', ng-if = '!isVMWorking(vm)', tooltip = '{{ vm.power_state }}')
| &nbsp;{{ vm.name_label }}
span(ng-if = 'vm.$container') &nbsp;({{ (vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label }})

View File

@@ -0,0 +1,21 @@
.form-group(ng-if = 'ctrl.active()')
label.col-md-2.control-label(ng-if = 'ctrl.key')
| Users / Groups
span.text-warning(ng-if = 'ctrl.isRequired()') &nbsp;*
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
ui-select(ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
ui-select-match(placeholder = 'Choose users or groups')
span(ng-if = '$item.email')
i.xo-icon-user.fa-fw
| {{$item.email}}
span(ng-if = '$item.name')
i.xo-icon-group.fa-fw
| {{$item.name}}
ui-select-choices(repeat = 'entity in ctrl.objects | filter:{ permission: "!admin" } | filter:$select.search')
div
span(ng-if = 'entity.email')
i.xo-icon-user.fa-fw
| {{entity.email}}
span(ng-if = 'entity.name')
i.xo-icon-group.fa-fw
| {{entity.name}}

View File

@@ -0,0 +1,28 @@
.form-group(ng-if = 'ctrl.active()')
label.col-md-2.control-label(ng-if = 'ctrl.key')
| Objects
span.text-warning(ng-if = 'ctrl.isRequired()') &nbsp;*
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
ui-select(ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
ui-select-match(placeholder = 'Choose an object')
i(class = 'xo-icon-{{$item.type | lowercase}}')
| {{$item.name_label}}
span(ng-if="($item.type === 'SR' || $item.type === 'VM') && $item.$container")
| ({{($item.$container | resolve).name_label}})
span(ng-if="$item.type === 'network'")
| ({{($item.$poolId | resolve).name_label}})
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:$select.search | orderBy:["type", "name_label"]')
div
i(class = 'xo-icon-{{object.type | lowercase}}')
| {{object.name_label}}
span(ng-if="(object.type === 'SR' || object.type === 'VM') && object.$container")
| ({{(object.$container | resolve).name_label}})
span(ng-if="object.type === 'network'")
| ({{(object.$poolId | resolve).name_label}})
.text-center
span(ng-repeat = 'type in ctrl.types')
label(tooltip = 'select/deselect all {{type}}s', style = 'cursor: pointer')
input.hidden(type = 'checkbox', ng-model = 'selectedTypes[type]', ng-change = 'ctrl.toggleType(selectedTypes[type], type)')
span.fa-stack
i(class = 'xo-icon-{{type | lowercase}}').fa-stack-1x
i.fa.fa-square-o.fa-stack-2x.text-info(ng-if = 'selectedTypes[type]')

View File

@@ -0,0 +1,13 @@
.form-group(ng-if = 'ctrl.active()')
label.col-md-2.control-label(ng-if = 'ctrl.key')
| Roles
span.text-warning(ng-if = 'ctrl.isRequired()') &nbsp;*
div(ng-class = '{"col-md-10": ctrl.key, "col-md-12": !ctrl.key}')
ui-select(ng-model = 'ctrl.intraModel', multiple, close-on-select = 'false', ng-required = 'ctrl.isRequired()', on-remove = 'ctrl.exportRemove($item)', on-select = 'ctrl.exportSelect($item)')
ui-select-match(placeholder = 'Choose a role')
i(class = 'xo-icon-{{$item.type | lowercase}}')
| {{$item.name}}
ui-select-choices(repeat = 'role in ctrl.objects | filter:$select.search | orderBy:"name"')
div
i(class = 'xo-icon-{{role.type | lowercase}}')
| {{role.name}}

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