Compare commits

..

1055 Commits

Author SHA1 Message Date
Julien Fontanet
ffd95261c3 4.16.0 2016-04-29 12:23:59 +02:00
Olivier Lambert
82f38040c1 changelog 2016-04-29 12:22:14 +02:00
Olivier Lambert
0eadfd5a58 Merge pull request #835 from vatesfr/abhamonr-fix-plugin-multiple-users-groups-loading
Avoid multiple users/groups loading in plugins view. (fix vatesfr/xo-…
2016-03-23 12:11:44 +01:00
wescoeur
eea34a4f6c Avoid multiple users/groups loading in plugins view. (fix vatesfr/xo-web#829) 2016-03-23 12:03:56 +01:00
Julien Fontanet
ca525bd08c 4.15.1 2016-03-22 15:28:15 +01:00
Olivier Lambert
ac2ffc4586 Fix #830 2016-03-22 14:35:57 +01:00
Olivier Lambert
5781269557 Remove old message about not supported SMB remote for delta 2016-03-22 13:32:19 +01:00
Olivier Lambert
e4422b9fe7 Display only permitted SR for VM copy 2016-03-22 12:14:25 +01:00
Olivier Lambert
269f76d546 Fix #832 2016-03-22 11:47:34 +01:00
Julien Fontanet
540e3f0aaa 4.15.0 2016-03-21 16:50:22 +01:00
Olivier Lambert
5f64ae28e0 Merge pull request #800 from vatesfr/abhamonr-delta-backup-on-smb
SMB can be used with delta backups.
2016-03-21 09:37:40 +01:00
Olivier Lambert
f669f64fcb add changelog 2016-03-18 16:17:36 +01:00
Julien Fontanet
be2db2dd8e Fix immediately spelling (thx @Danp2). 2016-03-18 10:02:30 +01:00
Olivier Lambert
9ccd3438ad Fix #821 2016-03-17 19:15:35 +01:00
Olivier Lambert
c6a0874b3b Merge pull request #758 from vatesfr/marsaudf-backup-ui-fixes
Fixed backup remote lists
2016-03-17 14:23:56 +01:00
Olivier Lambert
9c80470185 Merge pull request #820 from vatesfr/pierre-server-version
Added server version in About view (See #807)
2016-03-17 14:21:56 +01:00
Pierre
fd8da5ffba Added server version in About view 2016-03-17 10:52:32 +01:00
Olivier Lambert
e987af87f6 Merge pull request #809 from vatesfr/abhamonr-recursive-plugins-config
Complex configurations plugins. (recursion, array of objects...)
2016-03-17 10:31:31 +01:00
Pierre
0074cc3933 Fixed refresh bug. 2016-03-17 09:51:53 +01:00
Pierre
5f2ce89316 Fixed Object/String/Array display in plugin config. 2016-03-17 09:51:53 +01:00
Pierre
60492c48a6 Typo fix. 2016-03-17 09:51:53 +01:00
Pierre
eed2d70017 Better array display when items are objects 2016-03-17 09:51:53 +01:00
Pierre
b859adaa8c Fixes 2016-03-17 09:51:53 +01:00
Pierre
89a587f9ae enum handling 2016-03-17 09:51:53 +01:00
wescoeur
fb56bcff80 Complex configurations plugins. (recursion, array of objects...) 2016-03-17 09:51:53 +01:00
Fabrice Marsaud
99eb6907dd updater will block nav after 1 min out 2016-03-16 16:32:52 +01:00
Fabrice Marsaud
3743fad899 when updater-blocked, any nav attempt will retry connection 2016-03-16 16:18:02 +01:00
Olivier Lambert
c1e59a7e03 Merge pull request #818 from vatesfr/pierre-feedback-when-error-on-sr
Feedback when disconnecting a host from an SR does not work (See #810)
2016-03-16 15:54:11 +01:00
Pierre
b34dee1f83 Error message formatting: leading capital and trailing period. 2016-03-16 15:45:36 +01:00
Pierre
6edd65ad8f xo.pbd.disconnect instead of xoApi.call(...). Better error notification. 2016-03-16 15:01:29 +01:00
Olivier Lambert
0959ca6a40 Merge pull request #813 from vatesfr/pierre-network-management
Host & pool views: better network management (See #805)
2016-03-16 10:57:34 +01:00
Pierre
1287fa2cd0 Allowed network creation without PIF 2016-03-16 10:50:47 +01:00
Pierre
a5a07f250d UI improvements 2016-03-16 10:46:08 +01:00
Pierre
089fb526f5 IP configuration: DHCP & No IP. UI fixes. 2016-03-16 10:46:08 +01:00
Pierre
af58b7593a Configure PIF IP. UI fixes. 2016-03-16 10:46:07 +01:00
Pierre
d4508b25ce Only physical PIFs should be shown when creating network 2016-03-16 10:46:07 +01:00
Pierre
9edc218eaa Delete network 2016-03-16 10:46:07 +01:00
Olivier Lambert
3790f753aa Merge pull request #801 from vatesfr/pierre-pool-networks
Pool view: Interface panel and network creation (See #226)
2016-03-16 10:45:06 +01:00
wescoeur
8ce3a4f904 SMB can be used with delta backups. 2016-03-09 17:12:15 +01:00
Pierre
be0b9c7e53 Removed log 2016-03-09 10:38:45 +01:00
Pierre
6d75cd9025 Minor fix 2016-03-09 10:33:09 +01:00
Pierre
345d6f369e network.create instead of createNetwork for host and pool 2016-03-09 10:33:09 +01:00
Pierre
959ea86d85 Pool view: Interface panel and network creation 2016-03-09 10:33:09 +01:00
Olivier Lambert
b67a99af3d Add types for ISO SRs 2016-03-04 13:36:35 +01:00
Olivier Lambert
fa3b848d40 Merge pull request #799 from vatesfr/pierre-add-smb-sr
UI to add an SMB SR (user and password inputs) (Fix #731)
2016-03-03 18:25:55 +01:00
Pierre
0f971e9e7d Minor fix. 2016-03-03 17:55:43 +01:00
Pierre
c17f76c009 SMB case fix. 2016-03-03 17:50:23 +01:00
Pierre
bf23b5d295 Enabled Create button and removed search button when SMB. 2016-03-03 17:39:18 +01:00
Pierre
09c7256d42 UI to add an SMB SR (user and password inputs) 2016-03-03 16:58:46 +01:00
Olivier Lambert
eaee8a2fbb Merge pull request #798 from vatesfr/pierre-new-vm-from-pool-enhancement
New VM on pool: display all SRs (Fix #790)
2016-03-03 16:41:45 +01:00
Pierre
3b18dd67be Compatibility with self service 2016-03-03 16:16:03 +01:00
Pierre
c3f87b4248 Explicit message in summary 2016-03-03 15:27:07 +01:00
Pierre
1c79edc52f Detection of incompatible SRs 2016-03-03 13:05:26 +01:00
Julien Fontanet
fe2dfd0e8f Merge branch 'stable' into next-release 2016-03-03 13:03:18 +01:00
Olivier Lambert
fa6056c1b1 Add unknown state 2016-03-03 12:26:06 +01:00
Olivier Lambert
d5762c7ad8 limit VDI form for admin users 2016-03-03 12:26:06 +01:00
Julien Fontanet
d9c9dd2a4f Welcome message in the issue template 2016-03-03 11:31:27 +01:00
Olivier Lambert
3a4d945c68 Merge pull request #793 from Danp2/next-release
Fix issue with gathering NFS shares
2016-03-02 23:41:04 +01:00
Danp2
f4a364816b Fix issue with gathering NFS shares
scsiList vs nfsList
2016-03-02 16:11:26 -06:00
Olivier Lambert
931bc03cab Inverse critical/warning SR usage in health view 2016-03-02 18:30:05 +01:00
Olivier Lambert
1abd4937cd Merge pull request #792 from vatesfr/pierre-licenses
Host and pool licenses (Fix #763)
2016-03-02 17:39:09 +01:00
Pierre
0df8b51c62 Host view and pool view: License panel. 2016-03-02 17:09:20 +01:00
Julien Fontanet
e5b7190015 Fix file uploads (2). 2016-03-02 16:24:15 +01:00
Olivier Lambert
279b8aacf6 add missing map filters 2016-03-02 15:44:23 +01:00
Olivier Lambert
9eebaab2f4 Add a map param for backup schedule view 2016-03-02 15:42:33 +01:00
Julien Fontanet
16e9d60033 Fix file uploads. 2016-03-02 15:27:30 +01:00
Olivier Lambert
335b378e9a Merge pull request #789 from vatesfr/pierre-disk-names
Default VDI names and descriptions (Fix #780)
2016-03-02 15:20:39 +01:00
Pierre
9c41bc33a3 Default names for template VDIs 2016-03-02 15:13:34 +01:00
Olivier Lambert
7f7d6b4d5d Merge pull request #772 from vatesfr/pierre-cpu-weight-in-resource-set
CPU weight edition should be disabled for resource set members (Fix #…
2016-03-02 14:56:05 +01:00
Olivier Lambert
cc0e3bbce0 Merge pull request #773 from vatesfr/pierre-host-patches-in-pool-view
Pool view: host updates panel (Fix #762)
2016-03-02 14:41:11 +01:00
Pierre
2eead65fef CPU weight should not be editable when creating a VM from a resource set 2016-03-02 14:36:46 +01:00
Pierre
e664be451f VDIs default names initialization in view 2016-03-02 12:40:44 +01:00
Pierre
d2d8160096 Default VDI names and descriptions 2016-03-02 10:28:29 +01:00
Olivier Lambert
3bd503c28d remove useless device number in VM migration window 2016-03-02 10:27:57 +01:00
Olivier Lambert
aa1df8eb33 Add Misc panel in host view to deliver system S/N etc. Fix #760 2016-03-01 17:17:36 +01:00
Olivier Lambert
c1aace45ae Match target network names for migration. Fix #782 2016-03-01 16:40:45 +01:00
Olivier Lambert
217a60aadc Improve the migration VM modal. Fix #785 2016-03-01 15:47:37 +01:00
Olivier Lambert
5654f528ca fix the task list for angular 1.5 2016-03-01 15:35:48 +01:00
Olivier Lambert
4da036a064 Add a missing angular map 2016-03-01 15:16:55 +01:00
Olivier Lambert
2256b3d262 Merge pull request #788 from Danp2/next-release
Minor fix to unitConfirms
2016-03-01 13:36:24 +01:00
Danp2
d84ecc307d Minor fix to unitConfirms 2016-03-01 06:32:30 -06:00
Julien Fontanet
237313d5fb Merge pull request #781 from vatesfr/fix-vdi-iteration-in-vm-creation
VM creation: correctly iterate over template VDIs (fix #778).
2016-02-29 18:49:33 +01:00
Olivier Lambert
7caf766bca Do not display CDs VBDs 2016-02-29 18:44:08 +01:00
Julien Fontanet
0a3f9f5ef1 Add issue template. 2016-02-29 16:37:13 +01:00
Julien Fontanet
e890b8f7c1 VM creation: correctly iterate over template VDIs (fix #778). 2016-02-29 15:14:52 +01:00
Olivier Lambert
dc4d5f0ecb fixing angular 1.5 2016-02-26 18:03:07 +01:00
Pierre
a2f0980731 Pool view: host updates panel (Fix #762) 2016-02-26 17:59:33 +01:00
Julien Fontanet
0a5c029f8b lodash.sum(): does not work with objects anymore. 2016-02-26 17:36:06 +01:00
Julien Fontanet
85bb79e4fb Move shims to app/node_modules.
Hopefully this will avoid there accidental deletion by users.
2016-02-26 15:55:05 +01:00
Julien Fontanet
f18d1e50f8 Fix ng-file-upload import. 2016-02-26 15:55:05 +01:00
Julien Fontanet
943b10dd5d Update angular-chart.js to 0.8. 2016-02-26 15:55:05 +01:00
Julien Fontanet
0a48e17c88 Update ng-angular-upload to 12.0. 2016-02-26 15:55:05 +01:00
Olivier Lambert
da1381e14e Map usage before orderBy for dashboard view 2016-02-26 15:53:14 +01:00
Pierre
bdffb0ee10 CPU weight edition should be disabled for resource set members (Fix #767) 2016-02-26 14:51:53 +01:00
Julien Fontanet
7bdb7d2ca8 Fix angular-bootstrap usage in updater. 2016-02-26 14:34:56 +01:00
Julien Fontanet
92567561b8 Update angularjs-toaster to 1.2. 2016-02-26 13:06:02 +01:00
Julien Fontanet
335bdcd89d Update angular-xeditable-npm to 0.1.9. 2016-02-26 13:05:56 +01:00
Julien Fontanet
4c2fc13abb Update ui-select to 0.14. 2016-02-26 13:05:51 +01:00
Julien Fontanet
7f8f29daa2 Update angular-ui-bootstrap to 0.14. 2016-02-26 12:43:48 +01:00
Julien Fontanet
8fac845ecb Use angular-ui-{event,indeterminate} instead of deprecated angular-ui-utils. 2016-02-26 12:34:56 +01:00
Olivier Lambert
d8076e7630 Merge pull request #756 from vatesfr/pierre-self-service-dashboard
Self service dashboard (Fix #741)
2016-02-26 11:56:27 +01:00
Pierre
b370bc27c4 Fixed condition when no resource set found 2016-02-26 11:41:08 +01:00
Pierre
334c3f4488 Removed arrows when only 1 page is needed 2016-02-26 11:34:46 +01:00
Pierre
33822109c0 Minor fixes 2016-02-26 11:20:11 +01:00
Pierre
b1f18b0f5b Added templates, SRs and networks to details 2016-02-26 10:00:32 +01:00
Julien Fontanet
c0d6284368 Fix Jade compilation for Node < v4. 2016-02-25 17:48:16 +01:00
Pierre
16e294f6fc Resource sets details 2016-02-25 17:47:46 +01:00
Pierre
5f7925b2b8 Display page number 2016-02-25 17:47:46 +01:00
Pierre
2e001b0ce4 Pages layout 2016-02-25 17:47:46 +01:00
Pierre
c1ca3ff5b5 [WIP] All resource sets together (other layout commented). 2016-02-25 17:47:46 +01:00
Pierre
1de33cd4ca Self service dashboard (Fix #741)
Fixed no limit condition and icons.
2016-02-25 17:47:46 +01:00
Olivier Lambert
77b773388f fix network list in new vm, using map before orderby 2016-02-25 15:47:34 +01:00
Olivier Lambert
3e668ee439 Merge pull request #759 from vatesfr/fix-babel-6-imports
Fix Babel 6: `require module` --> `require(module).default`
2016-02-25 11:38:23 +01:00
Pierre
0d3ea9af36 Fix Babel 6: require module --> require(module).default 2016-02-25 11:21:47 +01:00
Fabrice Marsaud
0d81bc8056 Fixed backup remote lists 2016-02-25 09:24:38 +01:00
Julien Fontanet
e4b532a34d week heatmap: fix lodash.pluck usage. 2016-02-24 17:49:32 +01:00
Julien Fontanet
61f86c0ac3 Merge pull request #739 from vatesfr/update-deps
Update deps
2016-02-24 16:58:35 +01:00
Julien Fontanet
66fad37116 Remove unused lodash.puck dep. 2016-02-24 16:53:12 +01:00
Julien Fontanet
c7bbd8c823 Update nice-pipe to v3. 2016-02-24 16:44:07 +01:00
Julien Fontanet
dc6f8baf1e Update Babel to v6. 2016-02-24 16:44:07 +01:00
Julien Fontanet
0e76e65d65 Update Coffeeify to v2. 2016-02-24 16:44:07 +01:00
Julien Fontanet
877dbed999 Update Browserify to v13. 2016-02-24 16:44:07 +01:00
Julien Fontanet
668fd05fae Update Bluebird to v3. 2016-02-24 16:44:07 +01:00
Julien Fontanet
3e49998f41 Update Lodash to v4. 2016-02-24 16:44:07 +01:00
Julien Fontanet
99b183ac17 Update Angular to v1.5. 2016-02-24 16:42:51 +01:00
Fabrice Marsaud
bb04cddc48 4.14.1 2016-02-24 13:57:24 +01:00
Fabrice Marsaud
ba3f095dd8 4.14.0 2016-02-24 13:57:13 +01:00
Julien Fontanet
f8438421c8 4.13.1 2016-02-24 13:42:15 +01:00
Julien Fontanet
334361860b Merge pull request #753 from vatesfr/pierre-self-service-user
New-VM: Fixed summary when modifying an existing disk size
2016-02-24 13:42:00 +01:00
Pierre
23bd211758 Fixed summary when modifying an existing disk size 2016-02-24 13:26:58 +01:00
Julien Fontanet
4cc8fb9891 Merge pull request #752 from vatesfr/pierre-state-go
Better redirection when creating VM(s)
2016-02-24 12:51:32 +01:00
Pierre
ed3cd690fe Better redirection when creating VM(s) 2016-02-24 12:50:04 +01:00
Julien Fontanet
9a40c7cdc6 Merge pull request #750 from vatesfr/pierre-self-service-user
No limit: limit value should not exist when input field is empty
2016-02-24 12:44:37 +01:00
Pierre
ae4c9ce819 Better handle for no limit 2016-02-24 12:44:06 +01:00
Pierre
2f8bae1356 No limit: limit value should be undefined when input field is empty 2016-02-24 12:23:51 +01:00
Olivier Lambert
2dfcd5b7ef Merge pull request #740 from vatesfr/pierre-self-service-user
Create VM with self service (See #285)
2016-02-23 18:56:48 +01:00
Pierre
739926f64e Fixed button addon in disk list (VM view) 2016-02-23 18:51:04 +01:00
Pierre
a18dde07de Fixed multiple lines ng-disabled 2016-02-23 18:42:54 +01:00
Pierre
6480017a91 Fixed Create button disabled condition 2016-02-23 18:39:47 +01:00
Pierre
4467ec52f7 No limit: undefined instead of null. 2016-02-23 18:24:53 +01:00
Pierre
072d82a10e Size unit fix. 2016-02-23 18:15:43 +01:00
Pierre
b50b759f4f Default RAM size unit in New VM view 2016-02-23 18:11:07 +01:00
Pierre
c1477ad45f Typo fix. 2016-02-23 18:09:02 +01:00
Pierre
679d45399b Typo fix. Default resource set RAM size unit. 2016-02-23 17:31:37 +01:00
Pierre
0e15a789ff Fixed buttons addons 2016-02-23 16:55:47 +01:00
Pierre
dff5b3f497 Fixed resourceSet edition 2016-02-23 16:55:47 +01:00
Pierre
483e49a6ae Connect interface and server 2016-02-23 16:55:47 +01:00
Pierre
a689b5b917 Changed resourceSet^Ctructure for limits. Minor fixes. 2016-02-23 16:55:46 +01:00
Pierre
1363d98280 New VM: progress bars to show remaining available resources 2016-02-23 16:55:46 +01:00
Pierre
405f3dcbdd Resource sets: CPU, RAM and disk space restrictions 2016-02-23 16:55:46 +01:00
Pierre
4b48408bc9 Margin fix 2016-02-23 16:55:46 +01:00
Pierre
d6f1e2d7e2 Form alignements 2016-02-23 16:55:46 +01:00
Pierre
c04c8e3aa4 Moved "select resource set" on title row 2016-02-23 16:55:46 +01:00
Pierre
ff5a08d3b0 Enabled resource set edition 2016-02-23 16:55:46 +01:00
Pierre
d2049c759e Enabled ISO installing method 2016-02-23 16:55:46 +01:00
Pierre
f940cb0ace Redirect to list view instead of VM view 2016-02-23 16:55:46 +01:00
Pierre
507f2f4af4 Creation form updated when resource set selected 2016-02-23 16:55:45 +01:00
Pierre
deca7099f3 Checking available resource sets for a specific user 2016-02-23 16:55:45 +01:00
Pierre
46a741825a [WIP] Create VM as non-admin user with self service 2016-02-23 16:55:45 +01:00
Fabrice Marsaud
14fdcd3052 Reworked for sequential SR and Network choice 2016-02-23 16:55:44 +01:00
Fabrice Marsaud
c76b01608a Fixes. Commented quotas for later use. 2016-02-23 16:55:44 +01:00
Fabrice Marsaud
be709d6601 Rewrite on server API 2016-02-23 16:55:44 +01:00
Fabrice Marsaud
7dc8fac198 Fix constraint rules 2016-02-23 16:55:44 +01:00
Fabrice Marsaud
29ae7d57fd display and mock save/edit 2016-02-23 16:55:43 +01:00
Fabrice Marsaud
1bf9ce872b constraint reolutions 2016-02-23 16:55:43 +01:00
Fabrice Marsaud
0071c9504f first steps 2016-02-23 16:55:43 +01:00
Olivier Lambert
594a872c84 forget _reportWhen 2016-02-23 16:54:23 +01:00
Julien Fontanet
0d1f78e82e Better permission handling when not connected. 2016-02-23 16:49:14 +01:00
Julien Fontanet
a12de51897 generic modal: fix missing cancel button. 2016-02-23 16:12:59 +01:00
Olivier Lambert
74fa084dd0 update changelog 2016-02-23 16:02:37 +01:00
Julien Fontanet
a1ee258da5 Do not call vm.set() directly in new VM controller. 2016-02-23 15:18:34 +01:00
Julien Fontanet
c1af171c5d Do not call vm.set() directly in VM controller. 2016-02-23 15:04:50 +01:00
Olivier Lambert
e36c9560fa fix form declaration for conditional backup. Thanks to @Danp2 for pointing it 2016-02-23 13:58:37 +01:00
Olivier Lambert
5cd19ddc8d UI fix for non admin users 2016-02-23 11:00:36 +01:00
Olivier Lambert
ac243e5d11 Merge pull request #745 from vatesfr/pierre-overview-remote-status-indicator
Fixed error button condition in Overview. Minor UI fix.
2016-02-23 10:23:00 +01:00
Olivier Lambert
b480d019f6 add conditional reporting for rolling snaps 2016-02-23 10:21:46 +01:00
Olivier Lambert
cc13ab97d6 add conditional reporting for DR 2016-02-23 10:19:00 +01:00
Olivier Lambert
fee1d2ed04 add conditional report for basic backup 2016-02-23 10:15:36 +01:00
Pierre
4144d5faa6 Fixed error button condition in Overview. Minor UI fix. 2016-02-23 10:11:09 +01:00
Olivier Lambert
38a23c0bee add conditional reporting for delta backup 2016-02-23 09:50:23 +01:00
Olivier Lambert
8fcfebe170 Merge pull request #744 from Danp2/reportWhen
CR conditional reporting
2016-02-22 21:54:34 +01:00
Danp2
f917fa8138 CR conditional reporting
Add functionality to allow conditional reporting to continuous
replication backup
2016-02-22 14:31:40 -06:00
Julien Fontanet
f60b611304 Revert to use ~ for Angular versions. 2016-02-22 18:32:40 +01:00
Olivier Lambert
a54624e5c8 Merge pull request #743 from vatesfr/sources-disclaimer
Sources disclaimer
2016-02-22 18:08:09 +01:00
Olivier Lambert
370b14b82e better text and link to XOA page 2016-02-17 13:21:00 +01:00
Julien Fontanet
88205adeb2 Update Angular to v1.4.9. 2016-02-17 12:42:57 +01:00
Julien Fontanet
351ce995d9 Sources disclaimer appears once every week. 2016-02-17 12:42:42 +01:00
Julien Fontanet
a9aa92de90 modal service: new htmlMessage option. 2016-02-17 12:34:46 +01:00
Olivier Lambert
9ea665dea2 include default cloudconfig + link toward the official doc 2016-02-17 11:42:53 +01:00
Julien Fontanet
30b52527e7 modal service: new alert() method. 2016-02-17 11:40:54 +01:00
Julien Fontanet
bb4125153b Dashboard/Health: fix deletion of VDIs/VMs after unselected item(s). 2016-02-16 17:50:41 +01:00
Olivier Lambert
f0d5b2b1da Merge pull request #732 from vatesfr/pierre-overview-remote-status-indicator
Backup overview: Status indicator for the remote associated to each job (Fix #728)
2016-02-12 18:04:25 +01:00
Pierre
30c4048e4a Handle remote/remoteId property 2016-02-12 17:48:51 +01:00
Pierre
93770ca9ce Alert when remote does not exist 2016-02-12 17:21:41 +01:00
Pierre
e788783d12 Backup overview: Status indicator for the remote associated to each job 2016-02-12 17:10:11 +01:00
Olivier Lambert
1314444d7c Merge pull request #730 from vatesfr/pierre-boot-order
Fixed boot order options bug (Fix #726)
2016-02-12 11:27:44 +01:00
Pierre
2a14664d34 Fixed boot order options bug (Fix #726)
Unchecking a boot option should not uncheck all the options below it.
2016-02-12 11:12:48 +01:00
Olivier Lambert
07a03940a0 order installed patch by name 2016-02-11 11:45:56 +01:00
Olivier Lambert
5f2f6fff56 Merge pull request #725 from vatesfr/pierre-vm-view-ui-enhancements
New VM: fixed summary refresh (Fix #721)
2016-02-11 11:06:55 +01:00
Pierre
353548660c New VM: fixed summary refresh (Fix #721) 2016-02-11 10:58:00 +01:00
Olivier Lambert
912f07225c Merge pull request #723 from vatesfr/pierre-custom-cloud-config
Custom config in Config Drive
2016-02-11 10:47:29 +01:00
Pierre
a23b7eeff1 Uplad config file: status icons, size check. Minor fixes. 2016-02-11 10:32:58 +01:00
Pierre
574f0d71b2 Custom config in Config Drive 2016-02-10 16:32:11 +01:00
Olivier Lambert
a368312035 Merge pull request #720 from vatesfr/pierre-vm-view-ui-enhancements
Size input should allow float numbers (Fix #719)
2016-02-10 14:46:16 +01:00
Pierre
2f88b1ab65 +string instead of parseFloat(string) 2016-02-10 14:43:15 +01:00
Pierre
f85f97e061 Size input should allow float numbers (Fix #719) 2016-02-10 14:36:00 +01:00
Olivier Lambert
30dec13903 Merge pull request #718 from vatesfr/pierre-table-ellipsis
Fixed text overflow in tables. (See #713)
2016-02-10 14:02:37 +01:00
Pierre
3057e5c997 Fixed text overflow in tables. (See #713)
`overflow: hidden` for td and th. Inline CSS style when `overflow: visible` is needed.
2016-02-10 10:04:49 +01:00
Olivier Lambert
6c413eb1ba Fix issue #703 2016-02-08 17:38:22 +01:00
Julien Fontanet
dcdd9132e2 The main bug tracker is xo-web. 2016-02-08 15:33:35 +01:00
Olivier Lambert
d9b1c36055 typo in changelog 2016-02-04 22:14:39 +01:00
Olivier Lambert
a593a247d7 change log update 2016-02-04 22:13:51 +01:00
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
9d763773cf 4.1.0 2015-06-10 16:58:48 +02:00
Olivier Lambert
540f977146 Update CHANGELOG.md 2015-06-10 16:24:33 +02:00
Olivier Lambert
d16b09d3fc fix issue #287 2015-06-10 16:22:35 +02:00
Olivier Lambert
6f8a8d3b90 Update CHANGELOG.md 2015-06-10 13:19:02 +02:00
Olivier Lambert
00ef4166c7 fix #286 2015-06-10 10:35:56 +02:00
Olivier Lambert
b88414735e minor style modification for drag'n drop 2015-06-09 13:31:53 +02:00
Fabrice Marsaud
af092fae9b Fixed missing anguler injection 2015-06-09 12:58:11 +02:00
Fabrice Marsaud
b889efc913 migratePool fallback for dragNdrop migration 2015-06-09 12:58:11 +02:00
Fabrice Marsaud
877dd68a6b Final drag style 2015-06-09 12:58:11 +02:00
Fabrice Marsaud
2805a1c7bc A bit of style 2015-06-09 12:58:11 +02:00
Fabrice Marsaud
c5c000ea6f Work in progress 2015-06-09 12:58:11 +02:00
Fabrice Marsaud
673f1072bf Minor fix 2015-06-09 12:58:11 +02:00
Fabrice Marsaud
d0e93b9b9f Drag & drop VM migration in progress 2015-06-09 12:58:11 +02:00
Olivier Lambert
f239088bcb fix #270 2015-06-08 20:49:10 +02:00
Fabrice Marsaud
32642f105c Groups UI enhancement 2015-06-08 16:50:04 +02:00
Fabrice Marsaud
4adaf6d355 Group panel allows ACL suppression, and user search for add 2015-06-08 14:21:09 +02:00
Fabrice Marsaud
291e2a5e40 group: direct user add, without button 2015-06-08 13:34:26 +02:00
Fabrice Marsaud
05bdb56203 Minor code fix 2015-06-08 11:59:29 +02:00
Fabrice Marsaud
cb71df8345 Group view improvements 2015-06-08 11:54:03 +02:00
Fabrice Marsaud
c6c5f5188b Group dedicated edit page with acl recap 2015-06-05 15:29:34 +02:00
Fabrice Marsaud
a7b6ca0914 Fix for Issue #272 hide non auth objects 2015-06-04 17:16:49 +02:00
Fabrice Marsaud
30ba062695 Fixes for issue #271 newvm 2015-06-04 09:17:10 +02:00
Fabrice Marsaud
a595af7b3f vm controller clean up 2015-06-03 17:51:57 +02:00
Fabrice Marsaud
b2ee3172d8 console view data behaves ok on browser refresh 2015-06-03 17:45:23 +02:00
Fabrice Marsaud
73992ee8e9 console iso-device integration 2015-06-03 17:28:24 +02:00
Fabrice Marsaud
78885fd00a Fixed iso-device bug 2015-06-03 17:27:53 +02:00
Fabrice Marsaud
ce55ac6ccb Reworked iso-device module and integration on vm view 2015-06-03 17:01:30 +02:00
Fabrice Marsaud
8ce0951e5f Fix Issue #271 2015-06-03 14:53:42 +02:00
Julien Fontanet
7788fa9d3e Use standard with babel-eslint. 2015-06-03 09:40:38 +02:00
Julien Fontanet
7f36552c71 Release process. 2015-06-03 09:40:38 +02:00
Fabrice Marsaud
16f9437b29 Solving Issue #269 2015-06-02 15:51:40 +02:00
Julien Fontanet
0beaff718e Update release process. 2015-06-01 16:18:31 +02:00
Olivier Lambert
9b6f37b5d0 add changelog for patch releases 2015-06-01 16:16:08 +02:00
Olivier Lambert
3d6d4aea6a 4.0.2 2015-06-01 10:26:34 +02:00
Fabrice Marsaud
2356a21e54 Fix for Issue #264 field resets whil editing users 2015-06-01 09:32:20 +02:00
Olivier Lambert
a55e7ed34f 4.0.1 2015-05-30 18:35:34 +02:00
Fabrice Marsaud
e355e4d35d Fixed xoa-updater-service url 2015-05-30 14:24:57 +02:00
Julien Fontanet
6dcaf80f3f 4.0.0 2015-05-29 16:14:34 +02:00
Julien Fontanet
a465114d36 Remove unused imports & coding style. 2015-05-29 16:11:59 +02:00
Julien Fontanet
07fbcb3488 Various changes. 2015-05-29 16:02:54 +02:00
Julien Fontanet
534fbe1b6e Force Angular 1.4. 2015-05-29 16:02:45 +02:00
Julien Fontanet
f5c9c1ba0e Fix networks on new VM page on first load. 2015-05-29 16:02:15 +02:00
Julien Fontanet
5d5485f569 Fix host patches pagination. 2015-05-29 15:25:09 +02:00
Julien Fontanet
3d3fa5d18a Work around an Angular issue. 2015-05-29 14:20:05 +02:00
Julien Fontanet
312c41f229 Fix CD handling on VM page. 2015-05-29 14:04:10 +02:00
Fabrice Marsaud
2df1dc9028 Fix disks reordering on VM page. 2015-05-29 13:30:21 +02:00
Olivier Lambert
222f245e63 workaround with ng if 2015-05-29 13:29:52 +02:00
Olivier Lambert
2aa7702aed ng-if breaks the scope, replaced by ng-show 2015-05-29 13:15:16 +02:00
Olivier Lambert
0b185c35c2 restore UUID display 2015-05-29 12:19:41 +02:00
Fabrice Marsaud
48dcec3cc3 Separated user and group managment 2015-05-29 12:11:26 +02:00
Fabrice Marsaud
8567179fa3 reset ACL form after creation 2015-05-29 11:44:29 +02:00
Fabrice Marsaud
79d15ecd7e Sort user/group edit by id to avoid jumps while editing 2015-05-29 11:37:27 +02:00
Fabrice Marsaud
837c7e4bc7 Fix a missing loading anim when creating iSCSI SR 2015-05-29 11:30:51 +02:00
Fabrice Marsaud
2ae7e9920d Fix NFS_ISO missing settings for SR creation 2015-05-29 11:19:41 +02:00
Julien Fontanet
8cf955b674 Minor code simplification. 2015-05-29 10:50:35 +02:00
Julien Fontanet
33f897d43e Fix _gatherConnectedUuids(). 2015-05-29 10:45:32 +02:00
Fabrice Marsaud
ddb0946a0d Fixed UI bug on group user add 2015-05-29 08:30:34 +02:00
Fabrice Marsaud
0f5beac4a8 Fix group managment 2015-05-29 00:47:02 +02:00
Fabrice Marsaud
974e2f71f9 Roles added to ACLs 2015-05-28 23:58:36 +02:00
Fabrice Marsaud
3c427d7e28 acl work in progress 2015-05-28 23:26:03 +02:00
Fabrice Marsaud
0f10c8f5df bugfix on empty groups for settings/user 2015-05-28 21:50:49 +02:00
Olivier Lambert
7840b51f5c Update CHANGELOG.md 2015-05-28 19:27:18 +02:00
Olivier Lambert
6578855182 fix a bug with Other media install 2015-05-28 18:39:52 +02:00
Julien Fontanet
58d68497a4 Fixed width icons on ACLs subject selection. 2015-05-28 16:51:39 +02:00
Julien Fontanet
bddcf42a54 Fix access control to different pages. 2015-05-28 16:50:56 +02:00
Julien Fontanet
6318f4e7ac Correctly escape special chars in RegExp. 2015-05-28 16:20:37 +02:00
Julien Fontanet
0c6cced7ee Update xo-lib to 0.7.2. 2015-05-28 15:51:20 +02:00
Julien Fontanet
925bf47c9e Fix patches on host view. 2015-05-28 12:14:26 +02:00
Julien Fontanet
8472b991ff Only display running tasks on host view. 2015-05-28 12:06:56 +02:00
Julien Fontanet
ed59c32d96 Revert "Use lodash.slice instead of builtin method."
This reverts commit 1aaaee128f.

lodash.slice() incorrectly used on strings.
2015-05-28 11:54:27 +02:00
Julien Fontanet
b1981d7499 Various fixes. 2015-05-28 11:54:23 +02:00
Julien Fontanet
8983dfea57 Various fixes. 2015-05-28 10:58:24 +02:00
Julien Fontanet
5231b9b22b Again: UUID → id. 2015-05-28 10:25:04 +02:00
Julien Fontanet
55846a2314 Fix task.created date formatting. 2015-05-28 10:25:03 +02:00
Julien Fontanet
1c94f5749d Fix logs. 2015-05-28 10:25:03 +02:00
Fabrice Marsaud
90bacd9d31 VM Create Disk panal closes on success 2015-05-28 09:46:45 +02:00
Julien Fontanet
0053cbf782 No more UUID (→ id). 2015-05-27 19:44:28 +02:00
Julien Fontanet
5d120a79e8 Various fixes. 2015-05-27 19:42:31 +02:00
Julien Fontanet
3389569ea0 Various fixes. 2015-05-27 19:35:25 +02:00
Julien Fontanet
f546606de0 Various fixes. 2015-05-27 19:28:21 +02:00
Julien Fontanet
fef95b3aae Various fixes. 2015-05-27 19:21:49 +02:00
Julien Fontanet
5ba2b72439 Various fixes. 2015-05-27 18:59:15 +02:00
Julien Fontanet
4bb849f7c9 Updates xo-lib to v0.7.1. 2015-05-27 18:04:11 +02:00
Fabrice Marsaud
21b5e7e701 ACL Panel exposes groups 2015-05-27 17:20:42 +02:00
Julien Fontanet
34a1965497 Fix CoffeeScript indentation. 2015-05-27 17:19:31 +02:00
Julien Fontanet
1701682636 Rename filter: % → percentage. 2015-05-27 17:19:31 +02:00
Julien Fontanet
5d826972f1 Remove unused gulp-coffee. 2015-05-27 17:19:31 +02:00
Fabrice Marsaud
2467b336e5 Group managment in settings 2015-05-27 17:03:28 +02:00
Julien Fontanet
4f78414c7f Fixing host view… 2015-05-27 16:58:32 +02:00
Julien Fontanet
4532714bae Fix the tree view. 2015-05-27 16:36:01 +02:00
Olivier Lambert
352c23b0ba Update CHANGELOG.md 2015-05-27 15:25:38 +02:00
Julien Fontanet
8e432ee818 Global stats. 2015-05-27 15:18:49 +02:00
Julien Fontanet
47bb2d24f5 Avoid polluting the local scope in xo-api. 2015-05-27 14:24:55 +02:00
Julien Fontanet
f3fd4c607d Fix race condition in host view. 2015-05-27 14:24:32 +02:00
Julien Fontanet
0610ceafdf Remove unused $scope.removeMessage(). 2015-05-27 12:52:01 +02:00
Julien Fontanet
032fcdce40 Fix VDI handling in VM view. 2015-05-26 18:31:47 +02:00
Olivier Lambert
636bacd637 using standard style with a linter show that missing semicolon 2015-05-26 18:09:26 +02:00
Olivier Lambert
3f3fbd8bbc Update CHANGELOG.md 2015-05-26 10:48:34 +02:00
Fabrice Marsaud
955e88b4fb Fixed minor jade template error 2015-05-26 09:55:10 +02:00
Olivier Lambert
5954b552c9 fix patch issue 2015-05-25 15:31:58 +02:00
Julien Fontanet
aaad4c5d20 Fix production build (fix #261). 2015-05-25 10:21:45 +02:00
Julien Fontanet
a24c8526ea CSS sourcemaps only in dev mode! 2015-05-24 19:03:30 +02:00
Julien Fontanet
a533535520 Do not include CSS source maps in prod. 2015-05-24 18:47:38 +02:00
Julien Fontanet
badded3aa4 Inline external CSS. 2015-05-24 18:32:50 +02:00
Julien Fontanet
3055e612d4 Source maps for CSS. 2015-05-24 18:18:54 +02:00
Julien Fontanet
525cb1a2b6 Update deps. 2015-05-24 18:10:31 +02:00
Julien Fontanet
4dd70abc3b Remove a console.log. 2015-05-24 15:50:28 +02:00
Julien Fontanet
2ea4c214df Use more objects views and less the byType index. 2015-05-24 15:49:50 +02:00
Julien Fontanet
0a0174a79d New objects view: runningVms. 2015-05-24 15:21:14 +02:00
Julien Fontanet
3db031be1b Updates xo-collection to 0.3.2.. 2015-05-24 14:42:56 +02:00
Julien Fontanet
6d3a87fe7d New objects view: runningTasks. 2015-05-24 14:38:53 +02:00
Olivier Lambert
8cfd2cdd79 add host stats fix 2015-05-22 22:11:26 +02:00
Fabrice Marsaud
9e874e076f Stats stop if response exceeds 10 sec 2015-05-22 17:45:38 +02:00
Fabrice Marsaud
28192bf184 Simple x reattempt mode for stats 2015-05-22 17:35:53 +02:00
Julien Fontanet
a54957b4de Use $applyAsync() instead of $apply() for perf. 2015-05-22 15:16:02 +02:00
Julien Fontanet
f4b1a076b7 Better sync with xo-lib. 2015-05-22 15:15:39 +02:00
Julien Fontanet
27a3296d6e No need to use the UUID index. 2015-05-22 13:10:03 +02:00
Julien Fontanet
1aaaee128f Use lodash.slice instead of builtin method. 2015-05-22 12:12:39 +02:00
Julien Fontanet
15a16a2c35 Remove unnecessary modules. 2015-05-22 12:10:54 +02:00
Julien Fontanet
db23fe5a58 Fix SRs in list view. 2015-05-22 12:06:04 +02:00
Julien Fontanet
620c88b615 Move slice to xo-filters and use lodash.slice. 2015-05-22 12:05:49 +02:00
Julien Fontanet
99f2fb9764 Use views in list page. 2015-05-21 18:27:24 +02:00
Julien Fontanet
d5a3e67dbd Use more xo-collection/View in the tree page. 2015-05-21 17:57:27 +02:00
Julien Fontanet
55ef81f3e7 master-select module. 2015-05-21 17:48:48 +02:00
Julien Fontanet
41699fab1e Use lodash.clonedeep instead of angular.copy. 2015-05-21 17:48:48 +02:00
Julien Fontanet
32a1195157 Always use lodash.foreach. 2015-05-21 17:48:48 +02:00
Olivier Lambert
f53db2ddfa fix a display issue in general panel for VM view 2015-05-21 15:16:26 +02:00
Fabrice Marsaud
e060f9172b Trial handling removal 2015-05-21 11:16:46 +02:00
Fabrice Marsaud
4adef88e61 Minor update message fix 2015-05-21 11:14:26 +02:00
Fabrice Marsaud
d734f2cf89 Fixed host/patches panel 2015-05-21 10:41:35 +02:00
Fabrice Marsaud
3e81d14bd8 First delivery for pagination Issue #221 2015-05-21 09:09:14 +02:00
Olivier Lambert
e88a94d9e0 better UI for registration 2015-05-20 19:40:45 +02:00
Olivier Lambert
f4f16e4e87 updater UI fixes 2015-05-20 18:57:20 +02:00
Olivier Lambert
6268f3a3d9 small improvement for UI in vm creation 2015-05-20 18:00:50 +02:00
Fabrice Marsaud
06e7c8d19a VM logs are paginated 2015-05-20 17:50:46 +02:00
Olivier Lambert
32395232ea nicer vm creation view 2015-05-20 17:49:55 +02:00
Julien Fontanet
65d6ef91ff Fix defaultArgsMapper. 2015-05-20 17:19:41 +02:00
Julien Fontanet
4aecc875d1 Reorganize service. 2015-05-20 17:13:19 +02:00
Fabrice Marsaud
0e649a626c Handle unexisting boot order 2015-05-20 15:04:39 +02:00
Fabrice Marsaud
5fa249b0f3 Minor touch on boot reordering 2015-05-20 14:31:47 +02:00
Julien Fontanet
24ca86aad3 osFamily Angular filter. 2015-05-20 14:26:38 +02:00
Julien Fontanet
8a4f413289 TODO: deleteVms should be integrated in xo.deleteVms(). 2015-05-20 14:26:38 +02:00
Julien Fontanet
6dbad4501d Use ES6 export syntax. 2015-05-20 14:26:38 +02:00
Julien Fontanet
9ab6490fee Typo. 2015-05-20 14:26:38 +02:00
Julien Fontanet
a413efa550 Remove unnecessary values(). 2015-05-20 14:26:38 +02:00
Julien Fontanet
cd337d444c Minor improvement of xoApi.getObject(). 2015-05-20 14:26:38 +02:00
Julien Fontanet
45e1ce0a42 Fix global stats on the tree view. 2015-05-20 14:26:38 +02:00
Fabrice Marsaud
e5ef1e6efe First delivery for Issue #251 2015-05-20 14:05:06 +02:00
Julien Fontanet
b1ce3be3d2 Move consoles in /api/consoles/. 2015-05-20 10:28:52 +02:00
Olivier Lambert
e13ab73a29 stat changes 2015-05-19 21:19:18 +02:00
Fabrice Marsaud
aede952b12 Issue #253 2015-05-19 18:38:14 +02:00
Fabrice Marsaud
acc1476b29 Fixing Issue #250 2015-05-19 18:38:14 +02:00
Fabrice Marsaud
138bf56624 A fix on xoa-updater handling 2015-05-19 18:38:14 +02:00
Julien Fontanet
c608de4183 SPDX valid license. 2015-05-19 17:44:07 +02:00
Julien Fontanet
ccb6c02c31 Use more const. 2015-05-19 17:35:14 +02:00
Julien Fontanet
5cc457b28c Use xo-lib 0.7. 2015-05-19 16:55:54 +02:00
Julien Fontanet
a353b3d40d map Angular filter now based on lodash. 2015-05-19 16:54:10 +02:00
Julien Fontanet
6f7aca8e5b isEmpty Angular filter. 2015-05-19 16:53:50 +02:00
Julien Fontanet
92b0d4561e xo-web should only have dev deps. 2015-05-19 15:54:27 +02:00
Olivier Lambert
ef8b8346dc better UI for host 2015-05-19 14:55:10 +02:00
Fabrice Marsaud
058058a015 Updater rewritten with ws 2015-05-19 14:52:49 +02:00
Olivier Lambert
fddba7315a better recap for templates with disks 2015-05-18 20:57:18 +02:00
Olivier Lambert
a5e964ea19 start VM creation from template with disks 2015-05-18 20:39:48 +02:00
Julien Fontanet
3d2152e559 Fix relative URLs (fix #254). 2015-05-18 20:34:46 +02:00
Olivier Lambert
50f9c68c26 add spinner while fetching host stats 2015-05-18 17:13:04 +02:00
Olivier Lambert
b40207b367 start to add stats to host view 2015-05-18 17:13:04 +02:00
Julien Fontanet
6c9305d2b1 Fix unnecessary recompilation of app.js 2015-05-18 16:20:06 +02:00
Julien Fontanet
9fda3c911d Standard code style. 2015-05-18 16:13:36 +02:00
Olivier Lambert
473c3601ef better responsive tree menu for extra small devices 2015-05-18 12:38:38 +02:00
Olivier Lambert
fde8a3720d more responsive settings menu 2015-05-18 12:04:16 +02:00
Fabrice Marsaud
13a6d6b458 xoa-updater proxy settings when xoa-updater available 2015-05-18 09:26:39 +02:00
Olivier Lambert
29d9ba0446 some UI fixes for VM view 2015-05-15 10:46:57 +02:00
Fabrice Marsaud
71e271774e Tolerance to xoa-updater missing 2015-05-14 15:45:21 +02:00
Olivier Lambert
c9db49e255 tree view is now compatible with small devices 2015-05-14 15:10:09 +02:00
Olivier Lambert
22f35f0e86 pool view better UI 2015-05-14 15:10:09 +02:00
Fabrice Marsaud
375f3ac3ac updater loss and reconnection properly handled 2015-05-14 15:03:34 +02:00
Olivier Lambert
b60a02bc34 use ellipsis to compact lines in tables 2015-05-14 14:56:25 +02:00
Olivier Lambert
5a4d821c98 fix display issue for small devices in vm view 2015-05-14 14:24:14 +02:00
Olivier Lambert
cd0305c71d better UI display for small devices 2015-05-14 14:22:17 +02:00
Olivier Lambert
371459ff5e far better fluid display for host view 2015-05-14 14:15:09 +02:00
Fabrice Marsaud
5a8a7c6a0f A first delivery for trial handling 2015-05-14 13:01:27 +02:00
Fabrice Marsaud
69db541300 Starting to handle trial 2015-05-14 13:01:27 +02:00
Olivier Lambert
94949866ee reduce the warning lenght and add a tooltip 2015-05-13 18:41:51 +02:00
Fabrice Marsaud
c22b3e7449 Fix : removed a no mùore existing service provider 2015-05-12 10:22:57 +02:00
Fabrice Marsaud
096dde922b Minor fixes 2015-05-12 10:08:30 +02:00
Fabrice Marsaud
00f26d854f updater and register services merged 2015-05-12 10:08:30 +02:00
Fabrice Marsaud
6c8ff1717e bug fix 2015-05-12 10:08:30 +02:00
Fabrice Marsaud
c7288c1d8a No https for update/register 2015-05-12 10:08:30 +02:00
Fabrice Marsaud
2e52fe369d bugfix 2015-05-12 10:08:30 +02:00
Fabrice Marsaud
df3430add5 updating url fix 2015-05-12 10:08:30 +02:00
Fabrice Marsaud
7af848c94b Minor fix 2015-05-12 10:08:30 +02:00
Fabrice Marsaud
f57c462b5f Fix + enhancement 2015-05-12 10:08:30 +02:00
Fabrice Marsaud
6018035908 Update feature fully adapted to xoa-updater and xoa-register 2015-05-12 10:08:30 +02:00
Fabrice Marsaud
c8b0351786 Nice update/register panels 2015-05-12 10:08:30 +02:00
Fabrice Marsaud
26cc812f82 a bit of UI tuning 2015-05-12 10:08:30 +02:00
Olivier Lambert
67f98950e6 ui fixes 2015-05-12 10:08:30 +02:00
Fabrice Marsaud
8ba8537b9f First proto delivery for updating from xo-web 2015-05-12 10:08:30 +02:00
Fabrice Marsaud
a7f05a68e0 UI sketch for updating 2015-05-12 10:08:30 +02:00
Olivier Lambert
a0db228154 better docker UI and border for stats 2015-05-08 19:10:19 +02:00
Olivier Lambert
eec6fabe58 add UI info for Docker enabled VMs 2015-05-08 16:55:02 +02:00
Olivier Lambert
501c038f97 reflect changes done in server 2015-05-08 16:12:42 +02:00
Olivier Lambert
e0a0f717fd add container stuff 2015-05-08 15:37:52 +02:00
Olivier Lambert
4dd3f7487c Update README.md 2015-05-07 19:41:53 +02:00
Olivier Lambert
99d1cddaa5 fix #245 2015-05-07 19:17:17 +02:00
Olivier Lambert
2158e1a47e Merge branch 'patch' into next-release 2015-05-06 18:44:22 +02:00
Olivier Lambert
059238759a better patch view 2015-05-06 18:21:45 +02:00
Olivier Lambert
8d3ea7548a add a TODO 2015-05-06 16:05:46 +02:00
Olivier Lambert
221b411b63 patch management merged 2015-05-06 16:03:26 +02:00
Olivier Lambert
e2c173990f work in progress with pool patches 2015-05-05 19:41:09 +02:00
Olivier Lambert
a609a8d5d6 patch system in the host view is OK 2015-05-05 18:32:08 +02:00
Olivier Lambert
c5c2afddc2 UI fixes 2015-05-02 19:22:13 +02:00
Olivier Lambert
409d87f210 start to work on update and patching 2015-05-02 18:15:44 +02:00
Julien Fontanet
78baa4b01e Better/simpler styling of the Settings using flexbox. 2015-04-21 12:12:04 +02:00
204 changed files with 20140 additions and 3338 deletions

12
.babelrc Normal file
View File

@@ -0,0 +1,12 @@
{
"comments": false,
"compact": true,
"plugins": [
"transform-decorators-legacy",
"transform-runtime"
],
"presets": [
"stage-0",
"es2015"
]
}

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

View File

@@ -1,94 +0,0 @@
{
// Julien Fontanet JSHint configuration
// https://gist.github.com/julien-f/8095615
//
// Changes from defaults:
// - all enforcing options (except `++` & `--`) enabled
// - single quotes
// - indentation set to 2 instead of 4
// - almost all relaxing options disabled
// - allow expression statements (necessary for chai.expect())
// - environments are set to Browserify, mocha & Node.js
//
// See http://jshint.com/docs/ for more details
"maxerr" : 50, // {int} Maximum error before stopping
// Enforcing
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
"camelcase" : true, // true: Identifiers must be in camelCase
"curly" : true, // true: Require {} for every new block or scope
"eqeqeq" : true, // true: Require triple equals (===) for comparison
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
"freeze" : true, // true: Prohibit overwriting prototypes of native objects (Array, Date, ...)
"immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
"indent" : 2, // {int} Number of spaces to use for indentation
"latedef" : true, // true: Require variables/functions to be defined before being used
"newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()`
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
"noempty" : true, // true: Prohibit use of empty blocks
"nonbsp" : true, // true: Prohibit use of non breakable spaces
"nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment)
"plusplus" : false, // true: Prohibit use of `++` & `--`
"quotmark" : "single", // Quotation mark consistency:
// false : do nothing (default)
// true : ensure whatever is used is consistent
// "single" : require single quotes
// "double" : require double quotes
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
"unused" : true, // true: Require all defined variables be used
"strict" : false, // true: Requires all functions run in ES5 Strict Mode
"maxcomplexity" : 7, // {int} Max cyclomatic complexity per function
"maxdepth" : 3, // {int} Max depth of nested blocks (within functions)
"maxlen" : 80, // {int} Max number of characters per line
"maxparams" : 4, // {int} Max number of formal params allowed per function
"maxstatements" : 20, // {int} Max number statements per function
// Relaxing
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
"boss" : false, // true: Tolerate assignments where comparisons would be expected
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
"eqnull" : false, // true: Tolerate use of `== null`
"esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`)
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
"expr" : false, // true: Tolerate `ExpressionStatement` as Programs
"funcscope" : false, // true: Tolerate defining variables inside control statements
"globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
"iterator" : false, // true: Tolerate using the `__iterator__` property
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
"laxcomma" : false, // true: Tolerate comma-first style coding
"loopfunc" : false, // true: Tolerate functions being defined in loops
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
// (ex: `for each`, multiple try/catch, function expression…)
"multistr" : false, // true: Tolerate multi-line strings
"notypeof" : false, // true: Tolerate typeof comparison with unknown values.
"proto" : false, // true: Tolerate using the `__proto__` property
"scripturl" : false, // true: Tolerate script-targeted URLs
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
"validthis" : false, // true: Tolerate using this in a non-constructor function
"noyield" : false, // true: Tolerate generators without yields
// Environments
"browser" : false, // Web Browser (window, document, etc)
"browserify" : true, // Browserify (node.js code in the browser)
"couch" : false, // CouchDB
"devel" : false, // Development/debugging (alert, confirm, etc)
"dojo" : false, // Dojo Toolkit
"jquery" : false, // jQuery
"mocha" : false, // mocha
"mootools" : false, // MooTools
"node" : false, // Node.js
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
"phantom" : false, // PhantomJS
"prototypejs" : false, // Prototype and Scriptaculous
"rhino" : false, // Rhino
"worker" : false, // Web Workers
"wsh" : false, // Windows Scripting Host
"yui" : false, // Yahoo User Interface
// Custom Globals
"globals" : {} // additional predefined global variables
}

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,11 +1,513 @@
# ChangeLog
## **4.16.0** (2016-04-29)
Maintenance release
### Enhancements
- TOO\_MANY\_PENDING\_TASKS [\#861](https://github.com/vatesfr/xo-web/issues/861)
### Bug fixes
- Incorrect VM target name with continuous replication [\#904](https://github.com/vatesfr/xo-web/issues/904)
- Error while deleting users [\#901](https://github.com/vatesfr/xo-web/issues/901)
- Use an available path to the SR to create a config drive [\#882](https://github.com/vatesfr/xo-web/issues/882)
- VM autoboot don't set the right pool parameter [\#879](https://github.com/vatesfr/xo-web/issues/879)
- BUG: ACL with NFS ISO Library not working! [\#870](https://github.com/vatesfr/xo-web/issues/870)
- Broken paths in backups in SMB [\#865](https://github.com/vatesfr/xo-web/issues/865)
- Plugins page loads users/groups multiple times [\#829](https://github.com/vatesfr/xo-web/issues/829)
- "Ghost" VM remains after migration [\#769](https://github.com/vatesfr/xo-web/issues/769)
## **4.15.0** (2016-03-21)
Load balancing, SMB delta support, advanced network operations...
### Enhancements
- Add the job name inside the backup email report [\#819](https://github.com/vatesfr/xo-web/issues/819)
- Delta backup with quiesce [\#812](https://github.com/vatesfr/xo-web/issues/812)
- Hosts: No user feedback when error occurs with SR connect / disconnect [\#810](https://github.com/vatesfr/xo-web/issues/810)
- Expose components versions [\#807](https://github.com/vatesfr/xo-web/issues/807)
- Rework networks/PIFs management [\#805](https://github.com/vatesfr/xo-web/issues/805)
- Displaying all SRs and a list of available hosts for creating VM from a pool [\#790](https://github.com/vatesfr/xo-web/issues/790)
- Add "Source network" on "VM migration" screen [\#785](https://github.com/vatesfr/xo-web/issues/785)
- Migration queue [\#783](https://github.com/vatesfr/xo-web/issues/783)
- Match network names for VM migration [\#782](https://github.com/vatesfr/xo-web/issues/782)
- Disk names [\#780](https://github.com/vatesfr/xo-web/issues/780)
- Self service: should the user be able to set the CPU weight? [\#767](https://github.com/vatesfr/xo-web/issues/767)
- host & pool Citrix license status [\#763](https://github.com/vatesfr/xo-web/issues/763)
- pool view: Provide "updates" section [\#762](https://github.com/vatesfr/xo-web/issues/762)
- XOA ISO image: ambigious root disk label [\#761](https://github.com/vatesfr/xo-web/issues/761)
- Host info: provide system serial number [\#760](https://github.com/vatesfr/xo-web/issues/760)
- CIFS ISO SR Creation [\#731](https://github.com/vatesfr/xo-web/issues/731)
- MAC address not preserved on VM restore [\#707](https://github.com/vatesfr/xo-web/issues/707)
- Failing replication job should send reports [\#659](https://github.com/vatesfr/xo-web/issues/659)
- Display networks in the Pool view [\#226](https://github.com/vatesfr/xo-web/issues/226)
### Bug fixes
- Broken link to backup remote [\#821](https://github.com/vatesfr/xo-web/issues/821)
- Issue with self-signed cert for email plugin [\#817](https://github.com/vatesfr/xo-web/issues/817)
- Plugins view, reset form and errors [\#815](https://github.com/vatesfr/xo-web/issues/815)
- HVM recovery mode is broken [\#794](https://github.com/vatesfr/xo-web/issues/794)
- Disk bug when creating vm from template [\#778](https://github.com/vatesfr/xo-web/issues/778)
- Can't mount NFS shares in remote stores [\#775](https://github.com/vatesfr/xo-web/issues/775)
- VM disk name and description not passed during creation [\#774](https://github.com/vatesfr/xo-web/issues/774)
- NFS mount problem for Windows share [\#771](https://github.com/vatesfr/xo-web/issues/771)
- lodash.pluck not installed [\#757](https://github.com/vatesfr/xo-web/issues/757)
- this.\_getAuthenticationTokensForUser is not a function [\#755](https://github.com/vatesfr/xo-web/issues/755)
- CentOS 6.x 64bit template creates a VM that won't boot [\#733](https://github.com/vatesfr/xo-web/issues/733)
- Lot of xo:perf leading to XO crash [\#575](https://github.com/vatesfr/xo-web/issues/575)
- New collection checklist [\#262](https://github.com/vatesfr/xo-web/issues/262)
## **4.14.0** (2016-02-23)
Self service, custom CloudInit...
### Enhancements
- VM creation self service with quotas [\#285](https://github.com/vatesfr/xo-web/issues/285)
- Cloud config custom user data [\#706](https://github.com/vatesfr/xo-web/issues/706)
- Patches behind a proxy [\#737](https://github.com/vatesfr/xo-web/issues/737)
- Remote store status indicator [\#728](https://github.com/vatesfr/xo-web/issues/728)
- Patch list order [\#724](https://github.com/vatesfr/xo-web/issues/724)
- Enable reporting on additional backup types [\#717](https://github.com/vatesfr/xo-web/issues/717)
- Tooltip name for cancel [\#703](https://github.com/vatesfr/xo-web/issues/703)
- Portable VHD merging [\#646](https://github.com/vatesfr/xo-web/issues/646)
### Bug fixes
- Avoid merge between two delta vdi backups [\#702](https://github.com/vatesfr/xo-web/issues/702)
- Text in table is not cut anymore [\#713](https://github.com/vatesfr/xo-web/issues/713)
- Disk size edition issue with float numbers [\#719](https://github.com/vatesfr/xo-web/issues/719)
- Create vm, summary is not refreshed [\#721](https://github.com/vatesfr/xo-web/issues/721)
- Boot order problem [\#726](https://github.com/vatesfr/xo-web/issues/726)
## **4.13.0** (2016-02-05)
Backup checksum, SMB remotes...
### Enhancements
- Add SMB mount for remote [\#338](https://github.com/vatesfr/xo-web/issues/338)
- Centralize Perm in a lib [\#345](https://github.com/vatesfr/xo-web/issues/345)
- Expose interpool migration details [\#567](https://github.com/vatesfr/xo-web/issues/567)
- Add checksum for delta backup [\#617](https://github.com/vatesfr/xo-web/issues/617)
- Redirect from HTTP to HTTPS [\#626](https://github.com/vatesfr/xo-web/issues/626)
- Expose vCPU weight [\#633](https://github.com/vatesfr/xo-web/issues/633)
- Avoid metadata in delta backup [\#651](https://github.com/vatesfr/xo-web/issues/651)
- Button to clear logs [\#661](https://github.com/vatesfr/xo-web/issues/661)
- Units for RAM and disks [\#666](https://github.com/vatesfr/xo-web/issues/666)
- Remove multiple VDIs at once [\#676](https://github.com/vatesfr/xo-web/issues/676)
- Find orphaned VDI snapshots [\#679](https://github.com/vatesfr/xo-web/issues/679)
- New health view in Dashboard [\#680](https://github.com/vatesfr/xo-web/issues/680)
- Use physical usage for VDI and SR [\#682](https://github.com/vatesfr/xo-web/issues/682)
- TLS configuration [\#685](https://github.com/vatesfr/xo-web/issues/685)
- Better VM info on tree view [\#688](https://github.com/vatesfr/xo-web/issues/688)
- Absolute values in tooltips for tree view [\#690](https://github.com/vatesfr/xo-web/issues/690)
- Absolute values for host memory [\#691](https://github.com/vatesfr/xo-web/issues/691)
### Bug fixes
- Issues on host console screen [\#672](https://github.com/vatesfr/xo-web/issues/672)
- NFS remote mount fails in particular case [\#665](https://github.com/vatesfr/xo-web/issues/665)
- Unresponsive pages [\#662](https://github.com/vatesfr/xo-web/issues/662)
- Live migration fail in the same pool with local SR fails [\#655](https://github.com/vatesfr/xo-web/issues/655)
## **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.
### Enhancements
- Drag'n drop VM in tree view for live migration ([xo-web#277](https://github.com/vatesfr/xo-web/issues/277))
- Better group view with objects ACLs ([xo-web#276](https://github.com/vatesfr/xo-web/issues/276))
- Hide non-visible objects ([xo-web#272](https://github.com/vatesfr/xo-web/issues/272))
### Bug fixes
- 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))
- 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)
An issue in `xo-server` with the password of default admin account and also a UI fix.
### Bug fixes
- Cannot modify admin account ([xo-web#265](https://github.com/vatesfr/xo-web/issues/265))
- Password field seems to keep empty/reset itself after 1-2 seconds ([xo-web#264](https://github.com/vatesfr/xo-web/issues/264))
## **4.0.1** (2015-05-30)
An issue with the updater in HTTPS was left in the *4.0.0*. This patch release fixed
it.
### Bug fixes
- allow updater to work in HTTPS ([xo-web#266](https://github.com/vatesfr/xo-web/issues/266))
## **4.0.0** (2015-05-29)
[Blog post of this release](https://xen-orchestra.com/blog/xen-orchestra-4-0).
### Enhancements
- advanced ACLs ([xo-web#209](https://github.com/vatesfr/xo-web/issues/209))
- xenserver update management ([xo-web#174](https://github.com/vatesfr/xo-web/issues/174) & [xo-web#259](https://github.com/vatesfr/xo-web/issues/259))
- docker control ([xo-web#211](https://github.com/vatesfr/xo-web/issues/211))
- better responsive design ([xo-web#252](https://github.com/vatesfr/xo-web/issues/252))
- host stats ([xo-web#255](https://github.com/vatesfr/xo-web/issues/255))
- pagination ([xo-web#221](https://github.com/vatesfr/xo-web/issues/221))
- web updater
- better VM creation process([xo-web#256](https://github.com/vatesfr/xo-web/issues/256))
- VM boot order([xo-web#251](https://github.com/vatesfr/xo-web/issues/251))
- new mapped collection([xo-server#47](https://github.com/vatesfr/xo-server/issues/47))
- resource location in ACL view ([xo-web#245](https://github.com/vatesfr/xo-web/issues/245))
### Bug fixes
- wrong calulation of RAM amounts ([xo-web#51](https://github.com/vatesfr/xo-web/issues/51))
- checkbox not aligned ([xo-web#253](https://github.com/vatesfr/xo-web/issues/253))
- VM stats behavior more robust ([xo-web#250](https://github.com/vatesfr/xo-web/issues/250))
- XO not on the root of domain ([xo-web#254](https://github.com/vatesfr/xo-web/issues/254))
## **3.9.1** (2015-04-21)
A few bugs hve made their way into *3.9.0*, this minor release fixes
them.
## Bug fixes
### Bug fixes
- correctly keep the VM guest metrics up to date ([xo-web#172](https://github.com/vatesfr/xo-web/issues/172))
- fix edition of a VM snapshot ([b04111c](https://github.com/vatesfr/xo-server/commit/b04111c79ba8937778b84cb861bb7c2431162c11))
@@ -18,7 +520,7 @@ them.
[Blog post of this release](https://xen-orchestra.com/blog/xen-orchestra-3-9).
## Enhancements
### Enhancements
- ability to manually connect/disconnect a server ([xo-web#88](https://github.com/vatesfr/xo-web/issues/88) & [xo-web#234](https://github.com/vatesfr/xo-web/issues/234))
- display the connection status of a server ([xo-web#103](https://github.com/vatesfr/xo-web/issues/103))
@@ -32,7 +534,7 @@ them.
- XO-Server sources are compiled to JS prior distribution: less bugs & faster startups ([xo-server#50](https://github.com/vatesfr/xo-server/issues/50))
- use XAPI `event.from()` instead of `event.next()` which leads to faster connection ([xo-server#52](https://github.com/vatesfr/xo-server/issues/52))
## Bug fixes
### Bug fixes
- removed servers are properly disconnected ([xo-web#61](https://github.com/vatesfr/xo-web/issues/61))
- fix VM creation with multiple interfaces ([xo-wb#229](https://github.com/vatesfr/xo-wb/issues/229))
@@ -42,7 +544,7 @@ them.
[Blog post of this release](https://xen-orchestra.com/blog/xen-orchestra-3-8).
## Enhancements
### Enhancements
- initial plugin system ([xo-server#37](https://github.com/vatesfr/xo-server/issues/37))
- new authentication system based on providers ([xo-server#39](https://github.com/vatesfr/xo-server/issues/39))
@@ -51,7 +553,7 @@ them.
- network creation on the VM page ([xo-web#216](https://github.com/vatesfr/xo-web/issues/216))
- charts on the host and SR pages ([xo-web#217](https://github.com/vatesfr/xo-web/issues/217))
## Bug fixes
### Bug fixes
- fix *Invalid parameter(s)* message on the settings page ([xo-server#49](https://github.com/vatesfr/xo-server/issues/49))
- fix mouse clicks in console ([xo-web#205](https://github.com/vatesfr/xo-web/issues/205))

28
ISSUE_TEMPLATE.md Normal file
View File

@@ -0,0 +1,28 @@
<!--
Welcome to the issue section of Xen Orchestra!
Here you can:
- report an issue
- propose an enhancement
- ask a question
The template below is only a proposition for your ticket, feel free to
change it as appropriate :)
-->
### Context
- **XO version**: XO appliance / `stable` branch / `next-release` branch
If from the sources:
- **Component**: xo-web / xo-server / *unknown*
- **Node/npm version**: *just execute `npm version`*
### Expected behavior
<!-- What you expect to happen -->
### Actual behavior
<!-- What is actually happening -->

View File

@@ -1,5 +1,7 @@
# Xen Orchestra Web
![](http://i.imgur.com/tRffA5y.png)
XO-Web is part of [Xen Orchestra](https://github.com/vatesfr/xo), a web interface for XenServer or XAPI enabled hosts.
It is a web client for [XO-Server](https://github.com/vatesfr/xo-server).
@@ -31,13 +33,14 @@ $ npm run dev
If you are certain the bug is exclusively related to XO-Web, you may use the [bugtracker of this repository](https://github.com/vatesfr/xo-web/issues).
Otherwise, please consider using the [bugtracker of the general repository](https://github.com/vatesfr/xo/issues).
## 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
# Merge changes of the next-release branch.
git merge next-release
@@ -48,12 +51,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 pull --fast-forward master
git merge --ff-only stable
# Push the changes on git.
git push origin master:master next-release:next-release
git push --follow-tags origin stable next-release
# Publish this release to npm.
npm publish

View File

@@ -1,39 +1,39 @@
// Must be loaded before angular.
import 'angular-file-upload';
import angular from 'angular'
import angularChartJs from 'angular-chart.js'
import uiBootstrap from'angular-ui-bootstrap'
import uiIndeterminate from'angular-ui-indeterminate'
import uiRouter from'angular-ui-router'
import uiSelect from'angular-ui-select'
import angular from 'angular';
import uiBootstrap from'angular-ui-bootstrap';
import uiIndeterminate from'angular-ui-indeterminate';
import uiRouter from'angular-ui-router';
import uiSelect from'angular-ui-select';
import naturalSort from 'angular-natural-sort'
import xeditable from 'angular-xeditable'
import naturalSort from 'angular-natural-sort';
import xeditable from 'angular-xeditable';
import xoDirectives from 'xo-directives'
import xoFilters from 'xo-filters'
import xoServices from 'xo-services'
import xoDirectives from 'xo-directives';
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 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 selfState from './modules/self'
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'
import aboutState from './modules/about';
import consoleState from './modules/console';
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 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 treeState from './modules/tree';
import vmState from './modules/vm';
import isoDevice from './modules/iso-device';
import '../dist/bower_components/angular-chart.js/dist/angular-chart.js';
//====================================================================
// ===================================================================
export default angular.module('xoWebApp', [
uiBootstrap,
@@ -41,6 +41,7 @@ export default angular.module('xoWebApp', [
uiRouter,
uiSelect,
angularChartJs,
naturalSort,
xeditable,
@@ -49,28 +50,31 @@ export default angular.module('xoWebApp', [
xoServices,
aboutState,
backupState,
consoleState,
dashboardState,
deleteVmsState,
genericModalState,
hostState,
listState,
loginState,
migrateVmState,
navbarState,
newSrState,
newVmState,
poolState,
selfState,
settingsState,
srState,
taskScheduler,
treeState,
vmState,
isoDevice,
'chart.js'
updater,
vmState
])
// 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 (
@@ -86,96 +90,123 @@ export default angular.module('xoWebApp', [
// the console.
//
// See https://docs.angularjs.org/guide/production
$compileProvider.debugInfoEnabled(false);
$compileProvider.debugInfoEnabled(false)
// Redirect to default state.
$stateProvider.state('index', {
url: '/',
controller: function ($state, xoApi) {
let isAdmin = xoApi.user && (xoApi.user.permission === 'admin');
let isAdmin = xoApi.user && (xoApi.user.permission === 'admin')
$state.go(isAdmin ? 'tree' : 'list');
},
});
$state.go(isAdmin ? 'tree' : 'list')
}
})
// Redirects unmatched URLs to `/`.
$urlRouterProvider.otherwise('/');
$urlRouterProvider.otherwise('/')
// Changes the default settings for the tooltips.
$tooltipProvider.options({
appendToBody: true,
placement: 'bottom',
});
placement: 'bottom'
})
uiSelectConfig.theme = 'bootstrap';
uiSelectConfig.resetSearchInput = true;
uiSelectConfig.theme = 'bootstrap'
uiSelectConfig.resetSearchInput = true
})
.run(function (
$anchorScroll,
$cookies,
$rootScope,
$state,
editableOptions,
editableThemes,
modal,
notify,
xoApi,
xo
updater,
xoApi
) {
$rootScope.$on('$stateChangeStart', function (event, state, stateParams) {
let {user} = xoApi;
let loggedIn = !!user;
// Milliseconds are not necessary.
const now = Math.floor(Date.now() / 1e3)
const oneWeekAgo = now - 7 * 24 * 3600
const previousDisclaimer = $cookies.get('previousDisclaimer')
if (
!previousDisclaimer ||
+previousDisclaimer < oneWeekAgo
) {
modal.alert({
title: 'Xen Orchestra from the sources',
htmlMessage: [
'You are using XO from the sources! That\'s great for a personal/non-profit usage.',
'If you are a company, it\'s better to use it with <a href="https://xen-orchestra.com/#!/xoa">XOA (turnkey appliance)</a> and our dedicated pro support!',
'This version is <strong>not bundled with any support nor updates</strong>. Use it with caution for critical tasks.'
].map(p => `<p>${p}</p>`).join('')
})
$cookies.put('previousDisclaimer', now)
}
if (state.name === 'login') {
if (loggedIn) {
event.preventDefault();
$state.go('index');
let requestedStateName, requestedStateParams
$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) {
event.preventDefault();
$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;
return
}
if (user.permission === 'admin') {
return;
return
}
// The user must have the `admin` permission to access the
// settings pages.
if (/^settings\..*|tree$/.test(state.name)) {
event.preventDefault();
function forbidState () {
event.preventDefault()
notify.error({
title: 'Restricted area',
message: 'You do not have the permission to view this page',
});
message: 'You do not have the permission to view this page'
})
if (fromState.url === '^') {
$state.go('index')
}
}
let {id} = stateParams;
if (id && !xo.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
}
})
// Work around UI Router bug (https://github.com/angular-ui/ui-router/issues/1509)
$rootScope.$on('$stateChangeSuccess', function () {
$anchorScroll();
});
$anchorScroll()
})
editableThemes.bs3.inputClass = 'input-sm';
editableThemes.bs3.buttonsClass = 'btn-sm';
editableOptions.theme = 'bs3';
editableThemes.bs3.inputClass = 'input-sm'
editableThemes.bs3.buttonsClass = 'btn-sm'
editableOptions.theme = 'bs3'
})
.name
;

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

@@ -46,7 +46,6 @@ html.no-js(lang="en", dir="ltr")
//- Place favicon.ico and apple-touch-icon.png in the root directory
link(rel="stylesheet", href="styles/main.css")
link(rel="stylesheet", href="bower_components/angular-chart.js/dist/angular-chart.css")
body(
ng-app = 'xoWebApp'
)
@@ -59,5 +58,4 @@ html.no-js(lang="en", dir="ltr")
//- Main content (managed by the router).
.view-main(ui-view = "")
script(src="bower_components/Chart.js/Chart.min.js")
script(src="app.js")

View File

@@ -1,23 +1,25 @@
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import pkg from '../../../package';
import pkg from '../../../package'
//====================================================================
// ===================================================================
module.exports = angular.module('xoWebApp.about', [
uiRouter,
export default angular.module('xoWebApp.about', [
uiRouter
])
.config(function ($stateProvider) {
$stateProvider.state('about', {
url: '/about',
controller: 'AboutCtrl',
template: require('./view'),
});
template: require('./view')
})
})
.controller('AboutCtrl', function ($scope) {
$scope.pkg = pkg;
.controller('AboutCtrl', function ($scope, xo) {
xo.system.getServerVersion().then(version =>
$scope.serverVersion = version
)
$scope.pkg = pkg
})
// A module exports its name.
.name
;

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
p.text-center ({{pkg.name}} {{pkg.version}} - xo-server {{serverVersion}})
.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,275 @@
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.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 => {
remote = parse(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 _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
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._reportWhen = _reportWhen
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, _reportWhen) => {
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, _reportWhen) : save(id, vms, remoteId, tag, depth, cron, onlyMetadata, disableCompression, _reportWhen)
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, _reportWhen) => {
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,
_reportWhen
})
})
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, _reportWhen) => {
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
remoteId,
tag,
depth,
onlyMetadata,
compress: !disableCompression,
_reportWhen
})
})
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._reportWhen = undefined
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,154 @@
.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, ctrl.formData._reportWhen)')
.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 | map | orderBy:["type","name"]', 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
| &nbsp; 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 immediately after creation
.form-group
label.control-label.col-md-2(for = '_reportWhen') Report
.col-md-10
select.form-control(ng-model = 'ctrl.formData._reportWhen')
option(value = ''): em -- When to send reports --
option(value = 'never') Never
option(value = 'alway') Always
option(value = 'fail') Failure
.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
| &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.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 | map | 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
strong: a(ui-sref = 'backup.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
| &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,225 @@
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 _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
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.formData._reportWhen = _reportWhen
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, tag, sr, cron, enabled, _reportWhen) => {
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, _reportWhen) : save(id, vms, tag, sr, cron, _reportWhen)
return _save
.then(() => {
notify.info({
title: 'Continuous Replication',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(refresh)
}
const save = (id, vms, tag, sr, cron, _reportWhen) => {
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, _reportWhen})
})
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, _reportWhen) => {
const values = []
forEach(vms, vm => {
values.push({vm: vm.id, tag, sr: sr.id, _reportWhen})
})
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.formData._reportWhen = undefined
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,143 @@
.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, ctrl.formData._reportWhen)')
.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
| &nbsp;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
.form-group
label.control-label.col-md-2(for = '_reportWhen') Report
.col-md-10
select.form-control(ng-model = 'ctrl.formData._reportWhen')
option(value = ''): em -- When to send reports --
option(value = 'never') Never
option(value = 'alway') Always
option(value = 'fail') Failure
.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
| &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 Copy
th.hidden-xs To SR
th.hidden-xs Scheduling
th Enabled now
th
tr(ng-repeat = 'schedule in ctrl.schedules | map | 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&nbsp;
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') &nbsp;({{ ((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
| &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,262 @@
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)
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 _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
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._reportWhen = _reportWhen
this.formData.remote = this.remotes[remoteId]
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, remoteId, tag, depth, cron, enabled, _reportWhen) => {
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, _reportWhen) : save(id, vms, remoteId, tag, depth, cron, _reportWhen)
return _save
.then(() => {
notify.info({
title: 'Backup',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(refresh)
}
const save = (id, vms, remoteId, tag, depth, cron, _reportWhen) => {
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,
_reportWhen
})
})
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, _reportWhen) => {
const values = []
forEach(vms, vm => {
values.push({
vm: vm.id,
remote: remoteId,
tag,
depth,
_reportWhen
})
})
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._reportWhen = undefined
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,140 @@
.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, ctrl.formData._reportWhen)')
.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 | map | orderBy:["type","name"]', 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
| &nbsp; 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
.form-group
label.control-label.col-md-2(for = '_reportWhen') Report
.col-md-10
select.form-control(ng-model = 'ctrl.formData._reportWhen')
option(value = ''): em -- When to send reports --
option(value = 'never') Never
option(value = 'alway') Always
option(value = 'fail') Failure
.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
| &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.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 | map | 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&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.vm | resolve') {{ (item.vm | resolve).name_label }}
span(ng-if = '(item.vm | resolve).$container') &nbsp;({{ ((item.vm | resolve).$container | resolve).name_label }})
td.hidden-xs
strong: a(ui-sref = 'backup.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
| &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,227 @@
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 _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
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.formData._reportWhen = _reportWhen
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, tag, pool, depth, cron, enabled, _reportWhen) => {
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, _reportWhen) : save(id, vms, tag, pool, depth, cron, _reportWhen)
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, _reportWhen) => {
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, _reportWhen})
})
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, _reportWhen) => {
const values = []
forEach(vms, vm => {
values.push({id: vm.id, tag, pool: pool.id, depth, _reportWhen})
})
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.formData._reportWhen = undefined
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,153 @@
.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, ctrl.formData._reportWhen)')
.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
| &nbsp;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
.form-group
label.control-label.col-md-2(for = '_reportWhen') Report
.col-md-10
select.form-control(ng-model = 'ctrl.formData._reportWhen')
option(value = ''): em -- When to send reports --
option(value = 'never') Never
option(value = 'alway') Always
option(value = 'fail') Failure
.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
| &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 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 | map | 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&nbsp;
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') &nbsp;({{ ((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
| &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,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,231 @@
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 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.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 => {
schedules = filter(schedules, schedule => this.jobs[schedule.job] && this.jobs[schedule.job].key in mapJobKeyToState)
this.schedules = this.schedules ? map(schedules, schedule => {
schedule.error = find(this.schedules, oldSchedule => schedule.id === oldSchedule.id).error
return schedule
}) : schedules
})
xo.scheduler.getScheduleTable()
.then(table => this.scheduleTable = table)
xo.remote.getAll()
.then(remotes => {
this.backUpRemotes = map(remotes, parse)
forEach(this.schedules, schedule => {
const jobRemote = this.jobs[schedule.job].paramsVector.items[0].values[0]
const key = this.jobs[schedule.job].key
// TODO: Why is the property either 'remote' or 'remoteId'?
const remoteId = jobRemote.remoteId || jobRemote.remote
const remote = find(remotes, remote => remote.id === remoteId)
schedule.error = (!remote || !remote.enabled) && key !== 'continuousReplication' && key !== 'disasterRecovery' && key !== 'rollingSnapshot'
})
})
}
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,90 @@
.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.btn-danger(ui-sref = 'backup.remote' type = 'button' ng-if = 'schedule.error'): i.fa.fa-exclamation-triangle
| &nbsp;
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
| &nbsp;
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}')
| &nbsp;{{ 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') &nbsp;{{ 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 }}:&#32;
span(ng-repeat = '(key, param) in call.params')
strong {{ key }}:
| &nbsp;{{ param }}&nbsp;
span(ng-if = 'call.returnedValue')
| &nbsp;
i.text-primary.fa.fa-arrow-right
| &nbsp;{{ call.returnedValue }}
span.text-danger(ng-if = 'call.error')
| &nbsp;
i.fa.fa-times
| &nbsp;{{ 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, 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,247 @@
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 _reportWhen = job.paramsVector.items[0].values[0]._reportWhen
const cronPattern = schedule.cron
this.resetData()
// const formData = this.formData
this.formData.selectedVms = selectedVms
this.formData.tag = tag
this.formData.depth = depth
this.formData._reportWhen = _reportWhen
this.formData.scheduleId = schedule.id
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, tag, depth, cron, enabled, _reportWhen) => {
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, _reportWhen) : save(id, vms, tag, depth, cron, _reportWhen)
return _save
.then(() => {
notify.info({
title: 'Rolling snapshot',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(() => {
refresh()
})
}
const save = (id, vms, tag, depth, cron, _reportWhen) => {
const schedule = this.schedules[id]
const job = this.jobs[schedule.job]
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
tag,
depth,
_reportWhen
})
})
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, _reportWhen) => {
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
tag,
depth,
_reportWhen
})
})
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.formData._reportWhen = undefined
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,127 @@
.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, ctrl.formData._reportWhen)')
.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
.form-group
label.control-label.col-md-2(for = '_reportWhen') Report
.col-md-10
select.form-control(ng-model = 'ctrl.formData._reportWhen')
option(value = ''): em -- When to send reports --
option(value = 'never') Never
option(value = 'alway') Always
option(value = 'fail') Failure
.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 | map | 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,29 +1,51 @@
angular = require 'angular'
forEach = require('lodash.foreach')
includes = require('lodash.includes')
Clipboard = require('clipboard')
isoDevice = require('iso-device').default
#=====================================================================
module.exports = angular.module 'xoWebApp.console', [
require 'angular-ui-router'
require('angular-no-vnc').default
require 'angular-no-vnc'
isoDevice
]
.config ($stateProvider) ->
$stateProvider.state 'consoles_view',
url: '/consoles/:id'
controller: 'ConsoleCtrl'
template: require './view'
.controller 'ConsoleCtrl', ($scope, $stateParams, xoApi, xo) ->
.controller 'ConsoleCtrl', ($scope, $stateParams, xoApi, xo, xoHideUnauthorizedFilter, modal) ->
{id} = $stateParams
{get} = xoApi
push = Array::push.apply.bind Array::push
merge = do ->
(args...) ->
result = []
for arg in args
push result, arg if arg?
result
pool = null
host = null
do (
srsByContainer = xoApi.getIndex('srsByContainer')
poolSrs = null
hostSrs = null
) ->
updateSrs = () =>
srs = []
poolSrs and forEach(poolSrs, (sr) => srs.push(sr))
hostSrs and forEach(hostSrs, (sr) => srs.push(sr))
$scope.SRs = xoHideUnauthorizedFilter(srs)
$scope.$watchCollection(
() => pool and srsByContainer[pool.id],
(srs) =>
poolSrs = srs
updateSrs()
)
$scope.$watchCollection(
() => host and srsByContainer[host.id],
(srs) =>
hostSrs = srs
updateSrs()
)
$scope.$watch(
-> xoApi.get id
@@ -41,41 +63,57 @@ module.exports = angular.module 'xoWebApp.console', [
not includes(VM.current_operations, 'clean_reboot')
)
pool = get VM.poolRef
pool = get VM.$poolId
return unless pool
$scope.consoleUrl = "/consoles/#{id}"
$scope.consoleUrl = "./api/consoles/#{id}"
host = get VM.$container # host because the VM is running.
return unless host
# FIXME: We should filter on connected SRs (PBDs)!
SRs = get (merge host.SRs, pool.SRs)
$scope.VDIs = do ->
VDIs = []
for SR in SRs
push VDIs, SR.VDIs if SR.content_type is 'iso'
get VDIs
cdDrive = do ->
return VBD for VBD in (get VM.$VBDs) when VBD.is_cd_drive
null
$scope.mountedIso =
if cdDrive and cdDrive.VDI and (VDI = get cdDrive.VDI)
VDI.UUID
else
''
)
$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,59 +7,84 @@
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.UUID})'
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: .input-group
select.form-control(
ng-model = 'mountedIso'
ng-change = 'insert(mountedIso)'
ng-options = 'VDI.UUID as VDI.name_label group by (VDI.$SR | resolve).name_label for VDI in VDIs | orderBy:natural("name_label")'
)
.input-group-btn
button.btn.btn-default(
ng-click = 'eject()'
ng-disabled = '!mountedIso'
)
i.fa.fa-eject
.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"
type = "button"
xo-click = "stopVM(VM.UUID)"
xo-click = "stopVM(VM.id)"
)
i.fa.fa-stop.fa-fw
button.btn.btn-default.inversed(
ng-if = "VM.power_state == ('Halted')"
tooltip = "Start VM"
type = "button"
xo-click = "startVM(VM.UUID)"
xo-click = "startVM(VM.id)"
)
i.fa.fa-play.fa-fw
button.btn.btn-default.inversed(
ng-if = "VM.power_state == ('Running' || 'Paused')"
tooltip = "Reboot VM"
type = "button"
xo-click = "rebootVM(VM.UUID)"
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.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 }}
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 }}

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 | map | 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 | map | 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 | map | 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 | map | 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 | map | 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 | map | 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

@@ -1,16 +1,19 @@
import angular from 'angular';
import uiBootstrap from 'angular-ui-bootstrap';
// TODO: should be integrated xo.deleteVms()
import xoServices from 'xo-services';
import angular from 'angular'
import forEach from 'lodash.foreach'
import uiBootstrap from 'angular-ui-bootstrap'
import view from './view';
import xoServices from 'xo-services'
//====================================================================
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.deleteVms', [
uiBootstrap,
xoServices,
xoServices
])
.controller('DeleteVmsCtrl', function (
$scope,
@@ -19,23 +22,23 @@ export default angular.module('xoWebApp.deleteVms', [
VMsIds
) {
$scope.$watchCollection(() => xoApi.get(VMsIds), function (VMs) {
$scope.VMs = VMs;
});
$scope.VMs = VMs
})
// Do disks have to be deleted for a given VM.
let disks = $scope.disks = {};
angular.forEach(VMsIds, id => {
disks[id] = true;
});
let disks = $scope.disks = {}
forEach(VMsIds, id => {
disks[id] = true
})
$scope.delete = function () {
let value = [];
angular.forEach(VMsIds, id => {
value.push([id, disks[id]]);
});
let value = []
forEach(VMsIds, id => {
value.push([id, disks[id]])
})
$modalInstance.close(value);
};
$modalInstance.close(value)
}
})
.service('deleteVmsModal', function ($modal, xo) {
return function (ids) {
@@ -46,16 +49,15 @@ export default angular.module('xoWebApp.deleteVms', [
VMsIds: () => ids
}
}).result.then(function (toDelete) {
let promises = [];
let promises = []
angular.forEach(toDelete, ([id, deleteDisks]) => {
promises.push(xo.vm.delete(id, deleteDisks));
});
forEach(toDelete, ([id, deleteDisks]) => {
promises.push(xo.vm.delete(id, deleteDisks))
})
return promises;
});
};
return promises
})
}
})
// A module exports its name.
.name
;

View File

@@ -12,11 +12,14 @@ form(ng-submit="delete()")
th.col-sm-6 Description
th.col-sm-3 Delete disks?
tbody
tr(ng-repeat="VM in VMs | orderBy:natural('name_label') track by VM.UUID")
tr(ng-repeat="VM in VMs | orderBy:natural('name_label') track by VM.id")
td {{VM.name_label}}
td {{VM.name_description}}
td
input(type="checkbox", ng-model="disks[VM.UUID]")
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

@@ -1,40 +1,52 @@
import angular from 'angular';
import uiBootstrap from 'angular-ui-bootstrap';
import angular from 'angular'
import uiBootstrap from 'angular-ui-bootstrap'
//====================================================================
import template from './view'
// ===================================================================
export default angular.module('xoWebApp.genericModal', [
uiBootstrap,
uiBootstrap
])
.controller('GenericModalCtrl', function ($scope, $modalInstance, options) {
$scope.title = options.title;
$scope.message = options.message;
.controller('GenericModalCtrl', function ($modalInstance, $sce, options) {
const {
htmlMessage,
message,
noButtonLabel = undefined,
title,
yesButtonLabel = 'Ok'
} = options
$scope.yesButtonLabel = options.yesButtonLabel || 'Ok';
$scope.noButtonLabel = options.noButtonLabel;
this.title = title
this.message = message
this.htmlMessage = htmlMessage && $sce.trustAsHtml(htmlMessage)
this.yesButtonLabel = yesButtonLabel
this.noButtonLabel = noButtonLabel
})
.service('modal', function ($modal) {
return {
confirm: function (opts) {
var modal = $modal.open({
controller: 'GenericModalCtrl',
template: require('./view'),
resolve: {
options: function () {
return {
title: opts.title,
message: opts.message,
noButtonLabel: 'Cancel',
};
},
},
});
return modal.result;
}
};
alert: ({ title, htmlMessage, message }) => $modal.open({
controller: 'GenericModalCtrl as $ctrl',
template,
resolve: {
options: () => ({ title, htmlMessage, message })
}
}).result,
confirm: ({ title, htmlMessage, message }) => $modal.open({
controller: 'GenericModalCtrl as $ctrl',
template,
resolve: {
options: () => ({
title,
htmlMessage,
message,
noButtonLabel: 'Cancel'
})
}
}).result
}
})
// A module exports its name.
.name
;

View File

@@ -1,11 +1,11 @@
.modal-header
h3
i.fa.fa-exclamation-triangle.text-danger
| {{title}}
.modal-body
| {{message}}
| {{$ctrl.title}}
.modal-body(ng-if = "$ctrl.htmlMessage", ng-bind-html = "$ctrl.htmlMessage")
.modal-body(ng-if = "!$ctrl.htmlMessage") {{$ctrl.message}}
.modal-footer
button.btn.btn-primary(type="button", ng-click="$close()")
| {{yesButtonLabel}}
button.btn.btn-warning(ng-if="noButtonLabel", type="button", ng-click="$dismiss()")
| {{noButtonLabel}}
| {{$ctrl.yesButtonLabel}}
button.btn.btn-warning(ng-if="$ctrl.noButtonLabel", type="button", ng-click="$dismiss()")
| {{$ctrl.noButtonLabel}}

View File

@@ -1,11 +1,19 @@
angular = require 'angular'
forEach = require 'lodash.foreach'
intersection = require 'lodash.intersection'
map = require 'lodash.map'
omit = require 'lodash.omit'
sum = require 'lodash.sum'
throttle = require 'lodash.throttle'
find = require 'lodash.find'
filter = require 'lodash.filter'
#=====================================================================
module.exports = angular.module 'xoWebApp.host', [
require 'angular-file-upload'
require 'angular-ui-router'
require('ng-file-upload')
require('tag').default
]
.config ($stateProvider) ->
$stateProvider.state 'hosts_view',
@@ -13,20 +21,103 @@ module.exports = angular.module 'xoWebApp.host', [
controller: 'HostCtrl'
template: require './view'
.controller 'HostCtrl', (
$scope, $stateParams
$upload
$scope, $stateParams, $http
$timeout
$window
dateFilter
Upload
xoApi, xo, modal, notify, bytesToSizeFilter
) ->
do (
hostId = $stateParams.id
controllers = xoApi.getIndex('vmControllersByContainer')
poolPatches = xoApi.getIndex('poolPatchesByPool')
srs = xoApi.getIndex('srsByContainer')
tasks = xoApi.getIndex('runningTasksByHost')
vms = xoApi.getIndex('vmsByContainer')
) ->
Object.defineProperties($scope, {
controller: {
get: () => controllers[hostId]
},
poolPatches: {
get: () => $scope.host && poolPatches[$scope.host.$poolId]
},
sharedSrs: {
get: () => $scope.host && srs[$scope.host.$poolId]
},
srs: {
get: () => srs[hostId]
},
tasks: {
get: () => tasks[hostId]
},
vms: {
get: () => vms[hostId]
}
})
$window.bytesToSize = bytesToSizeFilter # FIXME dirty workaround to custom a Chart.js tooltip template
host = null
$scope.currentPatchPage = 1
$scope.currentLogPage = 1
$scope.currentPCIPage = 1
$scope.currentGPUPage = 1
$scope.currentLicensePage = 1
$scope.refreshStatControl = refreshStatControl = {
baseStatInterval: 5000
baseTimeOut: 10000
period: null
running: false
attempt: 0
start: () ->
return if this.running
this.stop()
this.running = true
this._reset()
$scope.$on('$destroy', () => this.stop())
return this._trig(Date.now())
_trig: (t1) ->
if this.running
timeoutSecurity = $timeout(
() => this.stop(),
this.baseTimeOut
)
return $scope.refreshStats($scope.host.id)
.then () => this._reset()
.catch (err) =>
if !this.running || this.attempt >= 2 || $scope.host.power_state isnt 'Running' || $scope.isVMWorking($scope.host)
return this.stop()
else
this.attempt++
.finally () =>
$timeout.cancel(timeoutSecurity)
if this.running
t2 = Date.now()
return this.period = $timeout(
() => this._trig(t2),
Math.max(this.baseStatInterval - (t2 - t1), 0)
)
_reset: () ->
this.attempt = 0
stop: () ->
if this.period
$timeout.cancel(this.period)
this.running = false
return
}
$scope.$watch(
-> xoApi.get $stateParams.id
(host) ->
$scope.host = host
return unless host?
$scope.pool = xoApi.get host.poolRef
$scope.hostParams = Object.getOwnPropertyNames(host.license_params)
pool = $scope.pool = xoApi.get host.$poolId
SRsToPBDs = $scope.SRsToPBDs = Object.create null
for PBD in host.$PBDs
@@ -36,9 +127,17 @@ module.exports = angular.module 'xoWebApp.host', [
continue unless PBD
SRsToPBDs[PBD.SR] = PBD
$scope.listMissingPatches($scope.host.id)
if host.power_state is 'Running'
refreshStatControl.start()
else
refreshStatControl.stop()
)
$scope.removeMessage = xo.message.delete
$scope.$watch('vms', (vms) =>
$scope.vCPUs = sum(map(vms, (vm) => +vm.CPUs.number))
)
$scope.cancelTask = (id) ->
modal.confirm({
@@ -62,12 +161,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'
@@ -108,19 +217,32 @@ 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
$data = {
id: host.UUID
id: host.id
}
if name_label isnt host.name_label
$data.name_label = name_label
if name_description isnt host.name_description
$data.name_description = name_description
if enabled isnt host.enabled
$data.enabled = host.enabled
if host.enabled
$scope.disableHost($data.id)
else
$scope.enableHost($data.id)
# enabled is not set via the "set" method, so we remove it before send it
delete $data.enabled
xoApi.call 'host.set', $data
@@ -129,43 +251,44 @@ 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
console.log "Remove log #{log}"
xo.log.delete log
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}"
xo.log.delete id
$scope.connectPBD = (UUID) ->
console.log "Connect PBD #{UUID}"
$scope.connectPBD = (id) ->
console.log "Connect PBD #{id}"
xoApi.call 'pbd.connect', {id: UUID}
xoApi.call 'pbd.connect', {id: id}
$scope.disconnectPBD = (UUID) ->
console.log "Disconnect PBD #{UUID}"
$scope.disconnectPBD = (id) ->
console.log "Disconnect PBD #{id}"
xoApi.call 'pbd.disconnect', {id: UUID}
xoApi.call 'pbd.disconnect', {id: id}
$scope.removePBD = (UUID) ->
console.log "Remove PBD #{UUID}"
$scope.removePBD = (id) ->
console.log "Remove PBD #{id}"
xoApi.call 'pbd.delete', {id: UUID}
xoApi.call 'pbd.delete', {id: id}
$scope.connectPIF = (UUID) ->
console.log "Connect PIF #{UUID}"
$scope.connectPIF = (id) ->
console.log "Connect PIF #{id}"
xoApi.call 'pif.connect', {id: UUID}
xoApi.call 'pif.connect', {id: id}
$scope.disconnectPIF = (UUID) ->
console.log "Disconnect PIF #{UUID}"
$scope.disconnectPIF = (id) ->
console.log "Disconnect PIF #{id}"
xoApi.call 'pif.disconnect', {id: UUID}
xoApi.call 'pif.disconnect', {id: id}
$scope.removePIF = (UUID) ->
console.log "Remove PIF #{UUID}"
$scope.removePIF = (id) ->
console.log "Remove PIF #{id}"
xoApi.call 'pif.delete', {id: UUID}
xoApi.call 'pif.delete', {id: id}
$scope.importVm = ($files, id) ->
file = $files[0]
@@ -176,7 +299,7 @@ module.exports = angular.module 'xoWebApp.host', [
xo.vm.import id
.then ({ $sendTo: url }) ->
return $upload.http {
return Upload.http {
method: 'POST'
url
data: file
@@ -196,7 +319,7 @@ module.exports = angular.module 'xoWebApp.host', [
}
params = {
host: $scope.host.UUID
pool: $scope.host.$pool
name,
}
@@ -205,10 +328,139 @@ module.exports = angular.module 'xoWebApp.host', [
if vlan then params.vlan = vlan
if description then params.description = description
xoApi.call 'host.createNetwork', params
xoApi.call 'network.create', params
.then ->
$scope.creatingNetwork = false
$scope.createNetworkWaiting = false
$scope.addIp = (pif, ip, netmask, dns, gateway, ipMethod) ->
notify.info {
title: 'IP configuration...'
message: 'Configuring new IP mode'
}
xoApi.call('pif.reconfigureIp', {
id: pif.id,
mode: ipMethod,
ip,
netmask,
dns,
gateway
})
$scope.physicalPifs = () ->
physicalPifs = []
forEach $scope.host.$PIFs, (pif) ->
pif = xoApi.get(pif)
if pif.physical
physicalPifs.push pif.id
return physicalPifs
$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)
return false if not hostPatch.length
hostPatch = xoApi.get(hostPatch[0])
return hostPatch.applied
$scope.listMissingPatches = (id) ->
return xo.host.listMissingPatches id
.then (result) ->
$scope.updates = omit(result,map($scope.poolPatches,'id'))
$scope.installPatch = (id, patchUid) ->
console.log("Install patch "+patchUid+" on "+id)
notify.info {
title: 'Patch host'
message: "Patching the host, please wait..."
}
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.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.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,
ramOnly: false,
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,20 +1,22 @@
.grid
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-host(class="xo-color-{{host.power_state | lowercase}}")
| {{host.name_label}}
small(ng-if="pool.name_label")
| (
a(ui-sref="pools_view({id: pool.UUID})") {{pool.name_label}}
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 Edit", ng-click="hostSettings.$cancel()")
i.fa.fa-undo.fa-fw
.panel-body
form(editable-form="", name="hostSettings", onbeforesave="saveHost($data)")
dl.dl-horizontal
@@ -28,14 +30,11 @@
| {{host.name_description}}
dt Enabled
dd
span(editable-checkbox="host.enabled", e-name="enabled", e-form="hostSettings")
| {{host.enabled}}
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
@@ -45,6 +44,12 @@
dd {{host.UUID}}
dt iQN
dd {{host.iSCSI_name}}
dt(ng-if="refreshStatControl.running && stats") vCPUs/CPUs:
dd(ng-if="refreshStatControl.running && stats") {{vCPUs}}/{{host.CPUs['cpu_count']}}
dt(ng-if="refreshStatControl.running && stats") Running VMs:
dd(ng-if="refreshStatControl.running && stats") {{vms | count}}
dt(ng-if="refreshStatControl.running && stats") RAM (used/free):
dd(ng-if="refreshStatControl.running && stats") {{host.memory.usage | bytesToSize}}/{{host.memory.size | bytesToSize}}
.btn-form(ng-show="hostSettings.$visible")
p.center
button.btn.btn-default(type="button", ng-disabled="hostSettings.$waiting", ng-click="hostSettings.$cancel()")
@@ -56,94 +61,231 @@
| Save
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-stats(style="color: #e25440;")
i.xo-icon-stats
| Stats
.panel-body
.grid
.grid-cell
.panel-body(ng-if="refreshStatControl.running && stats")
div(ng-if="statView.cpuOnly", ng-click="statView.cpuOnly = false")
p.stat-name
i.xo-icon-cpu
| &nbsp; CPU usage
canvas.chart.chart-line.chart-stat-full(
id="bigCpu"
data="stats.cpus"
labels="stats.date"
series="stats.cpuSeries"
colours="['#0000ff', '#9999ff', '#000099', '#5555ff', '#000055']"
legend="true"
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= Math.round(10*value)/10 %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= Math.round(10*value)/10 %>", pointDot: false, showScale: false, animation: false, datasetStrokeWidth: 0.8, scaleOverride: true, scaleSteps: 100, scaleStartValue: 0, scaleStepWidth: 1, pointHitDetectionRadius: 0}'
)
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-server
| &nbsp; RAM usage
canvas.chart.chart-line.chart-stat-full(
id="bigRam"
data="[stats.memoryUsed,stats.memory]"
labels="stats.date"
series="['Used RAM', 'Total RAM']"
colours="['#ff0000', '#ffbbbb']"
legend="true"
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
)
div(ng-if="statView.netOnly", ng-click="statView.netOnly = false")
p.stat-name
i.xo-icon-network
| &nbsp; Network I/O
canvas.chart.chart-line.chart-stat-full(
id="bigNet"
data="stats.pifs"
labels="stats.date"
series="stats.pifSeries"
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
legend="true"
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
)
div(ng-if="statView.loadOnly", ng-click="statView.loadOnly = false")
p.stat-name
i.fa.fa-cogs
| &nbsp; Load Average
canvas.chart.chart-line.chart-stat-full(
id="bigLoad"
data="[stats.load]"
labels="stats.date"
series="['Load']"
colours="['#960094']"
legend="true"
options='{responsive: true, maintainAspectRatio: false, multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
)
div(ng-if="!statView.netOnly && !statView.loadOnly && !statView.cpuOnly && !statView.ramOnly")
.row
.col-md-6(ng-click="statView.cpuOnly=true")
p.stat-name
i.xo-icon-cpu
| &nbsp; CPU usage
canvas.chart.chart-line.chart-stat-preview(
id="smallCpu"
data="stats.cpus"
labels="stats.date"
series="stats.cpuSeries"
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-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-server
| &nbsp; RAM usage
canvas.chart.chart-line.chart-stat-preview(
id="smallRam"
data="[stats.memoryUsed,stats.memory]"
labels="stats.date"
series="['Used RAM', 'Total RAM']"
colours="['#ff0000', '#ffbbbb']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.row
.col-md-6(ng-click="statView.netOnly=true")
p.stat-name
i.xo-icon-network
| &nbsp; Network I/O
canvas.chart.chart-line.chart-stat-preview(
id="smallNet"
data="stats.pifs"
labels="stats.date"
series="stats.pifSeries"
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-md-6(ng-click="statView.loadOnly=true")
p.stat-name
i.fa.fa-cogs
| &nbsp; Load Average
canvas.chart.chart-line.chart-stat-preview(
id="smallDisk"
data="[stats.load]"
labels="stats.date"
series="['Load']"
colours="['#960094']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.panel-body(ng-if="!refreshStatControl.running || !stats")
.row
.col-sm-4
p.stat-name CPU usage:
canvas(
id="bar"
class="chart chart-bar"
data="[[host.$vCPUs], [host.CPUs['cpu_count']]]"
labels="['']"
series="['vCPUs','CPUs']"
options="{scaleShowGridLines: false, barDatasetSpacing : 10, showScale: false}"
)
.grid-cell
p.center.mid-stat {{vCPUs}}/{{host.CPUs['cpu_count']}}
.col-sm-4
p.stat-name RAM used:
canvas(id="doughnut", class="chart chart-doughnut", data="[(host.memory.usage), (host.memory.size - host.memory.usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
.grid-cell
p.center.mid-stat {{host.memory.usage | bytesToSize}}
.col-sm-4
p.stat-name Running VMs:
p.center.big-stat {{host.VMs.length}}
p.center.mid-stat {{vms | count}}
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.UUID})")
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.UUID})")
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.UUID)")
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.UUID)")
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.UUID)")
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.UUID)")
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.UUID)")
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.UUID)")
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.UUID)")
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.UUID)'
.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%", ng-repeat="controller in [host.controller] | resolve track by controller.UUID", xo-sref="consoles_view({id: controller.UUID})")
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%"
ngf-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
.progress-bar-host(ng-repeat="controller in [host.controller] | resolve track by controller.UUID", role="progressbar", aria-valuemin="0", aria-valuenow="{{controller.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[controller.memory.size, host.memory.size] | %}}", tooltip="{{host.name_label}}: {{[controller.memory.size, host.memory.size] | %}}")
.progress-bar-host(role="progressbar", aria-valuemin="0", aria-valuenow="{{controller.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[controller.memory.size, host.memory.size] | percentage}}", tooltip="{{host.name_label}}: {{[controller.memory.size, host.memory.size] | percentage}}")
small {{host.name_label}}
.progress-bar.progress-bar-vm(ng-repeat="VM in host.VMs | resolve | orderBy:natural('name_label') track by VM.UUID", role="progressbar", aria-valuemin="0", aria-valuenow="{{VM.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[VM.memory.size, host.memory.size] | %}}", xo-sref="VMs_view({id: VM.UUID})", tooltip="{{VM.name_label}}: {{[VM.memory.size, host.memory.size] | %}}")
.progress-bar.progress-bar-vm(ng-repeat="VM in vms | map | orderBy:natural('name_label') track by VM.id", role="progressbar", aria-valuemin="0", aria-valuenow="{{VM.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[VM.memory.size, host.memory.size] | percentage}}", xo-sref="VMs_view({id: VM.id})", tooltip="{{VM.name_label}}: {{[VM.memory.size, host.memory.size] | percentage}}")
small {{VM.name_label}}
ul.list-inline.text-center
li Total: {{host.memory.size | bytesToSize}}
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
@@ -151,109 +293,151 @@
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)
//- Shared SR
tr(xo-sref="SRs_view({id: SR.UUID})", ng-repeat="SR in pool.SRs | resolve | orderBy:natural('name_label') track by SR.UUID")
td
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in sharedSrs | map | orderBy:natural('name_label') track by SR.id")
td.oneliner
| {{SR.name_label}}
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] | %}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | %}}")
.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] | %}}", tooltip="Allocated: {{[(SR.usage), SR.size] | %}}")
.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.ref].attached")
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.ref].ref)")
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.ref].attached")
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.ref].ref)")
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.ref].ref)")
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
tr(xo-sref="SRs_view({id: SR.UUID})", ng-repeat="SR in host.SRs | resolve | orderBy:natural('name_label') track by SR.UUID")
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in srs | map | orderBy:natural('name_label') track by SR.id")
td
| {{SR.name_label}}
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] | %}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | %}}")
.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] | %}}", tooltip="Allocated: {{[(SR.usage), SR.size] | %}}")
.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.ref].attached")
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.ref].ref)")
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.ref].attached")
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.ref].ref)")
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.ref].ref)")
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;")
| Interfaces
i.xo-icon-network
| PIFs
.panel-body
table.table.table-hover
th.col-md-1 Device
th.col-md-1 Network
th.col-md-1 VLAN
th.col-md-1 Address
th.col-md-2 Address
th.col-md-2 MAC
th.col-md-1 MTU
th.col-md-1 Link status
tr(ng-repeat="PIF in host.$PIFs | resolve | orderBy:natural('name_label') track by PIF.UUID")
td
| {{PIF.device}}
span.label.label-primary(ng-if="PIF.management") XAPI
td
span(ng-if="PIF.vlan > -1")
| {{PIF.vlan}}
span(ng-if="PIF.vlan == -1")
| -
td {{PIF.IP}} ({{PIF.mode}})
td {{PIF.MAC}}
td {{PIF.MTU}}
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.ref)")
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.ref)")
i.fa.fa-link.fa-lg
a(tooltip="Remove this interface", xo-click="removePIF(PIF.ref)")
i.fa.fa-trash-o.fa-lg
th.col-md-2 Link status
tbody(ng-repeat="PIF in host.$PIFs | resolve | orderBy:natural('device') track by PIF.id")
tr
td
| {{PIF.device}}&nbsp;
span.label.label-primary(ng-if="PIF.management") XAPI
| &nbsp;
span.label.label-primary(ng-if="PIF.physical") Phys.
td {{(PIF.$network | resolve).name_label}}
td
span(ng-if="PIF.vlan > -1")
| {{PIF.vlan}}
span(ng-if="PIF.vlan == -1")
| -
td {{PIF.ip}} ({{PIF.mode}})
span.quick-buttons(ng-click="configuringIp = !configuringIp" tooltip="Configure IP")
i.fa.fa-edit.fa-lg
td {{PIF.mac}}
td {{PIF.mtu}}
td
span.label.label-default(ng-if="!PIF.attached") Disconnected
span.label.label-success(ng-if="PIF.attached") Connected
span.pull-right.btn-group.quick-buttons(ng-if="canAdmin()")
i.fa.fa-unlink.fa-lg.text-danger(ng-if="PIF.disallowUnplug" tooltip="Disconnection not allowed")
i.fa.fa-unlink.fa-lg.text-danger(ng-if="!PIF.disallowUnplug && PIF.management" tooltip="Management PIF")
| &nbsp;&nbsp;
i.fa.fa-trash.fa-lg.text-danger(ng-if="PIF.disallowUnplug" tooltip="Disconnection not allowed")
i.fa.fa-trash.fa-lg.text-danger(ng-if="!PIF.disallowUnplug && PIF.management" tooltip="Management PIF")
a(tooltip="Disconnect this interface" xo-click="disconnectPIF(PIF.id)", ng-if = 'PIF.attached && !PIF.disallowUnplug && !PIF.management')
i.fa.fa-unlink.fa-lg
a(tooltip="Connect this interface" xo-click="connectPIF(PIF.id)", ng-if = '!PIF.attached')
i.fa.fa-link.fa-lg
a(tooltip="Remove this interface", xo-click="removePIF(PIF.id)", ng-if = '!PIF.physical && !PIF.disallowUnplug && !PIF.management')
i.fa.fa-trash-o.fa-lg
tr(ng-if="configuringIp")
td(colspan="7")
form.form-inline#configureIpForm(name = 'configureIpForm', ng-submit = 'addIp(PIF, newIp, newNetmask, newDns, newGateway, ipMethod); $parent.configuringIp=false', ng-init='ipMethod="Static"')
label
.form-group
input(type="radio" name="ipMethod" ng-model="ipMethod" value="Static" checked)
.form-group
| &nbsp;
span(for = 'newIp') IP:&nbsp;&nbsp;
input#newIp.form-control(type = 'text', ng-model = 'newIp', placeholder = '{{PIF.ip}}', required, ng-disabled="ipMethod !== 'Static'")
| &nbsp;&nbsp;
span(for = 'newNetmask') Netmask:&nbsp;&nbsp;
input#newNetmask.form-control(type = 'text', ng-model = 'newNetmask', placeholder = '{{PIF.netmask}}', required, ng-disabled="ipMethod !== 'Static'")
| &nbsp;&nbsp;
span(for = 'newDns') DNS:&nbsp;&nbsp;
input#newDns.form-control(type = 'text', ng-model = 'newDns', placeholder = '{{PIF.dns}}', ng-disabled="ipMethod !== 'Static'")
| &nbsp;&nbsp;
span(for = 'newGateway') Gateway:&nbsp;&nbsp;
input#newGateway.form-control(type = 'text', ng-model = 'newGateway', placeholder = '{{PIF.gateway}}', ng-disabled="ipMethod !== 'Static'")
| &nbsp;&nbsp;
br
label
.form-group
input(type="radio" name="ipMethod" ng-model="ipMethod" value="DHCP")
.form-group
| &nbsp;Use DHCP
br
label
.form-group
input(type="radio" name="ipMethod" ng-model="ipMethod" value="None")
.form-group
| &nbsp;Remove IP
br
button.btn.btn-primary(type = 'submit') OK
.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')
option(value = '', disabled) None
select.form-control(ng-model = 'newNetworkPIF', ng-change = 'updateMTU(newNetworkPIF)', ng-options='(PIF | resolve).device for PIF in physicalPifs()')
option(value = '') None
| &nbsp;
.form-group
label.control-label(for = 'newNetworkName') Name&nbsp;
@@ -274,109 +458,162 @@
.form-group
button.btn.btn-primary(type = 'submit')
i.fa.fa-plus-square
| Create
| 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="host.tasks.length")
i.fa.fa-spinner.fa-pulse(style="color: #e25440;")
.panel-heading.panel-title(ng-if="tasks | isNotEmpty")
i.fa.fa-spinner.fa-pulse
| Pending tasks
.panel-heading.panel-title(ng-if="!host.tasks.length")
i.fa.fa-spinner(style="color: #e25440;")
.panel-heading.panel-title(ng-if="tasks | isEmpty")
i.fa.fa-spinner
| Pending tasks
.panel-body
p.center(ng-if="!host.tasks.length") No recent tasks
table.table.table-hover(ng-if="host.tasks.length")
p.center(ng-if="tasks | isEmpty") No recent tasks
table.table.table-hover(ng-if="tasks | isNotEmpty")
th Date
th Progress
th Name
//- TODO: working reverse order, from recent to oldest
tr(ng-repeat="task in host.tasks | resolve | orderBy:'created':true track by task.UUID")
td {{task.created}}
tr(ng-repeat="task in tasks | map | orderBy:'created':true track by task.id")
td.oneliner {{task.created * 1e3 | date:'medium'}}
td
.progress-condensed
.progress-bar.progress-bar-success.progress-bar-striped.active.progress-bar-black(role="progressbar", aria-valuemin="0", aria-valuenow="{{task.progress*100}}", aria-valuemax="100", style="width: {{task.progress*100}}%", tooltip="Progress: {{task.progress*100 | number:1}}%")
| {{task.progress*100 | number:1}}%
td
td.oneliner
| {{task.name_label}}
span.pull-right.btn-group.quick-buttons
a(xo-click="cancelTask(task.UUID)")
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.UUID)")
a(xo-click="destroyTask(task.id)")
i.fa.fa-trash-o.fa-lg(tooltip="Destroy this task")
//- 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.length", 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.length") No recent logs
table.table.table-hover(ng-if="host.messages.length")
p.center(ng-if="host.messages | isEmpty") No recent logs
table.table.table-hover(ng-if="host.messages | isNotEmpty")
th Date
th Name
tr(ng-repeat="message in host.messages | resolve | orderBy:'-time' track by message.UUID")
tr(ng-repeat="message in host.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
td {{message.time*1e3 | date:"medium"}}
td
| {{message.name}}
span.pull-right.btn-group.quick-buttons
a(xo-click="deleteLog(message.UUID)")
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")
.grid
.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-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
p.center(ng-if="!host.patches.length") No patches
table.table.table-hover(ng-if="host.patches.length")
th Applied on
th Name
th Description
th Status
tr(ng-repeat="patch in host.patches | resolve | orderBy:'-time'")
td {{patch.time*1e3 | date:"medium"}}
td {{(patch.pool_patch | resolve).name_label}}
td {{(patch.pool_patch | resolve).name_description}}
//- TODO: allow patch application and removal
table.table.table-hover(ng-if="poolPatches || updates")
th.col-sm-2 Name
th.col-sm-5 Description
th.col-sm-3 Applied/Released date
th.col-sm-1 Size
th.col-sm-1 Status
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-if="patch.applied")
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 | orderBy:'-name'| slice:(5*(currentPatchPage-1)):(5*currentPatchPage)")
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-if="!patch.applied")
span.label.label-error Not applied
//- span.pull-right.btn-group.quick-buttons
//- a(xo-click="deletePatch(patch.UUID)")
//- i.fa.fa-trash-o.fa-lg(tooltip="Remove this patch")
.grid
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-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
table.table.table-hover(ng-if="host.$PCIs")
th PCI Info
th Device Name
tr(ng-repeat="pci in host.$PCIs | resolve | orderBy:'pci_id' track by pci.UUID")
td {{pci.pci_id}} ({{pci.class_name}})
td {{pci.device_name}}
tr(ng-repeat="pci in host.$PCIs | resolve | orderBy:'pci_id' | slice:(5*(currentPCIPage-1)):(5*currentPCIPage) track by pci.id")
td.oneliner {{pci.pci_id}} ({{pci.class_name}})
td.oneliner {{pci.device_name}}
.center(ng-if = '(host.$PCIs | resolve).length > 5')
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
table.table.table-hover(ng-if="host.$PGPUs.length !== 0")
th Device
tr(ng-repeat="pgpu in host.$PGPUs | resolve | orderBy:'device' track by pgpu.UUID")
td {{pgpu.device}}
tr(ng-repeat="pgpu in host.$PGPUs | resolve | orderBy:'device' | slice:(5*(currentGPUPage-1)):(5*currentGPUPage) track by pgpu.id")
td.oneliner {{pgpu.device}}
.center(ng-if = '(host.$PGPUs | resolve).length > 5')
pagination(boundary-links="true", total-items="(host.$PGPUs | resolve).length", ng-model="$parent.currentGPUPage", items-per-page="5", 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-asterisk
| Misc
.panel-body(style="overflow:hidden")
.row(ng-repeat="(key, value) in host.bios_strings")
label.control-label.col-sm-4
| {{key}}:
.col-sm-8 {{value}}
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-book
| License
.panel-body
.row
label.control-label.col-sm-3
| Server:
.col-sm-9 {{host.license_server.address}}:{{host.license_server.port}}
br
.row(ng-repeat="key in hostParams | slice:(10*(currentLicensePage-1)):(10*currentLicensePage) track by key")
label.control-label.col-sm-7
| {{key}}:
.col-sm-5 {{host.license_params[key]}}
.center
pagination(boundary-links="true", total-items="hostParams.length", ng-model="currentLicensePage", items-per-page="10", max-size="10", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")

View File

@@ -1,27 +0,0 @@
angular = require 'angular'
#=====================================================================
module.exports = angular.module 'xoWebApp.isoDevice', []
.directive 'isoDevice', -> {
restrict: 'E'
template: require './view'
scope: {
isos: '='
vm: '='
}
controller: 'IsoDevice as isoDevice'
bindToController: true
}
.controller 'IsoDevice', (xo) ->
this.eject = (VM) ->
xo.vm.ejectCd VM.UUID
this.insert = (VM, disc_id) ->
xo.vm.insertCd VM.UUID, disc_id, true
# A module exports its name.
.name

View File

@@ -1,27 +1,126 @@
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import xoTag from 'tag'
import includes from 'lodash.includes'
import xoApi from 'xo-api';
import xoApi from 'xo-api'
import view from './view';
import view from './view'
//====================================================================
// ===================================================================
export default angular.module('xoWebApp.list', [
uiRouter,
xoApi,
xoTag
])
.config(function ($stateProvider) {
$stateProvider.state('list', {
url: '/list',
controller: 'ListCtrl as list',
template: view,
});
})
.controller('ListCtrl', function (xoApi) {
this.byTypes = xoApi.byTypes;
template: view
})
})
.controller('ListCtrl', function (xo, xoApi, $state, $scope, $rootScope) {
const user = xoApi.user
$scope.createButton = user.permission !== 'admin'
if (user.permission !== 'admin') {
$scope.createButton = false
xo.resourceSet.getAll()
.then(sets => {
$scope.resourceSets = sets
$scope.createButton = sets.length > 0
})
}
this.hosts = xoApi.getView('host')
this.pools = xoApi.getView('pool')
this.SRs = xoApi.getView('SR')
this.VMs = xoApi.getView('VM')
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,205 @@
.sub-bar
.grid(style="margin-left:1em")
.btn-group.dropdown.col-sm-1(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.col-sm-1(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}}
.btn-group.col-sm-1.col-sm-offset-9(ng-if='createButton')
a.btn.navbar-btn.filter(xo-sref='VMs_new()' tooltip = 'Create VM')
i.fa.fa-desktop.text-success &nbsp
i.fa.fa-plus.text-success
//- 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.byTypes.pool | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by pool.UUID", ng-if="pool.name_label", xo-sref="pools_view({id: pool.UUID})")
.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.UUID})") {{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.UUID})") {{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
| {{pool.$running_hosts.length}}/{{pool.hosts.length}} 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.byTypes.host | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by host.UUID", xo-sref="hosts_view({id: host.UUID})")
.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] | %}}", tooltip="RAM: {{[host.memory.usage, host.memory.size] | %}} allocated")
| {{[host.memory.usage, host.memory.size] | %}}
.grid-cell.flat-cell
| {{host.VMs.length}} 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.byTypes.VM | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by VM.UUID", xo-sref="VMs_view({id: VM.UUID})")
.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.UUID})") {{container.name_label}}
div(ng-if="'host' === container.type", ng-init="pool = (container.poolRef | resolve)")
| Resident on:&nbsp;
a(ui-sref="hosts_view({id: container.UUID})") {{container.name_label}}
small(ng-if="pool.name_label")
| (
a(ui-sref="pools_view({id: pool.UUID})") {{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.byTypes.SR | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by SR.UUID", xo-sref="SRs_view({id: SR.UUID})")
.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] | %}} ({{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.UUID})") {{container.name_label}}
div(ng-if="'host' === container.type")
| Connected to&nbsp;
a(ui-sref="hosts_view({id: container.UUID})") {{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) {
var toState, toStateParams;
{
let tmp = $rootScope._login;
if (tmp) {
toState = tmp.state.name;
toStateParams = tmp.stateParams;
delete $rootScope._login;
} else {
toState = 'index';
}
}
$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,82 @@
import angular from 'angular'
import forEach from 'lodash.foreach'
import find from 'lodash.find'
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) => {
const network = find($scope.networks, (network) => network.name_label === xoApi.get(vif.$network).name_label)
$scope.selected.vif[vif.id] = network
// Try to find a target network with the same name
? network.id
// Otherwise the default network
: 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 target network:
table.table(ng-if="!intraPoolMigration")
tr
th.col-sm-5 VIF (network)
th.col-sm-7 Target Network
tbody
tr(ng-repeat="vif in (VIFs | orderBy:'device')")
td {{vif.MAC}} ({{(vif.$network | resolve).name_label}})
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

@@ -1,59 +1,125 @@
import angular from 'angular';
import filter from 'lodash.filter';
import uiRouter from 'angular-ui-router';
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import xoServices from 'xo-services';
import updater from '../updater'
import xoServices from 'xo-services'
import includes from 'lodash.includes'
import view from './view';
import view from './view'
//====================================================================
// ===================================================================
export default angular.module('xoWebApp.navbar', [
uiRouter,
xoServices,
updater,
xoServices
])
.controller('NavbarCtrl', function ($state, xoApi, xo, $scope) {
.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, {
status: {
get: () => xoApi.status,
get: () => xoApi.status
},
user: {
get: () => xoApi.user,
},
});
this.logIn = xoApi.logIn;
get: () => xoApi.user
}
})
this.logIn = xoApi.logIn
this.logOut = function () {
xoApi.logOut();
$state.go('login');
};
xoApi.logOut()
}
// 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 ALIVE_STATUS = {
cancelling: true,
pending: true,
};
let {canAccess} = xo;
let sieve = (task) => ALIVE_STATUS[task.status] && canAccess(task.$host);
$scope.$watchCollection(() => xoApi.byTypes.task, (tasks) => {
this.tasks = filter(tasks, sieve);
});
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')
})
.directive('navbar', function () {
return {
restrict: 'E',
controller: 'NavbarCtrl as navbar',
template: view,
scope: {},
};
scope: {}
}
})
// A module exports its name.
.name
;

View File

@@ -21,20 +21,20 @@ 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.
ul.nav.navbar-nav
li
a(href="https://xen-orchestra.com/#/pricing?pk_campaign=xoa_source", target="_blank")
a(href="https://xen-orchestra.com/#/pricing?pk_campaign=xoa_source", target="_blank", tooltip="Source version without Pro support. Use in production at your own risk.")
i.xo-icon-info.text-danger
| Unregistered version: no support provided!
span.hidden-sm No Pro Support!
//- Right items of the navbar.
ul.nav.navbar-nav.navbar-right
li.navbar-text(ng-if="'disconnected' === navbar.status")
@@ -44,15 +44,15 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
i.fa.fa-refresh.fa-spin
| Connecting to XO-Server
//- Running tasks
li.disabled(ng-if="!navbar.tasks.length", tooltip="No running tasks")
li.disabled(ng-if="!navbar.tasks.size", tooltip="No running tasks")
a.dropdown-toggle.inverse
i.xo-icon-task
li.dropdown(dropdown, ng-if="navbar.tasks.length")
li.dropdown(dropdown, ng-if="navbar.tasks.size")
a.dropdown-toggle.inverse(dropdown-toggle)
i.xo-icon-task
ul.dropdown-menu.inverse
li.task-menu(
ng-repeat="task in navbar.tasks | orderBy:natural('name_label') track by task.id"
ng-repeat="task in navbar.tasks.all | map | orderBy:natural('name_label') track by task.id"
)
a(
ui-sref="hosts_view({id: task.$host})"
@@ -85,15 +85,28 @@ 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 = 'self.index')
i.fa.fa-cloud
| Self Service
li.divider
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
a(ui-sref = 'backup.index')
i.fa.fa-archive
| Backup
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
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" }'
@@ -109,9 +122,18 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
//- /Main menu.
li
a
a(ui-sref="settings.update")
i.fa.fa-question-circle.text-warning(ng-if = '!navbar.updater.state', tooltip = 'No update information available')
i.fa.fa-question-circle.text-info(ng-if = 'navbar.updater.state == "connected"', tooltip = 'Update information may be available')
i.fa.fa-check.text-success(ng-if = 'navbar.updater.state == "upToDate"', tooltip = 'Your XOA is up-to-date')
i.fa.fa-bell.text-primary(ng-if = 'navbar.updater.state == "upgradeNeeded"', tooltip = 'You need to update your XOA (new version is available)')
i.fa.fa-bell-slash.text-warning(ng-if = 'navbar.updater.state == "registerNeeded"', tooltip = 'Your XOA is not registered for updates')
i.fa.fa-exclamation-triangle.text-danger(ng-if = 'navbar.updater.state == "error"', tooltip = 'Can\'t fetch update information')
li
a(ng-if = '!navbar.user.provider', ui-sref="{{navbar.user.provider ? 'settings.users' : 'settings.user'}}", tooltip="{{navbar.user.email}}")
i.fa.fa-user
| {{navbar.user.email}}
span.hidden-sm {{navbar.user.email}}
li
a(ng-click = 'navbar.logOut()')
i.fa.fa-sign-out

View File

@@ -1,11 +1,12 @@
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import Bluebird from 'bluebird';
import angular from 'angular'
import Bluebird from 'bluebird'
import forEach from 'lodash.foreach'
import uiRouter from 'angular-ui-router'
import view from './view';
import _indexOf from 'lodash.indexof';
import view from './view'
import _indexOf from 'lodash.indexof'
//====================================================================
// ===================================================================
export default angular.module('xoWebApp.newSr', [
uiRouter
@@ -14,463 +15,431 @@ export default angular.module('xoWebApp.newSr', [
$stateProvider.state('SRs_new', {
url: '/srs/new/:container',
controller: 'NewSrCtrl as newSr',
template: view,
});
template: view
})
})
.controller('NewSrCtrl', function ($scope, $state, $stateParams, xo, xoApi, notify, modal, bytesToSizeFilter) {
this.reset = function (data = {}) {
this.data = {};
delete this.lockCreation;
this.data = {}
delete this.lockCreation
this.lock = !(
('Local' === data.srType) &&
(data.srPath && data.srPath.path)
);
(data.srType === 'Local') &&
(data.srPath && data.srPath.path) ||
data.srType === 'SMB'
)
}
};
this.resetLists = function () {
delete this.data.nfsList
delete this.data.scsiList
delete this.lockCreation
this.lock = true
this.resetLists = function() {
delete this.data.nfsList;
delete this.data.scsiList;
delete this.lockCreation;
this.lock = true;
this.resetErrors();
};
this.resetErrors()
}
this.resetErrors = function () {
delete this.data.error;
};
delete this.data.error
}
/*
* Loads NFS paths and iScsi iqn`s
*/
this.populateSettings = function (type, server, auth, user, password) {
this.reset()
this.loading = true
this.reset();
this.loading = true;
server = this._parseAddress(server);
if ('NFS' === type || 'NFS_ISO' === type) {
server = this._parseAddress(server)
if (type === 'NFS' || type === 'NFS_ISO') {
xoApi.call('sr.probeNfs', {
host: this.container.UUID,
host: this.container.id,
server: server.host
})
.then(response => this.data.paths = response)
.catch(error => notify.warning({
title : 'NFS Detection',
message : error.message
title: 'NFS Detection',
message: error.message
}))
.finally(() => this.loading = false)
;
} else if ('iSCSI' === type) {
} else if (type === 'iSCSI') {
let params = {
host: this.container.UUID
};
if (auth) {
params.chapUser = user;
params.chapPassword = password;
host: this.container.id
}
params.target = server.host;
if (auth) {
params.chapUser = user
params.chapPassword = password
}
params.target = server.host
if (server.port) {
params.port = server.port;
params.port = server.port
}
xoApi.call('sr.probeIscsiIqns', params)
.then(response => {
if (response.length > 0) {
this.data.iqns = response;
this.data.iqns = response
} else {
notify.warning({
title : 'iSCSI Detection',
message : 'No IQNs found'
});
title: 'iSCSI Detection',
message: 'No IQNs found'
})
}
})
.catch(error => notify.warning({
title : 'iSCSI Detection',
message : error.message
title: 'iSCSI Detection',
message: error.message
}))
.finally(() => this.loading = false)
;
} else {
this.loading = false;
this.loading = false
}
};
}
/*
* Loads iScsi LUNs
*/
this.populateIScsiIds = function (iqn, auth, user, password) {
delete this.data.iScsiIds;
this.loading = true;
delete this.data.iScsiIds
this.loading = true
let params = {
host: this.container.UUID,
host: this.container.id,
target: iqn.ip,
targetIqn: iqn.iqn
};
}
if (auth) {
params.chapUser = user;
params.chapPassword = password;
params.chapUser = user
params.chapPassword = password
}
xoApi.call('sr.probeIscsiLuns', params)
.then(response => {
response.forEach(item => {
forEach(response, item => {
item.display = 'LUN ' + item.id + ': ' +
item.serial + ' ' + bytesToSizeFilter(item.size) +
' (' + item.vendor + ')';
});
' (' + item.vendor + ')'
})
this.data.iScsiIds = response;
this.data.iScsiIds = response
})
.catch(error => notify.warning({
title : 'LUNs Detection',
message : error.message
title: 'LUNs Detection',
message: error.message
}))
.finally(() => this.loading = false)
;
};
}
this._parseAddress = function (address) {
let index = address.indexOf(':');
let port = false;
let host = address;
if (-1 < index) {
port = address.substring(index + 1);
host = address.substring(0, index);
let index = address.indexOf(':')
let port = false
let host = address
if (index > -1) {
port = address.substring(index + 1)
host = address.substring(0, index)
}
return {
host,
port
};
};
}
}
this._prepareNfsParams = function (data) {
let server = this._parseAddress(data.srServer);
let server = this._parseAddress(data.srServer)
let params = {
host: this.container.UUID,
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
server: server.host,
serverPath: data.srPath.path
};
}
return params;
};
this._prepareScsiParams = function(data) {
return params
}
this._prepareScsiParams = function (data) {
let params = {
host: this.container.UUID,
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
target: data.srIqn.ip,
targetIqn: data.srIqn.iqn,
scsiId: data.srIScsiId.scsiId,
};
scsiId: data.srIScsiId.scsiId
}
let server = this._parseAddress(data.srServer);
let server = this._parseAddress(data.srServer)
if (server.port) {
params.port = server.port;
params.port = server.port
}
if (data.srAuth) {
params.chapUser = data.srChapUser;
params.chapPassword = data.srChapPassword;
params.chapUser = data.srChapUser
params.chapPassword = data.srChapPassword
}
return params;
};
return params
}
this.createSR = function (data) {
this.lock = true
this.creating = true
this.lock = true;
this.creating = true;
let operationToPromise
let operationToPromise;
switch(data.srType) {
switch (data.srType) {
case 'NFS':
let nfsParams = this._prepareNfsParams(data);
let nfsParams = this._prepareNfsParams(data)
operationToPromise = this._checkNfsExistence(nfsParams)
.then(() => xoApi.call('sr.createNfs', nfsParams))
;
break;
.then(() => xoApi.call('sr.createNfs', nfsParams))
break
case 'iSCSI':
let scsiParams = this._prepareScsiParams(data);
operationToPromise = this._checkScsiExistence(scsiParams)
.then(() => xoApi.call('sr.createIscsi', scsiParams))
;
break;
let scsiParams = this._prepareScsiParams(data)
operationToPromise = this._checkScsiExistence(scsiParams)
.then(() => xoApi.call('sr.createIscsi', scsiParams))
break
case 'lvm':
let device = data.srDevice.device;
let device = data.srDevice.device
operationToPromise = xoApi.call('sr.createLvm', {
host: this.container.UUID,
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
device
});
break;
})
break
case 'NFS_ISO':
case 'Local':
let server = this._parseAddress(data.srServer || '');
let path = (('NFS_ISO' === data.srType) ?
server.host + ':' :
'') + data.srPath.path;
operationToPromise = xoApi.call('sr.createIso', {
host: this.container.UUID,
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
path
});
break;
default:
type: 'local',
path: data.srPath.path
})
break
operationToPromise = Bluebird.reject({message: 'Unhanled SR Type'});
break;
case 'NFS_ISO':
let server = this._parseAddress(data.srServer || '')
const path = (
data.srType === 'NFS_ISO'
? server.host + ':'
: ''
) + data.srPath.path
operationToPromise = xoApi.call('sr.createIso', {
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
type: 'nfs',
path
})
break
case 'SMB':
operationToPromise = xoApi.call('sr.createIso', {
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
type: 'smb',
path: data.srServer,
user: data.user,
password: data.password
})
break
default:
operationToPromise = Bluebird.reject({message: 'Unhanled SR Type'})
break
}
operationToPromise
.then(id => {
$state.go('SRs_view', {id});
$state.go('SRs_view', {id})
})
.catch(error => {
notify.error({
title : 'Storage Creation Error',
message : error.message
});
title: 'Storage Creation Error',
message: error.message
})
})
.finally(() => {
this.lock = false;
this.creating = false;
this.lock = false
this.creating = false
})
;
};
}
this._checkScsiExistence = function (params) {
this.resetLists();
this.resetLists()
return xoApi.call('sr.probeIscsiExists', params)
.then(response => {
if (response.length > 0) {
this.data.scsiList = response;
this.data.scsiList = response
return modal.confirm({
title: 'Previous LUN Usage',
message: 'This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation. Are you sure?'
});
} else {
return Bluebird.resolve(true);
})
}
})
;
};
return true
})
}
this._checkNfsExistence = function (params) {
this.resetLists();
this.resetLists()
return xoApi.call('sr.probeNfsExists', params)
.then(response => {
if (response.length > 0) {
this.data.nfsList = response;
this.data.nfsList = response
return modal.confirm({
title: 'Previous Path Usage',
message: 'This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation. Are you sure?'
});
} else {
return Bluebird.resolve(true);
})
}
return true
})
;
}
};
const hostsByPool = xoApi.getIndex('hostsByPool')
const srsByContainer = xoApi.getIndex('srsByContainer')
this._gatherConnectedUuids = function () {
const srIds = []
this._gatherConnectedUuids = function() {
// Shared SRs.
forEach(srsByContainer[this.container.$poolId], sr => {
srIds.push(sr.id)
})
let SRs = [];
// Local SRs.
forEach(hostsByPool[this.container.$poolId], host => {
forEach(srsByContainer[host.id], sr => {
srIds.push(sr.id)
})
})
let pool = xoApi.get(this.container.poolRef);
pool.SRs.forEach(ref => SRs.push(xoApi.get(ref).UUID));
let hosts = [];
pool.hosts.forEach(ref => hosts.push(xoApi.get(ref)));
hosts.forEach(h => h.SRs.forEach(ref => SRs.push(xoApi.get(ref).UUID)));
return SRs;
};
return srIds
}
this._processSRList = function (list) {
let inUse = false
let SRs = this._gatherConnectedUuids()
let inUse = false;
let SRs = this._gatherConnectedUuids();
forEach(list, item => {
inUse = (item.used = _indexOf(SRs, item.uuid) > -1) || inUse
})
list.forEach(item => {
inUse = (item.used = _indexOf(SRs, item.uuid) > -1) || inUse;
});
this.lockCreation = inUse
this.lockCreation = inUse;
return list
}
return list;
this.loadScsiList = function (data) {
this.resetLists()
this.loading = true
};
this.loadScsiList = function(data) {
this.resetLists();
let params = this._prepareScsiParams(data);
let params = this._prepareScsiParams(data)
xoApi.call('sr.probeIscsiExists', params)
.then(response => {
if (response.length > 0) {
this.data.scsiList = this._processSRList(response);
this.data.scsiList = this._processSRList(response)
}
this.lock = !Boolean(data.srIScsiId);
this.lock = !Boolean(data.srIScsiId)
})
.catch(error => {
notify.error({
title : 'iSCSI Error',
message : error.message
});
title: 'iSCSI Error',
message: error.message
})
})
;
};
.finally(() => this.loading = false)
}
this.loadNfsList = function (data) {
this.resetLists()
this.resetLists();
let server = this._parseAddress(data.srServer);
let server = this._parseAddress(data.srServer)
xoApi.call('sr.probeNfsExists', {
host: this.container.UUID,
host: this.container.id,
server: server.host,
serverPath: data.srPath.path
})
.then(response => {
if (response.length > 0) {
this.data.scsiList = this._processSRList(response);
this.data.nfsList = this._processSRList(response)
}
this.lock = !Boolean(data.srPath.path);
this.lock = !Boolean(data.srPath.path)
})
.catch(error => {
notify.error({
title : 'NFS error',
message : error.message
});
title: 'NFS error',
message: error.message
})
})
;
};
}
this.reattachNfs = function (uuid, {name, nameError}, {desc, descError}, iso) {
this._reattach(uuid, 'nfs', {name, nameError}, {desc, descError}, iso);
};
this._reattach(uuid, 'nfs', {name, nameError}, {desc, descError}, iso)
}
this.reattachIScsi = function (uuid, {name, nameError}, {desc, descError}) {
this._reattach(uuid, 'iscsi', {name, nameError}, {desc, descError})
}
this._reattach(uuid, 'iscsi', {name, nameError}, {desc, descError});
};
this._reattach = function(uuid, type, {name, nameError}, {desc, descError}, iso = false) {
this.resetErrors();
let method = 'sr.reattach' + (iso ? 'Iso' : '');
this._reattach = function (uuid, type, {name, nameError}, {desc, descError}, iso = false) {
this.resetErrors()
let method = 'sr.reattach' + (iso ? 'Iso' : '')
if (nameError || descError) {
this.data.error = {
name: nameError,
desc: descError
};
}
notify.warning({
title: 'Missing parameters',
message: 'Complete the General section information, please'
});
})
} else {
this.lock = true;
this.attaching = true;
this.lock = true
this.attaching = true
xoApi.call(method, {
host: this.container.UUID,
host: this.container.id,
uuid,
nameLabel: name,
nameDescription: desc,
type
})
.then(id => {
$state.go('SRs_view', {id});
$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;
this.lock = false
this.attaching = false
})
;
}
}
};
this.reset();
this.reset()
$scope.$watch(() => xoApi.get($stateParams.container), container => {
this.container = container;
});
this.container = container
})
})
// A module exports its name.
.name
;

View File

@@ -3,16 +3,16 @@
p.page-title
i.xo-icon-sr
| Add SR on&nbsp;
a(ng-if="'pool' === newSr.container.type", ui-sref="pools_view({id: newSr.container.UUID})")
a(ng-if="'pool' === newSr.container.type", ui-sref="pools_view({id: newSr.container.id})")
| {{newSr.container.name_label}}
a(ng-if="'host' === newSr.container.type", ui-sref="hosts_view({id: newSr.container.UUID})")
a(ng-if="'host' === newSr.container.type", ui-sref="hosts_view({id: newSr.container.id})")
| {{newSr.container.name_label}}
form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
.grid
//- 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
@@ -27,6 +27,7 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
optgroup(label="ISO SR")
option(value="Local") Local
option(value="NFS_ISO") NFS ISO
option(value="SMB") SMB
.form-group(ng-class = '{"has-error": newSr.data.error.name}')
label.col-sm-3.control-label Name
.col-sm-9
@@ -38,10 +39,10 @@ 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"')
.form-group(ng-if = 'formData.srType === "NFS" || formData.srType === "iSCSI" || formData.srType === "NFS_ISO"')
label.col-sm-3.control-label
| Server
span(ng-if = 'formData.srType === "iSCSI"')
@@ -55,6 +56,23 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
button.btn.btn-default(type = 'button', ng-click = 'newSr.populateSettings(formData.srType, formData.srServer, formData.srAuth, formData.srChapUser, formData.srChapPassword)')
i.fa.fa-search
//- For SMB
.form-group(ng-if='formData.srType === "SMB"')
label.col-sm-3.control-label
| Server
.col-sm-9
input.form-control(type="text", name='srServer', ng-model='formData.srServer', placeholder='\\\\\\\\<server>\\\\<path>' required)
.form-group(ng-if='formData.srType === "SMB"')
label.col-sm-3.control-label
| User
.col-sm-9
input.form-control(type="text", name='user', ng-model='formData.user', required)
.form-group(ng-if='formData.srType === "SMB"')
label.col-sm-3.control-label
| Password
.col-sm-9
input.form-control(type="password", name='password', ng-model='formData.password', required)
//- For Local LVM
.form-group(ng-if = 'formData.srType === "lvm"')
label.col-sm-3.control-label Device
@@ -109,12 +127,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 +148,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 +169,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 +198,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

@@ -1,4 +1,10 @@
angular = require 'angular'
cloneDeep = require 'lodash.clonedeep'
filter = require 'lodash.filter'
forEach = require 'lodash.foreach'
trim = require 'lodash.trim'
includes = require 'lodash.includes'
forEach = require 'lodash.foreach'
#=====================================================================
@@ -8,7 +14,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
@@ -16,8 +22,97 @@ module.exports = angular.module 'xoWebApp.newVm', [
bytesToSizeFilter, sizeToBytesFilter
notify
) ->
{get} = xoApi
$scope.min = Math.min
user = xoApi.user
$scope.isAdmin = user.permission == 'admin'
userGroups = user.groups
if !$scope.isAdmin
$scope.resourceSets = []
$scope.userResourceSets = []
$scope.resourceSet = ''
xo.resourceSet.getAll()
.then (sets) ->
$scope.resourceSets = sets
$scope.resourceSet = $scope.resourceSets[0]
$scope.updateResourceSet($scope.resourceSet)
$scope.updateResourceSet = (resourceSet) ->
$scope.resourceSet = resourceSet
$scope.template = ''
$scope.templates = []
$scope.writable_SRs = []
$scope.ISO_SRs = []
srs = []
$scope.resourceSetNetworks = []
$scope.pools = []
forEach $scope.resourceSet.objects, (id) ->
obj = xoApi.get id
if obj.type is 'VM-template'
$scope.templates.push(obj)
else if obj.type is 'SR'
srs.push(obj)
else if obj.type is 'network'
$scope.resourceSetNetworks.push(obj)
$scope.writable_SRs = filter(srs, (sr) => sr.content_type isnt 'iso')
$scope.ISO_SRs = filter(srs, (sr) => sr.content_type is 'iso')
$scope.multipleVmsActive = false
$scope.vmsNames = ['VM1', 'VM2']
$scope.numberOfVms = 1
$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.updateTotalDiskBytes()
$scope.initExistingValues = (template) ->
$scope.name_label = template.name_label
sizes = {}
$scope.templateVBDs = []
$scope.existingDiskSizeValues = {}
$scope.existingDiskSizeUnits = {}
forEach xoApi.get(template.$VBDs), (VBD) ->
if VBD.is_cd_drive or not VBD.VDI? or not (VDI = xoApi.get(VBD.VDI))?
return
$scope.templateVBDs.push(VBD)
sizes[VBD.position] = bytesToSizeFilter VDI.size
$scope.existingDiskSizeValues[VBD.position] = +sizes[VBD.position].split(' ')[0]
$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()
$scope.memory = template.memory.size
{get} = xoApi
removeItems = do ->
splice = Array::splice.call.bind Array::splice
(array, index, n) -> splice array, index, n ? 1
@@ -31,6 +126,105 @@ module.exports = angular.module 'xoWebApp.newVm', [
result
pool = default_SR = null
host = null
poolHosts = null
hostsSrs = null
do (
networks = xoApi.getIndex('networksByPool')
srsByContainer = xoApi.getIndex('srsByContainer')
vmTemplatesByContainer = xoApi.getIndex('vmTemplatesByContainer')
poolSrs = null
hostSrs = null
poolTemplates = null
hostTemplates = null
) ->
Object.defineProperties($scope, {
networks: {
get: () => pool && networks[pool.id]
}
})
$scope.updateSrs = () =>
srs = []
$scope.selectedLocalSrs = {}
Object.defineProperty($scope.selectedLocalSrs, "size", {
value: 0,
writable: true,
enumerable: false
})
$scope.forcedHost = undefined
poolSrs and forEach(poolSrs, (sr) => srs.push(sr))
hostSrs and forEach(hostSrs, (sr) => srs.push(sr))
poolHosts and forEach(poolHosts, (host) =>
forEach(hostsSrs[host.id], (sr) ->
srs.push(sr))
)
if pool or $scope.resourceSet
selectedSrs = []
forEach($scope.templateVBDs, (vbd) ->
selectedSrs.push(xoApi.get(vbd.VDI).$SR)
)
forEach($scope.VDIs, (vdi) ->
selectedSrs.push(vdi.SR)
)
if $scope.resourceSet
forEach(selectedSrs, (sr) ->
sr = xoApi.get sr
container = xoApi.get sr.$container
if container.type is 'host'
if not $scope.selectedLocalSrs[sr.$container]
$scope.selectedLocalSrs[sr.$container] = []
$scope.selectedLocalSrs.size++
$scope.forcedHost = sr.$container
if not includes($scope.selectedLocalSrs[sr.$container], sr.id)
$scope.selectedLocalSrs[sr.$container].push(sr.id)
)
else
forEach(poolHosts, (host) ->
forEach(hostsSrs[host.id], (sr) ->
if includes(selectedSrs, sr.id)
if not $scope.selectedLocalSrs[host.id]
$scope.selectedLocalSrs[host.id] = []
$scope.selectedLocalSrs.size++
$scope.forcedHost = host.id
if not includes($scope.selectedLocalSrs[host.id], sr.id)
$scope.selectedLocalSrs[host.id].push(sr.id)
)
)
if not $scope.resourceSet
$scope.writable_SRs = filter(srs, (sr) => sr.content_type isnt 'iso')
$scope.ISO_SRs = filter(srs, (sr) => sr.content_type is 'iso')
updateTemplates = () =>
templates = []
poolTemplates and forEach(poolTemplates, (template) => templates.push(template))
hostTemplates and forEach(hostTemplates, (template) => templates.push(template))
$scope.templates = templates
$scope.$watchCollection(
() => pool and srsByContainer[pool.id],
(srs) =>
poolSrs = srs
$scope.updateSrs()
)
$scope.$watchCollection(
() => host and srsByContainer[host.id],
(srs) =>
hostSrs = srs
$scope.updateSrs()
)
$scope.$watchCollection(
() => pool and vmTemplatesByContainer[pool.id],
(templates) =>
poolTemplates = templates
updateTemplates()
)
$scope.$watchCollection(
() => host and vmTemplatesByContainer[host.id],
(templates) =>
hostTemplates = templates
updateTemplates()
)
$scope.$watch(
-> get $stateParams.container
(container) ->
@@ -41,54 +235,69 @@ module.exports = angular.module 'xoWebApp.newVm', [
if container.type is 'host'
host = container
pool = (get container.poolRef) ? {}
pool = (get container.$poolId) ? {}
poolHosts = []
hostsSrs = {}
else
host = {}
pool = container
objects = filter(xoApi.all, (obj) -> obj.type is 'host' or obj.type is 'SR')
poolHosts = filter(objects, (obj) -> obj.type is 'host' and obj.$poolId is pool.id)
hostsSrs = {}
forEach(poolHosts, (host) ->
hostsSrs[host.id] = filter(objects, (obj) -> obj.type is 'SR' and obj.$container is host.id)
)
default_SR = get pool.default_SR
default_SR = if default_SR
default_SR.UUID
else
''
# Computes the list of templates.
$scope.templates = get (merge pool.templates, host.templates)
# FIXME: We should filter on connected SRs (PBDs)!
# Computes the list of SRs.
SRs = get (merge pool.SRs, host.SRs)
# Computes the list of ISO SRs.
$scope.ISO_SRs = (SR for SR in SRs when SR.content_type is 'iso')
# Computes the list of writable SRs.
$scope.writable_SRs = (SR for SR in SRs when SR.content_type isnt 'iso')
# Computes the list of networks.
$scope.networks = get pool.networks
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.memory = null
$scope.memoryValue = null
$scope.units = ['MiB', 'GiB', 'TiB']
$scope.memoryUnit = $scope.units[1]
$scope.name_description = 'Created by XO'
$scope.name_label = ''
$scope.template = ''
$scope.totalDiskBytes = 0
$scope.firstSR = ''
$scope.VDIs = []
$scope.VIFs = []
$scope.isDiskTemplate = false
$scope.cloudConfigSshKey = ''
$scope.cloudConfigCustom = '#cloud-config\n#hostname: myhostname\n#ssh_authorized_keys:\n# - ssh-rsa <myKey>\n#packages:\n# - htop\n'
$scope.cloudConfigLoading = false
$scope.cloudConfigError = false
$scope.bootAfterCreate = true
$scope.updateMemory = ->
if $scope.memoryValue
$scope.memory = sizeToBytesFilter $scope.memoryValue + ' ' + $scope.memoryUnit
else
$scope.memory = $scope.template.memory.size
$scope.updateMemoryUnit = (memoryUnit) ->
$scope.memoryUnit = memoryUnit
$scope.updateMemory()
$scope.updateTotalDiskBytes = ->
$scope.totalDiskBytes = 0
forEach $scope.existingDiskSizeValues, (value, key) ->
$scope.totalDiskBytes += sizeToBytesFilter value + ' ' + $scope.existingDiskSizeUnits[key]
forEach $scope.VDIs, (VDI) ->
$scope.totalDiskBytes += (sizeToBytesFilter VDI.sizeValue + ' ' + VDI.sizeUnit) || 0
$scope.addVIF = do ->
id = 0
->
(network = '') ->
$scope.VIFs.push {
id: id++
network: ''
network
}
$scope.addVIF()
$scope.removeVIF = (index) -> removeItems $scope.VIFs, index
@@ -98,22 +307,39 @@ module.exports = angular.module 'xoWebApp.newVm', [
newIndex = index + direction
[VDIs[index], VDIs[newIndex]] = [VDIs[newIndex], VDIs[index]]
$scope.removeVDI = (index) -> removeItems $scope.VDIs, index
$scope.removeVDI = (index) ->
removeItems $scope.VDIs, index
$scope.updateTotalDiskBytes()
VDI_id = 0
$scope.addVDI = ->
$scope.VDIs.push {
id: VDI_id++
bootable: false
name_label: $scope.name_label + '_disk' + (VDI_id - 1)
name_description: 'Created by XO'
size: ''
SR: default_SR
sizeValue: ''
sizeUnit: $scope.units[1]
SR: default_SR || $scope.writable_SRs[0] && $scope.writable_SRs[0].id
type: 'system'
}
$scope.updateSrs()
$scope.$watch('name_label', (newName, oldName) ->
forEach $scope.VDIs, (vdi, index) ->
if vdi.name_label is oldName + '_disk' + index
vdi.name_label = newName + '_disk' + index
)
# 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
@@ -123,50 +349,132 @@ 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 = angular.copy template.template_info.disks
VDIs = $scope.VDIs = cloneDeep template.template_info.disks
forEach VDIs, (vdi, index) ->
vdi.name_label = $scope.name_label + '_disk' + index
vdi.name_description = 'Created by XO'
# if the template has no config disk
# nor it's Other install media (specific case)
if VDIs.length is 0 and template.name_label isnt 'Other install media'
$scope.isDiskTemplate = true
else $scope.isDiskTemplate = false
for VDI in VDIs
VDI.id = VDI_id++
VDI.SR or= default_SR || $scope.writable_SRs[0] && $scope.writable_SRs[0].id
VDI.size = bytesToSizeFilter VDI.size
VDI.SR or= default_SR
VDI.sizeValue = if VDI.size then +VDI.size.split(' ')[0] 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.updateTotalDiskBytes()
$scope.updateSrs()
$scope.createVM = ->
$scope.uploadCloudConfig = (file) ->
$scope.cloudConfigError = false
return unless file
reader = new FileReader()
reader.onerror = () ->
$scope.cloudConfigError = true
reader.onload = (event) ->
$scope.cloudConfigCustom = event.target.result
reader.onloadend = (event) ->
$scope.cloudConfigLoading = false
if file.size > 2e6
reader.onerror()
return
$scope.cloudConfigLoading = true
reader.readAsText(file)
$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 'index'
xenDefaultWeight = 256
$scope.weightMap = {
'Quarter (1/4)': xenDefaultWeight / 4,
'Half (1/2)': xenDefaultWeight / 2,
'Normal': xenDefaultWeight,
'Double (x2)': xenDefaultWeight * 2
}
$scope.createVM = (name_label) ->
{
resourceSet
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 = angular.copy VDIs
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
# Default VDI name and description
VDI.name_label = VDI.name_label || name_label + '_disk' + index
VDI.name_description = VDI.name_description || 'Created by XO'
# TODO: handles invalid values.
forEach existingDisks, (disk, index) ->
if disk.name_label is ''
delete disk.name_label
if disk.name_description is ''
delete disk.name_description
# Does not edit the displayed data directly.
VIFs = angular.copy VIFs
VIFs = cloneDeep VIFs
for VIF in VIFs
# 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'
@@ -181,17 +489,23 @@ 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 = {
resourceSet: resourceSet && resourceSet.id
installation
pv_args
name_label
template: template.UUID
template: template.id
VDIs
VIFs
existingDisks
}
# TODO:
# - disable the form during creation
# - indicate the progress of the operation
@@ -199,32 +513,74 @@ 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.memory = memoryValue + ' ' + memoryUnit
return xo.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, '-')
if $scope.configDriveMethod == 'standard'
$scope.cloudContent = '#cloud-config\nhostname: ' + hostname + '\nssh_authorized_keys:\n - ' + $scope.cloudConfigSshKey + '\n'
else
$scope.cloudContent = $scope.cloudConfigCustom
# 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
if resourceSet
# FIXME When using self service, ACL permissions are not updated fast enough to access VM view right after creation
$state.go 'index'
else
# 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

@@ -1,50 +1,138 @@
.grid
.panel.panel-default
p.page-title
.col-sm-4
p.page-title.col-sm-4
i.xo-icon-vm
| Create VM on&nbsp;
a(ng-if="'pool' === container.type", ui-sref="pools_view({id: container.UUID})")
| {{container.name_label}}
a(ng-if="'host' === container.type", ui-sref="hosts_view({id: container.UUID})")
| {{container.name_label}}
| Create VM
span(ng-if='isAdmin') on
a(ng-if="'pool' === container.type", ui-sref="pools_view({id: container.id})")
| {{container.name_label}}
a(ng-if="'host' === container.type", ui-sref="hosts_view({id: container.id})")
| {{container.name_label}}
form.col-sm-4.form-horizontal(ng-if="resourceSet")
.form-group(style="margin-top:4px;margin-bottom:4px;")
label.col-sm-5.control-label Resource set:
.col-sm-7(ng-if='resourceSets.length > 1')
select.form-control(
style="max-width:20em;"
ng-model="$parent.resourceSet"
ng-options="resourceSet.name for resourceSet in resourceSets | orderBy:natural('name') track by resourceSet.id"
ng-change="updateResourceSet(resourceSet)"
required=""
)
.col-sm-7.form-control-static(ng-if='resourceSets.length === 1')
| {{ resourceSet.name }}
//- 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.UUID", required="")
select.form-control(ng-model="template", ng-options="template.name_label for template in templates | map | 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' ng-disabled='resourceSet')
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")
.input-group
input.form-control(type='number' min="0" step="0.01" placeholder="{{ template.memory.size | bytesConvert:memoryUnit:'iB' }}" ng-model="memoryValue" ng-change="updateMemory()")
span.input-group-btn.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle)
| {{ memoryUnit }}&nbsp;
span.caret
ul.dropdown-menu(role = 'menu' style='min-width:0')
li(ng-repeat="memoryUnit in units")
a(ng-click="updateMemoryUnit(memoryUnit)") {{ memoryUnit }}
.grid
//- Install panel
.panel.panel-default
//- Cloud Config Panel, only for templates with existing disks
.panel.panel-default(ng-if="isDiskTemplate")
.panel-heading.panel-title
i.fa.fa-download(style="color: #e25440;")
i.fa.fa-cloud
| Config Drive
span.pull-right
label(style = 'cursor: pointer;')
input.hidden(type = 'checkbox', ng-model = '$parent.configDriveActive', ng-click = '$parent.configDriveMethod = "standard"')
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
fieldset(ng-disabled = '!$parent.configDriveActive')
.form-group
label.col-sm-3.control-label SSH Key
.col-sm-9
.input-group
span.input-group-addon
input(
type = 'radio'
name = 'configDriveMethod'
ng-model = '$parent.configDriveMethod'
value = 'standard'
)
input.form-control(
type="text"
placeholder="ssh-rsa AAAA.... you@machine"
ng-model="$parent.cloudConfigSshKey"
ng-disabled = '$parent.configDriveMethod !== "standard"'
name="cloudConfigSshKey"
required
)
.form-group
label.col-sm-3.control-label
a(href='http://cloudinit.readthedocs.org/en/latest/topics/examples.html', target='_blank') Custom config
.col-sm-9
.input-group
span.input-group-addon
input(
type = 'radio'
name = 'configDriveMethod'
ng-model = '$parent.configDriveMethod'
value = 'custom'
)
textarea.form-control(
rows='4'
style="resize: none;"
ng-model="$parent.cloudConfigCustom"
ng-disabled = '$parent.configDriveMethod !== "custom"'
name="cloudConfigCustom"
required
)
br
button.btn.btn-default(
type = 'button'
ng-disabled = '$parent.configDriveMethod !== "custom"'
ngf-select = '$parent.uploadCloudConfig($files[0]); fileName = $files[0].name'
) Select file
span(style='max-width: 1em' ng-init='fileName = "None"')
| Selected file : {{ fileName }}
i.fa.fa-spinner.fa-spin(ng-show = 'cloudConfigLoading')
i.fa.fa-exclamation-triangle.text-danger(ng-show = 'cloudConfigError' tooltip = 'Error while loading file')
//- Install panel, only if an installation method is needed
.panel.panel-default(ng-if="!isDiskTemplate")
.panel-heading.panel-title
i.fa.fa-download
| Install settings
.panel-body
.form-group(ng-show="availableMethods.cdrom")
@@ -55,20 +143,20 @@ form.form-horizontal(ng-submit="createVM()")
input(
type = 'radio'
name = 'installation_method'
ng-model = 'installation_method'
ng-model = '$parent.installation_method'
value = 'cdrom'
required
)
select.form-control.disabled(
ng-disabled="'cdrom' !== installation_method"
ng-model="installation_cdrom"
required
ng-model="$parent.installation_cdrom"
)
option(value = '') Please select
optgroup(ng-repeat="SR in ISO_SRs | orderBy:natural('name_label') track by SR.UUID", ng-if="SR.VDIs.length", label="{{SR.name_label}}")
option(ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.UUID", ng-value="VDI.UUID")
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
@@ -76,25 +164,28 @@ form.form-horizontal(ng-submit="createVM()")
input(
type = 'radio'
name = 'installation_method'
ng-model = '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="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>
input.form-control(type="text", ng-disabled="'network' !== installation_method", placeholder="e.g: http://ftp.debian.org/debian", ng-model="$parent.installation_network")
.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
@@ -105,10 +196,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.UUID as network.name_label for network in (networks | orderBy:natural("name_label"))'
ng-options = 'network as (network.name_label + " (" + (network.$pool | resolve).name_label + ")") for network in (networks || resourceSetNetworks) | map | orderBy:natural("name_label") track by network.id'
ng-model = 'VIF.network'
required
)
@@ -125,11 +216,59 @@ form.form-horizontal(ng-submit="createVM()")
i.fa.fa-plus
| Add interface
//- end of misc and interface panel
//- Cloud config panel
.grid(ng-if = 'coreOsCloudConfig')
.panel.panel-default
.panel-heading.panel-title
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(); checkNumberOfVms()', 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-sr(style="color: #e25440;")
i.xo-icon-disk
| Disks
.panel-body
table.table.table-hover
@@ -138,16 +277,62 @@ form.form-horizontal(ng-submit="createVM()")
th.col-md-1 Bootable?
th.col-md-2 Size
th.col-md-2 Name
th.col-md-4 Description
th.col-md-1 &#160;
th.col-md-3 Description
th.col-md-2 &#160;
//- Buttons
tr(ng-repeat="VBD in (templateVBDs | orderBy:'position') 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 + ' on ' + (SR.$container | resolve).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); updateSrs()', required)
option(value = '') Please select
td.text-center
i.fa.fa-check(ng-if = 'VBD.bootable')
td(style = "overflow: visible")
.input-group
input.form-control(
type='number'
min="0"
step="0.01"
placeholder="Size of this virtual disk"
ng-model="existingDiskSizeValues[VBD.position]"
ng-readonly='!configDriveActive'
ng-change = 'updateVdiSize(VBD.position)'
required
)
span.input-group-btn.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle ng-disabled='!configDriveActive')
| {{ existingDiskSizeUnits[VBD.position] }}&nbsp
span.caret
ul.dropdown-menu(role="menu" style="min-width:0")
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.UUID 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 + ' on ' + (SR.$container | resolve).name_label + ' (' + (SR.size - SR.physical_usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))" ng-change="updateSrs()")
td.text-center
input(type="checkbox", ng-model="VDI.bootable")
td
input.form-control(type="text", ng-model="VDI.size", required="")
td(style = "overflow: visible")
.input-group
input.form-control(
type='number'
min="0"
step="0.01"
placeholder="Size of this virtual disk"
ng-model="VDI.sizeValue"
ng-change = 'updateTotalDiskBytes()'
required
)
span.input-group-btn.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle)
| {{ VDI.sizeUnit }}&nbsp
span.caret
ul.dropdown-menu(role="menu" style="min-width:0")
li(ng-repeat="unit in units")
a(ng-click="VDI.sizeUnit = unit; updateTotalDiskBytes()") {{ unit }}
td
input.form-control(type="text", placeholder="Name of this virtual disk", ng-model="VDI.name_label")
td
@@ -172,32 +357,140 @@ 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.stat-name
| Name:
p.center.big {{name_label}}
.grid-cell
p.stat-name
| Template:
p.center {{template.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
.grid-cell
p.stat-name vCPUs
p.center.big {{CPUs || template.CPUs.number}}
//- p.stat-name vCPUs
p.center.big(tooltip="vCPUs")
| {{CPUs || template.CPUs.number || 0}}x&nbsp;
i.xo-icon-cpu
.grid-cell
p.stat-name RAM
p.center.big {{(memory) || (template.memory.size | bytesToSize)}}
//- p.stat-name RAM
p.center.big(tooltip="RAM")
span(ng-if="memoryValue") {{memory | bytesToSize}}
span(ng-if="!memoryValue") {{template.memory.size | bytesToSize}}
| &nbsp;
i.xo-icon-memory
.grid-cell
p.stat-name Disks
p.center.big {{(VDIs.length) || (template.$VBDs.length) || 0}}
//- p.stat-name Disks
p.center.big(tooltip="Disks")
| {{(VDIs.length) || (templateVBDs.length) || 0}}x&nbsp;
i.xo-icon-disk
.grid-cell
p.stat-name Interfaces
p.center.big {{VIFs.length}}
//- p.stat-name Interfaces
p.center.big(tooltip="Network interfaces")
| {{(VIFs.length) || (template.VIFs.length) || 0}}x&nbsp;
i.xo-icon-network
.grid(ng-if="template && resourceSet")
.grid-cell
.center-block(ng-if="resourceSet.limits.cpus" style="width:60%")
.progress
.progress-bar(
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{resourceSet.limits.cpus.total - resourceSet.limits.cpus.available}}",
aria-valuemax="{{resourceSet.limits.cpus.total}}",
style="width: {{[resourceSet.limits.cpus.total - resourceSet.limits.cpus.available, resourceSet.limits.cpus.total] | percentage}}",
tooltip="{{resourceSet.limits.cpus.total - resourceSet.limits.cpus.available}} vCPUs already in use"
)
.progress-bar(
ng-class = '{"progress-bar-success": numberOfVms * (CPUs || template.CPUs.number || 0) <= resourceSet.limits.cpus.available, "progress-bar-danger": numberOfVms * (CPUs || template.CPUs.number || 0) > resourceSet.limits.cpus.available}'
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{numberOfVms * (CPUs || template.CPUs.number || 0)}}",
aria-valuemax="{{resourceSet.limits.cpus.total}}",
style="width: {{[min(numberOfVms * (CPUs || template.CPUs.number || 0), resourceSet.limits.cpus.available), resourceSet.limits.cpus.total] | percentage}}",
tooltip="{{numberOfVms * (CPUs || template.CPUs.number || 0)}} vCPUs / {{resourceSet.limits.cpus.available}} remaining"
)
.grid-cell
.center-block(ng-if="resourceSet.limits.memory" style="width:60%")
.progress
.progress-bar(
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{resourceSet.limits.memory.total - resourceSet.limits.memory.available}}",
aria-valuemax="{{resourceSet.limits.memory.total}}",
style="width: {{[resourceSet.limits.memory.total - resourceSet.limits.memory.available, resourceSet.limits.memory.total] | percentage}}",
tooltip="{{resourceSet.limits.memory.total - resourceSet.limits.memory.available | bytesToSize}} already in use"
)
.progress-bar(
ng-class = '{"progress-bar-success": numberOfVms * memory <= resourceSet.limits.memory.available, "progress-bar-danger": numberOfVms * memory > resourceSet.limits.memory.available}'
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{numberOfVms * memory}}",
aria-valuemax="{{resourceSet.limits.memory.total}}",
style="width: {{[min(numberOfVms * memory, resourceSet.limits.memory.available), resourceSet.limits.memory.total] | percentage}}",
tooltip="{{numberOfVms * memory | bytesToSize}} / {{resourceSet.limits.memory.available | bytesToSize}} remaining"
)
.grid-cell
.center-block(ng-if="resourceSet.limits.disk" style="width:60%")
.progress
.progress-bar(
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{resourceSet.limits.disk.total - resourceSet.limits.disk.available}}",
aria-valuemax="{{resourceSet.limits.disk.total}}",
style="width: {{[resourceSet.limits.disk.total - resourceSet.limits.disk.available, resourceSet.limits.disk.total] | percentage}}",
tooltip="{{resourceSet.limits.disk.total - resourceSet.limits.disk.available | bytesToSize}} already in use"
)
.progress-bar(
ng-class = '{"progress-bar-success": numberOfVms * totalDiskBytes <= resourceSet.limits.disk.available, "progress-bar-danger": numberOfVms * totalDiskBytes > resourceSet.limits.disk.available}'
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{numberOfVms * totalDiskBytes}}",
aria-valuemax="{{resourceSet.limits.disk.total}}",
style="width: {{[min(numberOfVms * totalDiskBytes, resourceSet.limits.disk.available), resourceSet.limits.disk.total] | percentage}}",
tooltip="{{numberOfVms * totalDiskBytes | bytesToSize}} / {{resourceSet.limits.disk.available | bytesToSize}} remaining"
)
.grid-cell
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(ng-if="selectedLocalSrs.size === 1")
label
| The VM will be created on {{(forcedHost | resolve).name_label}} since
span(ng-repeat="sr in selectedLocalSrs[forcedHost]") {{(sr | resolve).name_label}}
span(ng-if="$index < selectedLocalSrs[forcedHost].length - 2") ,
span(ng-if="$index === selectedLocalSrs[forcedHost].length - 2") and
span(ng-if="selectedLocalSrs[forcedHost].length > 1") are
span(ng-if="selectedLocalSrs[forcedHost].length === 1") is
| on {{(forcedHost | resolve).name_label}}
p.text-danger(ng-if="selectedLocalSrs.size > 1")
label.control-label Incompatible disks:
ul(ng-if="selectedLocalSrs.size > 1")
li.text-danger(ng-repeat="(host, srs) in selectedLocalSrs")
span(ng-repeat="sr in srs") {{(sr | resolve).name_label}}
span(ng-if="$index < srs.length - 2") ,
span(ng-if="$index === srs.length - 2") and
span(ng-if="srs.length > 1") are
span(ng-if="srs.length === 1") is
| on {{(host | resolve).name_label}}
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',
'resourceSet.limits.cpus && (CPUs || template.CPUs.number || 0) > resourceSet.limits.cpus.available',
'resourceSet.limits.memory && memory > resourceSet.limits.memory.available',
'resourceSet.limits.disk && totalDiskBytes > resourceSet.limits.disk.available',
'selectedLocalSrs.size > 1'
].join(' || ')
)
i.fa.fa-play(ng-if = '!creatingVM')
i.fa.fa-circle-o-notch.fa-spin(ng-if = 'creatingVM')
| Create VM

View File

@@ -1,66 +1,240 @@
import angular from 'angular';
import angular from 'angular'
import forEach from 'lodash.foreach'
import uiRouter from 'angular-ui-router'
import uiRouter from 'angular-ui-router';
import xoTag from 'tag'
import view from './view';
import view from './view'
//====================================================================
// ===================================================================
export default angular.module('xoWebApp.pool', [
uiRouter,
xoTag
])
.config(function ($stateProvider) {
$stateProvider.state('pools_view', {
url: '/pools/:id',
controller: 'PoolCtrl',
template: view,
});
template: view
})
})
.controller('PoolCtrl', function ($scope, $stateParams, xoApi, xo, modal) {
$scope.$watch(() => xoApi.get($stateParams.id), function (pool) {
$scope.pool = pool;
});
.controller('PoolCtrl', function ($scope, $stateParams, xoApi, xo, modal, notify) {
{
const {id} = $stateParams
const hostsByPool = xoApi.getIndex('hostsByPool')
const runningHostsByPool = xoApi.getIndex('runningHostsByPool')
const srsByContainer = xoApi.getIndex('srsByContainer')
const networksByPool = xoApi.getIndex('networksByPool')
Object.defineProperties($scope, {
pool: {
get: () => xoApi.get(id)
},
hosts: {
get: () => hostsByPool[id]
},
runningHosts: {
get: () => runningHostsByPool[id]
},
srs: {
get: () => srsByContainer[id]
},
networks: {
get: () => networksByPool[id]
}
})
}
$scope.$watch(() => $scope.pool && $scope.hosts, result => {
if (result) {
$scope.listMissingPatches()
xo.pool.getLicenseState($scope.pool.id).then(result => {
$scope.license = result
})
}
})
$scope.currentLogPage = 1
$scope.savePool = function ($data) {
let {pool} = $scope;
let {name_label, name_description} = $data;
let {pool} = $scope
let {name_label, name_description} = $data
$data = {
id: pool.UUID,
id: pool.id
}
if (name_label !== pool.name_label) {
$data.name_label = name_label;
$data.name_label = name_label
}
if (name_description !== pool.name_description) {
$data.name_description = name_description;
$data.name_description = name_description
}
xoApi.call('pool.set', $data);
};
xoApi.call('pool.set', $data)
}
$scope.deleteAllLog = function () {
return modal.confirm({
title: 'Log deletion',
message: 'Are you sure you want to delete all the logs?',
message: 'Are you sure you want to delete all the logs?'
}).then(function () {
// TODO: return all promises.
angular.forEach($scope.pool.messages, function(value, key) {
xo.log.delete(value);
console.log('Remove log', value);
});
});
};
forEach($scope.pool.messages, function (message) {
xo.log.delete(message.id)
console.log('Remove log', message.id)
})
})
}
$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);
};
console.log('Remove log', id)
return xo.log.delete(id)
}
$scope.nbUpdates = {}
$scope.totalUpdates = 0
$scope.listMissingPatches = () => {
forEach($scope.hosts, function (host, host_id) {
xo.host.listMissingPatches(host_id)
.then(result => {
$scope.nbUpdates[host_id] = result.length
$scope.totalUpdates += result.length
}
)
})
}
$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.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)
})
}
$scope.canAdmin = function (id = undefined) {
if (id === undefined) {
id = $scope.pool && $scope.pool.id
}
return id && xoApi.canInteract(id, 'administrate') || false
}
$scope.connectPIF = function (id) {
console.log(`Connect PIF ${id}`)
xoApi.call('pif.connect', {id: id})
}
$scope.disconnectPIF = function (id) {
console.log(`Disconnect PIF ${id}`)
xoApi.call('pif.disconnect', {id: id})
}
$scope.removePIF = function (id) {
console.log(`Remove PIF ${id}`)
xoApi.call('pif.delete', {id: id})
}
$scope.deleteNetwork = function (id) {
return modal.confirm({
title: 'Network deletion',
message: 'Are you sure you want to delete this network?'
}).then(function () {
console.log(`Delete network ${id}`)
notify.info({
title: 'Network deletion...',
message: 'Deleting the network'
})
xoApi.call('network.delete', {id: id})
})
}
$scope.disallowDelete = function (network) {
let disallow = false
forEach(network.PIFs, pif => {
const PIF = xoApi.get(pif)
if (PIF.disallowUnplug || PIF.management) {
disallow = true
return false
}
})
return disallow
}
$scope.createNetwork = function (name, description, pif, mtu, vlan) {
$scope.createNetworkWaiting = true
notify.info({
title: 'Network creation...',
message: 'Creating the network'
})
const params = {
pool: $scope.pool.id,
name: name
}
if (mtu) {
params.mtu = mtu
}
if (pif) {
params.pif = pif
}
if (vlan) {
params.vlan = vlan
}
if (description) {
params.description = description
}
return xoApi.call('network.create', params).then(function () {
$scope.creatingNetwork = false
$scope.createNetworkWaiting = false
})
}
$scope.physicalPifs = () => {
const physicalPifs = []
const host = xoApi.get($scope.pool.master)
forEach(host.$PIFs, pif => {
pif = xoApi.get(pif)
if (pif.physical) {
physicalPifs.push(pif.id)
}
})
return physicalPifs
}
// $scope.patchPool = ($files, id) ->
// file = $files[0]
// xo.pool.patch id
// .then ({ $sendTo: url }) ->
// return $upload.http {
// return Upload.http {
// method: 'POST'
// url
// data: file
@@ -79,9 +253,7 @@ export default angular.module('xoWebApp.pool', [
// notify.info
// title: 'Upload patch'
// message: 'Success'
})
// A module exports its name.
.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
@@ -24,15 +24,14 @@
| {{pool.name_description}}
dt Master
dd(ng-repeat="master in [pool.master] | resolve")
a(ui-sref="hosts_view({id: master.UUID})")
a(ui-sref="hosts_view({id: master.id})")
| {{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.UUID})") {{default_SR.name_label}}
a(ui-sref="SRs_view({id: default_SR.id})") {{default_SR.name_label}}
dt HA
dd
| {{pool.HA_enabled}}
@@ -49,64 +48,71 @@
| Save
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-stats(style="color: #e25440;")
i.xo-icon-stats
| Stats
.grid
.grid-cell
.row
.col-xs-6
p.stat-name Hosts:
p.center.big-stat {{pool.hosts.length}}
.grid-cell
p.center.big-stat {{hosts | count}}
.col-xs-6
p.stat-name Running:
p.center.big-stat {{pool.$running_hosts.length}}
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%")
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.UUID})")
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.UUID)")
button.btn(tooltip="Patch the pool", tooltip-placement="top", type="button", style="width: 90%", ngf-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
th Name
th.col-md-4 Description
th.col-md-6 Memory
tr(xo-sref="hosts_view({id: host.UUID})", ng-repeat="host in pool.hosts | resolve | orderBy:natural('name_label') track by host.UUID")
td {{host.name_label}}
td {{host.name_description}}
tr(xo-sref="hosts_view({id: host.id})", ng-repeat="host in hosts | map | orderBy:natural('name_label') track by host.id")
td.oneliner {{host.name_label}}
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] | %}}")
.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,33 +121,171 @@
th Type
th Size
th.col-md-4 Physical/Allocated usage
tr(xo-sref="SRs_view({id: SR.UUID})", ng-repeat="SR in pool.SRs | resolve | orderBy:natural('name_label') track by SR.UUID")
td {{SR.name_label}}
td {{SR.name_description}}
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}}&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] | %}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | %}}")
.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] | %}}", tooltip="Allocated: {{[(SR.usage), SR.size] | %}}")
//- Logs panel
.grid
.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")
//- Networks/Interfaces panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-comments(style="color: #e25440;")
i.xo-icon-network
| Networks
.panel-body
table.table.table-hover
th.col-md-2 Name
th.col-md-2 Description
th.col-md-7 PIFs
th.col-md-1
tr(ng-repeat="network in networks track by network.id" ng-init="showPIFs = false")
td {{network.name_label}}
td {{network.name_description}}
td
a(ng-if="network.PIFs.length && !showPIFs" ng-click="$parent.showPIFs=true") show PIFs ({{network.PIFs.length}})
a(ng-if="network.PIFs.length && showPIFs" ng-click="$parent.showPIFs=false") hide PIFs
table.table.table-sm.table-hover(ng-if="network.PIFs.length && showPIFs")
th.col-md-2 Device
th.col-md-2 Host
th.col-md-1 VLAN
th.col-md-2 Address
th.col-md-2 MAC
th.col-md-2 Link status
tr(ng-repeat="PIF in network.PIFs | resolve | orderBy:natural('($host | resolve).name_label')")
td
| {{PIF.device}}
span.label.label-primary(ng-if="PIF.management") XAPI
| &nbsp;
span.label.label-primary(ng-if="PIF.physical") Phys.
td {{(PIF.$host | resolve).name_label}}
td
span(ng-if="PIF.vlan > -1") {{PIF.vlan}}
span(ng-if="PIF.vlan == -1") -
td {{PIF.ip}} ({{PIF.mode}})
td {{PIF.mac}}
td
span.label.label-default(ng-if="!PIF.attached") Disconnected
span.label.label-success(ng-if="PIF.attached") Connected
span.pull-right.btn-group.quick-buttons(ng-if="canAdmin()")
i.fa.fa-unlink.fa-lg.text-danger(ng-if="PIF.disallowUnplug" tooltip="Disconnection not allowed")
i.fa.fa-unlink.fa-lg.text-danger(ng-if="!PIF.disallowUnplug && PIF.management" tooltip="Management PIF")
a(tooltip="Disconnect this interface" xo-click="disconnectPIF(PIF.id)", ng-if = 'PIF.attached && !PIF.disallowUnplug && !PIF.management')
i.fa.fa-unlink.fa-lg
a(tooltip="Connect this interface" xo-click="connectPIF(PIF.id)", ng-if = '!PIF.attached')
i.fa.fa-link.fa-lg
span(ng-if="!network.PIFs.length") No PIF attached to this network
td
span.pull-right.btn-group.quick-buttons
i.fa.fa-trash-o.text-danger.fa-lg(ng-if="disallowDelete(network)" tooltip="Some PIFs cannot be disconnected")
i.fa.fa-trash-o.text-danger.fa-lg(ng-if="network.name_label === 'Host internal management network'" tooltip="Management network")
a(tooltip="Remove network" xo-click="$parent.deleteNetwork(network.id)", ng-if = 'canAdmin() && network.name_label !== "Host internal management network" && !disallowDelete(network)')
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", 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-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 physicalPifs()')
option(value = '') None
| &nbsp;
.form-group
label.control-label(for = 'newNetworkName') Name&nbsp;
input#newNetworkName.form-control(type = 'text', ng-model = 'newNetworkName', required)
| &nbsp;
.form-group
label.control-label(for = 'newNetworkDescription') Description&nbsp;
input#newNetworkDescription.form-control(type = 'text', ng-model = 'newNetworkDescription', placeholder= 'Network created with Xen Orchestra')
| &nbsp;
.form-group
label.control-label(for = 'newNetworkVlan') VLAN&nbsp;
input#newNetworkVlan.form-control(type = 'text', ng-model = 'newNetworkVlan', placeholder = 'Defaut: no VLAN')
| &nbsp;
.form-group
label(for = 'newNetworkMTU') MTU&nbsp;
input#newNetworkMTU.form-control(type = 'text', ng-model = 'newNetworkMTU', placeholder = 'Default: 1500')
| &nbsp;
.form-group
button.btn.btn-primary(type = 'submit')
i.fa.fa-plus-square
| Create
span(ng-if = 'createNetworkWaiting')
| &nbsp;
i.xo-icon-loading-sm
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-refresh
| Updates
span.quick-edit(
ng-if="totalUpdates"
tooltip="Update all"
ng-click="installAllPatches()"
)
i.fa.fa-download.fa-fw
.panel-body
p.center(ng-if="!totalUpdates") Everything up to date
table.table.table-hover(ng-if="totalUpdates")
tr
th Host
th Description
th Missing patches
th Install
tr( ng-repeat="host in hosts" ng-if="nbUpdates[host.id]")
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
//- Logs panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-comments
| Logs
span.quick-edit(ng-if="pool.messages.length", tooltip="Remove all logs", xo-click="deleteAllLog()")
span.quick-edit(ng-if="pool.messages | isNotEmpty", tooltip="Remove all logs", xo-click="deleteAllLog()")
i.fa.fa-trash-o.fa-fw
.panel-body
p.center(ng-if="!pool.messages.length") No recent logs
table.table.table-hover(ng-if="pool.messages.length")
p.center(ng-if="pool.messages | isEmpty") No recent logs
table.table.table-hover(ng-if="pool.messages | isNotEmpty")
th Date
th Name
tr(ng-repeat="message in pool.messages | resolve | orderBy:'-time' track by message.UUID")
tr(ng-repeat="message in pool.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
td {{message.time*1e3 | date:"medium"}}
td
| {{message.name}}
span.pull-right.btn-group.quick-buttons
a(xo-click="deleteLog(message.UUID)")
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 || 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=">>")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-book
| License
.panel-body
.row(ng-repeat="(key, value) in license")
label.control-label.col-sm-3
| {{key}}:
.col-sm-9 {{value}}

View File

@@ -0,0 +1,250 @@
import angular from 'angular'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import assign from 'lodash.assign'
import differenceBy from 'lodash.differenceby'
import filter from 'lodash.filter'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import includes from 'lodash.includes'
import intersection from 'lodash.intersection'
import map from 'lodash.map'
import view from './view'
// ====================================================================
export default angular.module('self.admin', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('self.admin', {
url: '/admin',
resolve: {
users (xo) {
return xo.user.getAll()
},
groups (xo) {
return xo.group.getAll()
}
},
controller: 'AdminCtrl as ctrl',
template: view
})
})
.controller('AdminCtrl', function (xo, xoApi, $scope, users, groups, sizeToBytesFilter, bytesToSizeFilter) {
users.push(...groups)
this.sizeUnits = ['MiB', 'GiB', 'TiB']
let validHosts
this.resourceSets = {}
const loadSets = () => {
xo.resourceSet.getAll()
.then(sets => this.resourceSets = sets)
}
const reset = () => {
this.srs = []
this.networks = []
this.templates = []
this.eligibleHosts = []
validHosts = []
delete this.editing
delete this.selectedNetworks
delete this.selectedSrs
delete this.selectedTemplates
delete this.selectedPools
delete this.selectedSubjects
delete this.name
delete this.cpuMax
delete this.memoryMax
delete this.diskMax
this.memoryUnit = this.sizeUnits[1]
this.diskUnit = this.sizeUnits[1]
}
this.reset = reset
reset()
loadSets()
this.pools = xoApi.getView('pool').all
const hosts = xoApi.getView('host').all
const srs = xoApi.getView('SR').all
const networks = xoApi.getView('network').all
const vmTemplatesByContainer = xoApi.getIndex('vmTemplatesByContainer')
this.subjects = users
const collectById = function (array) {
const collection = {}
forEach(array, item => collection[item.id] = item)
return collection
}
this.listSubjects = collectById(users)
// When a pool selection happens
const filterSrs = () => filter(srs, sr => {
let found = false
forEach(this.selectedPools, pool => !(found = sr.$poolId === pool.id))
return found
})
const gatherTemplates = () => {
const vmTemplates = {}
forEach(this.selectedPools, pool => {
assign(vmTemplates, vmTemplatesByContainer[pool.id])
})
return vmTemplates
}
$scope.$watchCollection(() => this.selectedPools, () => {
validHosts = filter(hosts, host => {
let found = false
forEach(this.selectedPools, pool => !(found = host.$poolId === pool.id))
return found
})
this.srs = filterSrs()
this.selectedSrs = intersection(this.selectedSrs, this.srs)
this.vmTemplates = gatherTemplates()
// TODO : Why isn't this working fine? (`intersection` uses SameValueZero as comparison: http://ecma-international.org/ecma-262/6.0/#sec-samevaluezero)
// this.selectedTemplates = intersection(this.selectedTemplates, this.vmTemplates)
this.selectedTemplates = filter(this.selectedTemplates, (template) => this.vmTemplates.hasOwnProperty(template.id))
this.networks = filterNetworks()
this.selectedNetworks = intersection(this.selectedNetworks, this.networks)
this.eligibleHosts = resolveHosts()
})
const filterNetworks = () => {
const selectableHosts = filter(validHosts, host => {
let keptBySr
forEach(this.selectedSrs, sr => !(keptBySr = intersection(sr.$PBDs, host.$PBDs).length > 0))
return keptBySr
})
return filter(networks, network => {
let kept = false
forEach(selectableHosts, host => !(kept = intersection(network.PIFs, host.PIFs).length > 0))
return kept
})
}
// When a SR selection happens
const constraintNetworks = () => {
this.networks = filterNetworks()
this.selectedNetworks = intersection(this.selectedNetworks, this.networks)
resolveHosts()
}
const resolveHosts = () => {
const keptHosts = filter(validHosts, host => {
let keptBySr = false
forEach(this.selectedSrs, sr => !(keptBySr = intersection(sr.$PBDs, host.$PBDs).length > 0))
let keptByNetwork
forEach(this.selectedNetworks, network => !(keptByNetwork = intersection(network.PIFs, host.PIFs).length > 0))
return keptBySr && keptByNetwork
})
this.eligibleHosts = keptHosts
this.excludedHosts = differenceBy(map(hosts), keptHosts, item => item && item.id)
}
$scope.$watchCollection(() => this.selectedSrs, constraintNetworks)
$scope.$watchCollection(() => this.selectedNetworks, resolveHosts)
this.save = function (name, subjects, pools, templates, srs, networks, cpuMax, memoryMax, memoryUnit, diskMax, diskUnit, id) {
return save(name, subjects, pools, templates, srs, networks, cpuMax, memoryMax, memoryUnit, diskMax, diskUnit, id)
.then(reset)
.then(loadSets)
}
this.create = function (name, subjects, pools, templates, srs, networks, cpuMax, memoryMax, memoryUnit, diskMax, diskUnit) {
return xo.resourceSet.create(name)
.then(set => {
save(name, subjects, pools, templates, srs, networks, cpuMax, memoryMax, memoryUnit, diskMax, diskUnit, set.id)
})
.then(reset)
.then(loadSets)
}
const save = function (name, subjects, pools, templates, srs, networks, cpuMax, memoryMax, memoryUnit, diskMax, diskUnit, id) {
const limits = {}
if (cpuMax) {
limits.cpus = cpuMax
}
if (memoryMax) {
limits.memory = sizeToBytesFilter(`${memoryMax} ${memoryUnit}`)
}
if (diskMax) {
limits.disk = sizeToBytesFilter(`${diskMax} ${diskUnit}`)
}
const getIds = arr => map(arr, item => item.id)
subjects = getIds(subjects)
pools = getIds(pools)
templates = getIds(templates)
srs = getIds(srs)
networks = getIds(networks)
const objects = Array.of(...templates, ...srs, ...networks)
return xo.resourceSet.set(id, name, subjects, objects, limits)
}
this.edit = id => {
window.scroll(0, 0)
const set = find(this.resourceSets, rs => rs.id === id)
if (set) {
this.editing = id
this.name = set.name
const getObjects = arr => map(arr, id => xoApi.get(id))
const objects = getObjects(set.objects)
const selectedPools = {}
forEach(objects, object => {
const poolId = object.poolId || object.$poolId
if (poolId) { selectedPools[poolId] = true }
})
this.selectedPools = getObjects(Object.keys(selectedPools))
this.selectedSrs = filter(objects, object => object.type === 'SR')
this.selectedNetworks = filter(objects, object => object.type === 'network')
this.selectedTemplates = filter(objects, object => object.type === 'VM-template')
this.selectedSubjects = filter(users, user => includes(set.subjects, user.id))
this.cpuMax = set.limits.cpus && set.limits.cpus.total
if (set.limits.memory) {
const memory = bytesToSizeFilter(set.limits.memory.total).split(' ')
this.memoryMax = +memory[0]
this.memoryUnit = memory[1]
} else {
delete this.memoryMax
this.memoryUnit = this.sizeUnits[1]
}
if (set.limits.disk) {
const disk = bytesToSizeFilter(set.limits.disk.total).split(' ')
this.diskMax = +disk[0]
this.diskUnit = disk[1]
} else {
delete this.diskMax
this.diskUnit = this.sizeUnits[1]
}
}
}
this.delete = id => {
xo.resourceSet.delete(id).then(() => {
if (id === this.editing) {
reset()
}
loadSets()
})
}
})
// A module exports its name.
.name

View File

@@ -0,0 +1,180 @@
.panel.panel-default
p.page-title
i.fa.fa-wrench(style="color: #e25440;")
| Administration
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-pencil-square-o
| Creation and edition
.panel-body
.alert.alert-info(ng-if = 'ctrl.editing') Editing an existing set
form.form-horizontal(ng-submit = 'ctrl[ctrl.editing ? "save" : "create"](ctrl.name, ctrl.selectedSubjects, ctrl.selectedPools, ctrl.selectedTemplates, ctrl.selectedSrs, ctrl.selectedNetworks, ctrl.cpuMax, ctrl.memoryMax, ctrl.memoryUnit, ctrl.diskMax, ctrl.diskUnit, ctrl.editing)')
.form-group
.col-sm-4
input.form-control(type = 'text', ng-model = 'ctrl.name', placeholder = 'resource set name', required)
.col-sm-4
ui-select(ng-model = 'ctrl.selectedSubjects', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'choose user(s) and/or group(s)')
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 = 'subject in ctrl.subjects | filter:{ permission: "!admin" } | filter:$select.search')
div(ng-if = 'subject.email')
i.xo-icon-user.fa-fw
| {{subject.email}}
div(ng-if = 'subject.name')
i.xo-icon-group.fa-fw
| {{subject.name}}
.col-sm-4
ui-select(ng-model = 'ctrl.selectedPools', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'choose pool(s)')
i(class = 'xo-icon-{{$item.type | lowercase}}')
| {{$item.name_label}}
ui-select-choices(repeat = 'pool in ctrl.pools | map | filter:$select.search | orderBy:["type", "name_label"]')
div
i(class = 'xo-icon-{{pool.type | lowercase}}')
| {{pool.name_label}}
fieldset(ng-disabled = 'ctrl.selectedPools | isEmpty')
.form-group
.col-sm-4
ui-select(ng-model = 'ctrl.selectedTemplates', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'choose VM templates')
i(class = 'xo-icon-{{$item.type | lowercase}}')
| {{$item.name_label}}
ui-select-choices(repeat = 'template in ctrl.vmTemplates | map | filter:$select.search | orderBy:["type", "name_label"]')
div
i(class = 'xo-icon-{{template.type | lowercase}}')
| {{template.name_label}}
.col-sm-4
ui-select(ng-model = 'ctrl.selectedSrs', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'choose storages')
i(class = 'xo-icon-{{$item.type | lowercase}}')
| {{$item.name_label}}
span(ng-if="$item.$container")
| ({{($item.$container | resolve).name_label}})
ui-select-choices(repeat = 'sr in ctrl.srs | map | filter:$select.search | orderBy:["type", "name_label"]')
div
i(class = 'xo-icon-{{sr.type | lowercase}}')
| {{sr.name_label}}
span(ng-if="sr.$container")
| ({{(sr.$container | resolve).name_label}})
.col-sm-4
fieldset(ng-disabled = 'ctrl.selectedSrs | isEmpty')
ui-select(ng-model = 'ctrl.selectedNetworks', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'choose networks')
i(class = 'xo-icon-{{$item.type | lowercase}}')
| {{$item.name_label}}
span(ng-if="$item.$poolId")
| ({{($item.$poolId | resolve).name_label}})
ui-select-choices(repeat = 'network in ctrl.networks | map | filter:$select.search | orderBy:["type", "name_label"]')
div
i(class = 'xo-icon-{{network.type | lowercase}}')
| {{network.name_label}}
span(ng-if="network.$poolId")
| ({{(network.$poolId | resolve).name_label}})
.form-group
.col-sm-4
input.form-control(type = 'number' min = '0' placeholder = 'Maximum CPUs' ng-model = 'ctrl.cpuMax')
.col-sm-4
.input-group
input.form-control(type = 'number' min = '0' placeholder = 'Maximum RAM' ng-model = 'ctrl.memoryMax')
span.input-group-btn.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle)
| {{ ctrl.memoryUnit }}&nbsp;
span.caret
ul.dropdown-menu(role = 'menu' style='min-width:0')
li(ng-repeat = 'unit in ctrl.sizeUnits')
a(ng-click = 'ctrl.memoryUnit = unit') {{ unit }}
.col-sm-4
.input-group
input.form-control(type = 'number' min = '0' placeholder = 'Max. disk Space' ng-model = 'ctrl.diskMax')
span.input-group-btn.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle)
| {{ ctrl.diskUnit }}&nbsp;
span.caret
ul.dropdown-menu(role = 'menu' style='min-width:0')
li(ng-repeat = 'unit in ctrl.sizeUnits')
a(ng-click = 'ctrl.diskUnit = unit') {{ unit }}
.row
.col-sm-8
h4 Available hosts
p.text-muted VMs created from this resource set shall run on the following hosts
ul.list-group
li.list-group-item(ng-if = 'ctrl.eligibleHosts | isEmpty'): em.text-muted No hosts available
li.list-group-item(ng-if = 'ctrl.eligibleHosts | isNotEmpty', ng-repeat = 'host in ctrl.eligibleHosts')
| {{ host.name_label }}
span(ng-if = '(host.$poolId | resolve)') &nbsp;({{ (host.$poolId | resolve).name_label }})
.col-sm-4
h4 Excluded hosts
ul.list-group
li.list-group-item(ng-repeat = 'host in ctrl.excludedHosts')
s
| {{ host.name_label }}
span(ng-if = '(host.$poolId | resolve)') &nbsp;({{ (host.$poolId | resolve).name_label }})
.form-group
.col-sm-10
button.btn.btn-lg.btn-primary(type = 'submit', ng-disabled = '(ctrl.selectedSrs | isEmpty) || (ctrl.selectedNetworks | isEmpty) || (ctrl.selectedTemplates | isEmpty) || (ctrl.selectedSubjects | isEmpty)')
span(ng-if='!ctrl.editing') Create
span(ng-if='ctrl.editing') Edit
| &nbsp;
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.reset()') Reset
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-list-alt
| Resource sets
.panel-body
div(ng-repeat = 's in ctrl.resourceSets | orderBy:"name"')
.row
.col-sm-9
h4 {{ s.name }}
.col-sm-3
button.btn.btn-primary(type = 'button', ng-click = 'ctrl.edit(s.id)'): i.fa.fa-pencil
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(s.id)'): i.fa.fa-trash
.row
.col-sm-9
ul.list-group
li.list-group-item
span(ng-repeat = 'subject in s.subjects')
span(ng-if = 'ctrl.listSubjects[subject].email')
i.fa.fa-user
| &nbsp;{{ ctrl.listSubjects[subject].email }}&nbsp;
span(ng-if = 'ctrl.listSubjects[subject].name')
i.fa.fa-users
| &nbsp;{{ ctrl.listSubjects[subject].name }}&nbsp;
li.list-group-item
span(ng-repeat = 'template in s.objects')
span(ng-if = '(template | resolve).type == "VM-template"')
i.xo-icon-vm
| &nbsp;{{ (template | resolve).name_label }}&nbsp;
li.list-group-item
span(ng-repeat = 'sr in s.objects')
span(ng-if = '(sr | resolve).type == "SR"')
i.xo-icon-sr
| &nbsp;{{ (sr | resolve).name_label }} ({{ ((sr | resolve).$container | resolve).name_label }})&nbsp;
li.list-group-item
span(ng-repeat = 'network in s.objects')
span(ng-if = '(network | resolve).type == "network"')
i.xo-icon-network
| &nbsp;{{ (network | resolve).name_label }} ({{ ((network | resolve).$poolId | resolve).name_label }})&nbsp;
li.list-group-item(ng-if="s.limits && (s.limits.cpus || s.limits.memory || s.limits.disk)")
span(ng-if="s.limits.cpus && s.limits.cpus.total")
i.xo-icon-cpu
| &nbsp;Max. vCPUs: {{ s.limits.cpus.total }} ({{ s.limits.cpus.available }} remaining)
br
span(ng-if="s.limits.memory && s.limits.memory.total")
i.xo-icon-memory
| &nbsp;Max. RAM: {{ s.limits.memory.total | bytesToSize }} ({{ s.limits.memory.available | bytesToSize }} remaining)
br
span(ng-if="s.limits.disk && s.limits.disk.total")
i.xo-icon-disk
| &nbsp;Max. disk space: {{ s.limits.disk.total | bytesToSize }} ({{ s.limits.disk.available | bytesToSize }} remaining)
.col-sm-3
//- ul.list-group
li.list-group-item max. CPUS: {{ s.cpuMax }}
li.list-group-item max. RAM: {{ s.memoryMax }}
li.list-group-item max. Disk space: {{ s.diskMax }}
hr

View File

@@ -0,0 +1,100 @@
import angular from 'angular'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import slice from 'lodash.slice'
import forEach from 'lodash.foreach'
import find from 'lodash.find'
import view from './view'
// ====================================================================
export default angular.module('self.dashboard', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('self.dashboard', {
url: '/dashboard',
resolve: {
users (xo) {
return xo.user.getAll()
},
groups (xo) {
return xo.group.getAll()
}
},
controller: 'DashboardCtrl as ctrl',
template: view
})
})
.controller('DashboardCtrl', function (xo, xoApi, $scope, $window, users, groups, bytesToSizeFilter) {
this.resourceSetsPerPage = 5
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
this.get = xoApi.get
this.pageIndex = 0
this.numberOfPages = 0
this.resourceSetsToShow = []
const loadSets = () => {
xo.resourceSet.getAll()
.then(sets => {
this.resourceSets = sets
this.resourceSet = this.resourceSets[0]
this.numberOfPages = Math.ceil(sets.length / this.resourceSetsPerPage)
this.updateResourceSetsToShow()
})
}
loadSets()
this.updateResourceSetsToShow = () => {
this.resourceSetsToShow = slice(this.resourceSets, this.resourceSetsPerPage * this.pageIndex, this.resourceSetsPerPage * (this.pageIndex + 1))
}
const getList = (ids, list) => {
const collection = []
forEach(ids, id => {
const item = find(list, item => item.id === id)
if (item) {
collection.push(item)
}
})
return collection
}
this.getUsers = (ids) => getList(ids, users)
this.getGroups = (ids) => getList(ids, groups)
this.getObjectsByType = (arr) => {
const objects = {}
forEach(arr, id => {
const obj = this.get(id)
if (!objects[obj.type]) {
objects[obj.type] = []
}
objects[obj.type].push(obj)
})
return objects
}
$scope.$watch('ctrl.resourceSet', (resourceSet) => {
if (!resourceSet) {
return
}
this.cpusStats = [0, 0]
this.memoryStats = [0, 0]
this.diskStats = [0, 0]
if (resourceSet.limits.cpus) {
this.cpusStats = [resourceSet.limits.cpus.total - resourceSet.limits.cpus.available, resourceSet.limits.cpus.available]
}
if (resourceSet.limits.memory) {
this.memoryStats = [resourceSet.limits.memory.total - resourceSet.limits.memory.available, resourceSet.limits.memory.available]
}
if (resourceSet.limits.disk) {
this.diskStats = [resourceSet.limits.disk.total - resourceSet.limits.disk.available, resourceSet.limits.disk.available]
}
})
})
// A module exports its name.
.name

View File

@@ -0,0 +1,123 @@
.panel.panel-default.alert.alert-danger.text-center(ng-if="!ctrl.resourceSets.length")
| No resource set found.
a(ui-sref = 'self.admin') Create one here.
.grid(ng-if="ctrl.resourceSets.length")
.panel.panel-default
.col-sm-4(ng-if="ctrl.resourceSets.length <= ctrl.resourceSetsPerPage")
button.btn.btn-default.col-sm-1.col-sm-offset-3(
ng-if="ctrl.resourceSets.length > ctrl.resourceSetsPerPage"
style="margin-top:4px;margin-bottom:4px;"
ng-disabled="ctrl.pageIndex === 0"
ng-click="ctrl.pageIndex = ctrl.pageIndex - 1; ctrl.updateResourceSetsToShow()"
)
i.fa.fa-chevron-left
p.page-title.col-sm-4
i.fa.xo-icon-cpu(style="color: #e25440;")
| Dashboard
span(ng-if="ctrl.resourceSets.length > ctrl.resourceSetsPerPage" style="font-size: 0.8em") ({{ ctrl.pageIndex + 1 }}/{{ ctrl.numberOfPages }})
button.btn.btn-default.col-sm-1(
ng-if="ctrl.resourceSets.length > ctrl.resourceSetsPerPage"
style="margin-top:4px;margin-bottom:4px;"
ng-disabled="ctrl.pageIndex + 1 === ctrl.numberOfPages"
ng-click="ctrl.pageIndex = ctrl.pageIndex + 1; ctrl.updateResourceSetsToShow()"
)
i.fa.fa-chevron-right
.well.panel(
ng-repeat="resourceSet in ctrl.resourceSetsToShow"
ng-init="users = ctrl.getUsers(resourceSet.subjects); groups = ctrl.getGroups(resourceSet.subjects); objects = ctrl.getObjectsByType(resourceSet.objects); showDetails = false"
)
.panel.panel-default
.grid
.col-sm-4
p.page-title.col-sm-4 {{ resourceSet.name }}
.col-sm-4(style="padding-right:4px")
button.btn.btn-default.pull-right(ng-click="showDetails = !showDetails" style="margin-top:4px; margin-bottom:4px;")
i.fa(ng-class="{'fa-chevron-up': showDetails, 'fa-chevron-down': !showDetails}")
.panel.panel-default(ng-if="showDetails")
ul.list-group
li.list-group-item(ng-if="users.length")
.grid
label.control-label.col-sm-2
i.xo-icon-user
| Users:
.col-sm-10
span(ng-repeat="user in users track by user.id") {{ user.email }}
span(ng-if="!$last") ,
li.list-group-item(ng-if="groups.length")
.grid
label.control-label.col-sm-2
i.xo-icon-group
| Groups:
.col-sm-10
span(ng-repeat="group in groups track by group.id") {{ group.name }}
span(ng-if="!$last") ,
li.list-group-item(ng-if="objects['VM-template'].length")
.grid
label.control-label.col-sm-2
i.xo-icon-vm
| Templates:
.col-sm-10
span(ng-repeat="template in objects['VM-template'] track by template.id") {{ template.name_label }}
span(ng-if="!$last") ,
li.list-group-item(ng-if="objects.SR.length")
.grid
label.control-label.col-sm-2
i.xo-icon-sr
| SRs:
.col-sm-10
span(ng-repeat="sr in objects.SR track by sr.id") {{ sr.name_label }} ({{ ctrl.get(sr.$container).name_label }})
span(ng-if="!$last") ,
li.list-group-item(ng-if="objects.network.length")
.grid
label.control-label.col-sm-2
i.xo-icon-network
| Networks:
.col-sm-10
span(ng-repeat="network in objects.network track by network.id") {{ network.name_label }} ({{ ctrl.get(network.$pool).name_label }})
span(ng-if="!$last") ,
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-cpu
| vCPUs
span(ng-if="resourceSet.limits.cpus" style="font-variant: normal; font-weight: normal") ({{ resourceSet.limits.cpus.total }})
.panel-body.text-center(style="height:185px")
canvas(
ng-if="resourceSet.limits.cpus"
class="chart chart-doughnut"
data="[resourceSet.limits.cpus.total - resourceSet.limits.cpus.available, resourceSet.limits.cpus.available]"
labels="['Used', 'Available']"
options='{responsive: false,tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %> vCPUs"}'
)
p.big-stat(ng-if="!resourceSet.limits.cpus || !resourceSet.limits.cpus.total") &infin;
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| Memory
span(ng-if="resourceSet.limits.memory" style="font-variant: normal; font-weight: normal") ({{ resourceSet.limits.memory.total | bytesToSize }})
.panel-body.text-center(style="height:185px")
canvas(
ng-if="resourceSet.limits.memory"
class="chart chart-doughnut"
data="[resourceSet.limits.memory.total - resourceSet.limits.memory.available, resourceSet.limits.memory.available]"
labels="['Used', 'Available']"
options='{responsive: false,tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}'
)
p.big-stat(ng-if="!resourceSet.limits.memory || !resourceSet.limits.memory.total") &infin;
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr
| Storage
span(ng-if="resourceSet.limits.disk" style="font-variant: normal; font-weight: normal") ({{ resourceSet.limits.disk.total | bytesToSize }})
.panel-body.text-center(style="height:185px")
canvas(
ng-if="resourceSet.limits.disk"
class="chart chart-doughnut"
data="[resourceSet.limits.disk.total - resourceSet.limits.disk.available, resourceSet.limits.disk.available]"
labels="['Used', 'Available']"
options='{responsive: false,tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}'
)
p.big-stat(ng-if="!resourceSet.limits.disk || !resourceSet.limits.disk.total") &infin;

34
app/modules/self/index.js Normal file
View File

@@ -0,0 +1,34 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import admin from './admin'
import dashboard from './dashboard'
import view from './view'
export default angular.module('self', [
uiRouter,
admin,
dashboard
])
.config(function ($stateProvider) {
$stateProvider.state('self', {
abstract: true,
data: {
requireAdmin: true
},
template: view,
url: '/self'
})
// Redirect to default sub-state.
$stateProvider.state('self.index', {
url: '',
controller: function ($state) {
$state.go('self.dashboard')
}
})
})
.name

View File

@@ -0,0 +1,12 @@
.menu-grid
.side-menu
ul.nav
li
a(ui-sref = '.dashboard', ui-sref-active = 'active')
i.fa.fa-fw.xo-icon-cpu.fa-menu
span.menu-entry Dashboard
li
a(ui-sref = '.admin')
i.fa.fa-fw.fa-wrench.fa-menu
span.menu-entry Administration
.side-content(ui-view = '')

View File

@@ -1,80 +1,131 @@
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import uiSelect from 'angular-ui-select';
import angular from 'angular'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import filter from 'lodash.filter';
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 xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view';
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,
xoApi,
xoServices,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.acls', {
controller: 'SettingsAcls as ctrl',
url: '/acls',
resolve: {
acls(xo) {
return xo.acl.get();
users (xo) {
return xo.user.getAll()
},
users(xo) {
return xo.user.getAll();
groups (xo) {
return xo.group.getAll()
},
roles (xo) {
return xo.role.getAll()
}
},
template: view,
});
template: view
})
})
.controller('SettingsAcls', function ($scope, acls, users, 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.users = users;
this.types = Object.keys(HIGH_LEVEL_OBJECTS)
this.selectedTypes = {}
this.users = users
this.roles = roles
this.groups = groups
{
let usersById = this.usersById = Object.create(null);
let usersById = this.usersById = Object.create(null)
for (let user of users) {
usersById[user.id] = user;
usersById[user.id] = user
}
let groupsById = this.groupsById = Object.create(null)
for (let group of groups) {
groupsById[group.id] = group
}
let rolesById = this.rolesById = Object.create(null)
for (let role of roles) {
rolesById[role.id] = role
}
}
this.objects = xoApi.all;
this.entities = this.users.concat(this.groups)
let refreshAcls = () => {
xo.acl.get().then(acls => {
this.acls = acls;
});
};
this.objects = xoApi.all
this.getUser = (id) => {
for (let user of this.users) {
if (user.id === id) {
return user;
return user
}
}
};
}
this.addAcl = () => {
xo.acl.add(this.subject.id, this.object.id).then(refreshAcls);
};
this.removeAcl = (subject, object) => {
xo.acl.remove(subject, object).then(refreshAcls);
};
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);
let isHighLevel = (object) => HIGH_LEVEL_OBJECTS[object.type]
return (objects) => filter(objects, isHighLevel)
})
.name
;

View File

@@ -1,66 +1,88 @@
.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'
)
ui-select(ng-model = 'ctrl.subject')
ui-select-match(placeholder = 'Choose a user or group')
div
i.fa.fa-user
| {{$select.selected.email}}
ui-select-choices(
repeat = 'user in ctrl.users | filter:{ permission: "!admin" } | filter:$select.search'
)
span(ng-if = '$select.selected.email')
i.xo-icon-user.fa-fw
| {{$select.selected.email}}
span(ng-if = '$select.selected.name')
i.xo-icon-group.fa-fw
| {{$select.selected.name}}
ui-select-choices(repeat = 'entity in ctrl.entities | filter:{ permission: "!admin" } | filter:$select.search')
div
i.fa.fa-user
| {{user.email}}
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}}
.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}}
button.btn.btn-success
i.fa.fa-plus
| Create
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')
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"')
div
i(class = 'xo-icon-{{role.type | lowercase}}')
| {{role.name}}
.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
tr
th User
th Object
th Action
tr(ng-repeat = 'acl in ctrl.acls')
td {{ctrl.usersById[acl.subject].email}}
th Role
th
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
button.btn.btn-sm.btn-danger(ng-click = 'ctrl.removeAcl(acl.subject, acl.object)')
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

@@ -0,0 +1,160 @@
import angular from 'angular'
import filter from 'lodash.filter'
import find from 'lodash.find'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import uiEvent from 'angular-ui-event'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
export default angular.module('settings.group', [
uiRouter,
uiSelect,
uiEvent,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.group', {
controller: 'SettingsGroup as ctrl',
url: '/group/:groupId',
resolve: {
acls (xo) {
return xo.acl.get()
},
groups (xo) {
return xo.group.getAll()
},
roles (xo) {
return xo.role.getAll()
},
users (xo) {
return xo.user.getAll()
}
},
template: view
})
})
.controller('SettingsGroup', function ($scope, $state, $stateParams, $interval, acls, groups, roles, users, xoApi, xo) {
this.acls = acls
this.roles = roles
this.users = users
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
{
let rolesById = Object.create(null)
for (let role of roles) {
rolesById[role.id] = role
}
this.rolesById = rolesById
}
this.objects = xoApi.all
this.removals = Object.create(null)
const findGroup = groups => {
this.group = filter(groups, gr => gr.id === $stateParams.groupId).pop()
if (!this.group) {
$state.go('settings.groups')
}
}
findGroup(groups)
const refreshUsers = () => {
xo.user.getAll().then(users => {
this.users = users
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
})
}
const refreshGroups = () => {
if (!this.isModified()) {
xo.group.getAll().then(groups => findGroup(groups))
}
}
const refreshAcls = () => {
xo.acl.get().then(acls => {
this.acls = acls
})
}
const interval = $interval(() => {
refreshUsers()
refreshGroups()
}, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.addUserToGroup = (group, user) => {
if (user !== null) {
group.users.push(user.id)
this.addedUser = null
this.modified = true
}
}
this.saveGroup = (group) => {
const users = []
group.users.forEach(user => {
let remove = this.removals && this.removals[user]
if (!remove) {
users.push(user)
}
})
this.removals = Object.create(null)
xo.group.setUsers(group.id, users)
.then(() => {
group.users = users
this.modified = false
})
}
this.cancelEdition = () => {
this.modified = false
this.removals = Object.create(null)
refreshGroups()
}
this.isModified = () => this.modified || Object.keys(this.removals).length
this.matchesGroup = acl => {
return acl.subject === this.group.id
}
this.removeAcl = (object, role) => {
xo.acl.remove(this.group.id, object, role).then(refreshAcls)
}
})
.filter('notInGroup', function () {
return function (users, group) {
const filtered = []
users.forEach(user => {
if (!group.users || group.users.indexOf(user.id) === -1) {
filtered.push(user)
}
})
return filtered
}
})
.filter('canAccess', () => {
return (objects, group, acls) => {
const accessed = []
const groupAcls = filter(acls, acl => acl.subject === group.id)
groupAcls.forEach(acl => {
const found = find(objects, object => object.id === acl.object)
found && accessed.push(found)
})
return accessed
}
})
.name

View File

@@ -0,0 +1,69 @@
.grid-sm
.panel.panel-default
p.page-title
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-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-street-view
| Members&nbsp;
span(ng-if = 'ctrl.isModified()') (*)
.panel-body
ul.list-group(ng-if = '!ctrl.group.users.length')
li.list-group-item.disabled: em (empty)
ul.list-group(ng-if = 'ctrl.group.users.length')
li.list-group-item(ng-repeat = 'user in ctrl.group.users')
span(ng-if = '!ctrl.removals[user]') {{ ctrl.userEmails[user] }}&nbsp;
del(ng-if = 'ctrl.removals[user]') {{ ctrl.userEmails[user] }}&nbsp;
span.pull-right
label
input.hidden(type = 'checkbox', ng-model = 'ctrl.removals[user]')
| &nbsp;
i.fa.fa-trash-o(tooltip="Remove user from group", style = 'cursor: pointer')
p
ui-select(ng-if = '(ctrl.users | notInGroup:ctrl.group).length', ng-model = 'ctrl.addedUser', on-select = 'ctrl.addUserToGroup(ctrl.group, ctrl.addedUser)')
ui-select-match(
placeholder = 'Choose a user to add'
) {{$select.selected.email}}
ui-select-choices(
repeat = 'addedUser in ctrl.users | notInGroup:ctrl.group | filter:$select.search'
) {{addedUser.email}}
em.text-muted(ng-if = '!(ctrl.users | notInGroup:ctrl.group).length') No available users to add
button.btn.btn-primary(ng-if = 'ctrl.isModified()', type="button", ng-click = 'ctrl.saveGroup(ctrl.group)')
i.fa.fa-save
| Save
| &nbsp;
button.btn.btn-default(ng-if = 'ctrl.isModified()', type="button", ng-click = 'ctrl.cancelEdition()')
i.fa.fa-times
| Cancel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-key
| ACLs&nbsp;
.panel-body
table.table.table-hover
tr
th Object
th Role
th
tr(ng-repeat = 'acl in ctrl.acls | filter:ctrl.matchesGroup track by acl.id')
td {{(acl.object | resolve).name_label}}
td {{ ctrl.rolesById[acl.action].name }}
td
button.btn.btn-danger(ng-click = 'ctrl.removeAcl(acl.object, acl.action)')
i.fa.fa-trash
.panel.panel-default
.panel-heading.panel-title
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"]')
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}})

View File

@@ -0,0 +1,189 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import uiEvent from 'angular-ui-event'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
import modal from './modal'
export default angular.module('settings.groups', [
uiRouter,
uiSelect,
uiEvent,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.groups', {
controller: 'SettingsGroups as ctrl',
url: '/groups',
resolve: {
users (xo) {
return xo.user.getAll()
},
groups (xo) {
return xo.group.getAll()
}
},
template: view
})
})
.controller('SettingsGroups', function ($scope, $interval, users, groups, xoApi, xo, $modal) {
this.uiCollapse = Object.create(null)
this.addedUsers = []
this.users = users
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
this.groups = groups
const selectedGroups = this.selectedGroups = {}
this.newGroups = []
const refreshUsers = () => {
xo.user.getAll().then(users => {
this.users = users
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
})
}
const refreshGroups = () => {
if (!this._editingGroup && !this.modified) {
return xo.group.getAll().then(groups => this.groups = groups)
} else {
return this.groups
}
}
const interval = $interval(() => {
refreshUsers()
refreshGroups()
}, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.addGroup = () => {
this.newGroups.push({
// Fake (unique) id needed by Angular.JS
id: Math.random()
})
}
if (!this.groups.length) {
this.addGroup()
}
this.deleteGroup = id => {
const modalInstance = $modal.open({
template: modal,
backdrop: false
})
return modalInstance.result
.then(() => {
return xo.group.delete(id)
.then(() => {
return refreshGroups()
})
.then(groups => {
if (!groups.length) {
this.addGroup()
}
})
})
.catch(() => {})
}
this.saveGroups = () => {
const newGroups = this.newGroups
const groups = this.groups
const updateGroups = []
for (let i = 0, len = groups.length; i < len; i++) {
const group = groups[i]
const {id} = group
if (selectedGroups[id]) {
delete selectedGroups[id]
xo.group.delete(id)
} else {
xo.group.set(group)
updateGroups.push(group)
}
}
for (let i = 0, len = newGroups.length; i < len; i++) {
const group = newGroups[i]
const {name} = group
if (!name) {
continue
}
xo.group.create({name})
.then(function (id) {
group.id = id
group.users = []
})
updateGroups.push(group)
}
this.groups = updateGroups
this.newGroups.length = 0
this.modified = false
if (!this.groups.length) {
this.addGroup()
}
}
this.addUserToGroup = (group, index) => {
group.users.push(this.addedUsers[index].id)
delete this.addedUsers[index]
}
this.flagUserRemoval = (group, index, remove) => {
group.removals || (group.removals = {})
group.removals[group.users[index]] = remove
}
this.saveGroup = (group) => {
const users = []
group.users.forEach(user => {
let remove = group.removals && group.removals[user]
if (!remove) {
users.push(user)
}
})
group.removals && delete group.removals
xo.group.setUsers(group.id, users)
.then(() => {
group.users = users
this.uiCollapse[group.id] = false
})
}
this.editingGroup = (editing = undefined) => editing !== undefined && (this._editingGroup = editing) || this._editingGroup
this.cancelModifications = () => {
this.newGroups.length = 0
this.editingGroup(false)
this.modified = false
refreshGroups()
}
})
.filter('notInGroup', function () {
return function (users, group) {
const filtered = []
users.forEach(user => {
if (!group.users || group.users.indexOf(user.id) === -1) {
filtered.push(user)
}
})
return filtered
}
})
.name

View File

@@ -0,0 +1,12 @@
.modal-header
button.close(
type = 'button',
ng-click = '$dismiss()'
)
span(aria-hidden = 'true') &times;
h4.modal-title Confirm group suppression
.modal-body
p Are you sure you want to delete this group ? It's user list and associated ACLs will be lost after that.
button.btn.btn-default(type = 'button', ng-click = '$close()') Ok
| &ensp;
button.btn.btn-default(type = 'button', ng-click = '$dismiss()') Cancel

View File

@@ -0,0 +1,49 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-group(style="color: #e25440;")
| Groups
.grid-sm
.panel.panel-default
form(ng-submit="ctrl.saveGroups()", autocomplete="off").panel-body
table.table.table-hover
tr
th.col-md-5 Name
th.col-md-5 Information
th.col-md-2
tr(ng-repeat="group in ctrl.groups | orderBy:natural('id') track by group.id")
td
input.form-control(type="text", ng-model="group.name", ui-event = '{focus: "ctrl.editingGroup(true)", blur: "ctrl.editingGroup(false)"}', ng-change = 'ctrl.modified = true')
td
span(ng-if = '!group.users.length'): em (empty)
span(ng-if = 'group.users.length')
strong {{ group.users.length }} members:&nbsp;
span(ng-repeat = 'user in group.users | limitTo:4')
| {{ ctrl.userEmails[user] }}{{ $last ? (group.users.length > 4 ? ',...' : '') : ', ' }}
| &nbsp;
td
a.btn.btn-primary(ui-sref = 'settings.group({groupId: group.id})')
| Edit&nbsp;
i.fa.fa-pencil
| &nbsp;
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.deleteGroup(group.id)')
i.fa.fa-trash
tr(ng-repeat="group in ctrl.newGroups")
td
input.form-control(type = "text", ng-model = "group.name", placeholder = "New group name", ng-change = 'ctrl.modified = true')
td
button.btn.btn-btn-default(type = 'button', ng-click = 'ctrl.newGroups.splice($index, 1)')
i.fa.fa-times
td &#160;
p
button.btn.btn-success(type="button", ng-click="ctrl.addGroup()")
i.fa.fa-plus
| &nbsp;
span(ng-if = 'ctrl.modified')
button.btn.btn-primary(type="submit")
i.fa.fa-save
| Save
| &nbsp;
button.btn.btn-default(type="button", ng-click = "ctrl.cancelModifications()")
i.fa.fa-times
| Cancel

View File

@@ -1,33 +1,45 @@
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import acls from './acls';
import servers from './servers';
import users from './users';
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';
import view from './view'
export default angular.module('settings', [
uiRouter,
acls,
group,
groups,
plugins,
servers,
users,
update,
user,
users
])
.config(function ($stateProvider) {
$stateProvider.state('settings', {
abstract: true,
data: {
requireAdmin: true
},
template: view,
url: '/settings',
});
url: '/settings'
})
// Redirect to default sub-state.
$stateProvider.state('settings.index', {
url: '',
controller: function ($state) {
$state.go('settings.servers');
$state.go('settings.servers')
}
});
})
})
.name
;

View File

@@ -0,0 +1,213 @@
import angular from 'angular'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import includes from 'lodash.includes'
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'
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 setOptionalProperties (configurationSchema) {
if (!configurationSchema) {
return
}
forEach(configurationSchema.properties, (property, key) => {
let { required } = configurationSchema
if (!required) {
required = configurationSchema.required = []
}
property.optional = !includes(required, key)
const { type, items } = property
if (type === 'object') {
setOptionalProperties(property)
} else if (type === 'array' && items && items.type === 'object') {
setOptionalProperties(items)
}
})
}
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 => {
const { configurationSchema } = plugin
plugin._loaded = plugin.loaded
plugin._autoload = plugin.autoload
if (!plugin.configuration) {
plugin.configuration = {}
}
setOptionalProperties(configurationSchema)
loadDefaults(configurationSchema, plugin.configuration)
}
const refreshPlugin = id => {
return 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(() => {
this.disabled[id] = false
})
}
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'
})
refreshPlugin(plugin.id)
})
.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(() => {
refreshPlugin(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)
refreshPlugin(plugin.id)
}
}
})
.name

View File

@@ -0,0 +1,52 @@
.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
.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;&nbsp;
span.text-info {{ plugin.name }}
span(style="font-size:0.7em") (v{{ plugin.version }})
.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(form = 'pluginform' ng-if = 'plugin.configurationSchema', ng-submit = 'ctrl.configure(plugin)')
fieldset(ng-disabled = 'ctrl.disabled[plugin.id]')
object-input(
form = '"pluginform"',
property = 'plugin.configurationSchema',
model = 'plugin.configuration',
group = ''
)
.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,127 +1,148 @@
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import uiSelect from 'angular-ui-select';
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'
import filter from 'lodash.filter';
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import xoApi from 'xo-api';
import xoServices from 'xo-services';
import view from './view';
import view from './view'
export default angular.module('settings.servers', [
uiRouter,
uiSelect,
xoApi,
xoServices,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.servers', {
controller: 'SettingsServers as ctrl',
url: '/servers',
resolve: {
servers(xo) {
return xo.server.getAll();
},
servers (xo) {
return xo.server.getAll()
}
},
template: view,
});
template: view
})
})
.controller('SettingsServers', function ($scope, $interval, servers, xoApi, xo, notify) {
this.servers = servers;
const selected = this.selectedServers = {};
const newServers = this.newServers = [];
.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...'
});
notify.info({
title: 'Server connect',
message: 'Connecting the server...'
})
xo.server.connect(id).catch(error => {
notify.error({
title: 'Server connection error',
message: error.message
});
});
};
})
})
}
this.disconnectServer = (id) => {
notify.info ({
title: 'Server disconnect',
message: 'Disconnecting the server...'
});
xo.server.disconnect(id);
};
notify.info({
title: 'Server disconnect',
message: 'Disconnecting the server...'
})
xo.server.disconnect(id)
}
this.addServer = () => {
newServers.push({
// Fake (unique) id needed by Angular.JS
id: Math.random(),
status: 'connecting'
});
};
})
}
this.addServer();
this.addServer()
this.saveServers = () => {
const newServers = this.newServers;
const servers = this.servers;
const updateServers = [];
const addresses = []
forEach(xoApi.getView('host').all, host => addresses.push(host.address))
const newServers = this.newServers
const servers = this.servers
const updateServers = []
for (let i = 0, len = servers.length; i < len; i++) {
const server = servers[i];
const {id} = server;
const server = servers[i]
const {id} = server
if (selected[id]) {
delete selected[id];
xo.server.remove(id);
}
else {
delete selected[id]
xo.server.remove(id)
} else {
if (!server.password) {
delete server.password;
delete server.password
}
xo.server.set(server);
delete server.password;
updateServers.push(server);
server.readOnly = $scope.readOnly[id]
xo.server.set(server)
delete server.password
updateServers.push(server)
}
}
for (let i = 0, len = newServers.length; i < len; i++) {
const server = newServers[i];
const {host, username, password} = server;
const server = newServers[i]
const {host, username, password, readOnly} = server
if (!host) {
continue;
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,
autoConnect: false,
}).then(function(id) {
server.id = id;
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',
message: error.message
});
});
});
delete server.password;
updateServers.push(server);
})
})
})
delete server.password
updateServers.push(server)
}
this.servers = updateServers;
this.newServers.length = 0;
this.addServer();
};
this.servers = updateServers
this.newServers.length = 0
this.addServer()
}
})
.name
;

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

@@ -0,0 +1,99 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import _assign from 'lodash.assign'
import ansiUp from 'ansi_up'
import updater from '../../updater'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import {AuthenticationFailed} from '../../updater'
import view from './view'
export default angular.module('settings.update', [
uiRouter,
updater,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.update', {
controller: 'SettingsUpdate as ctrl',
url: '/update',
onExit: updater => {
updater.removeAllListeners('end')
},
template: view
})
})
.filter('ansitohtml', function ($sce) {
return function (input) {
return $sce.trustAsHtml(ansiUp.ansi_to_html(input))
}
})
.controller('SettingsUpdate', function (xoApi, xo, updater, notify) {
this.updater = updater
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, renewRegister) => {
this.regPwd = ''
this.updater.register(email, password, renewRegister)
.tap(() => this.renewRegister = false)
.then(() => this.updater.update())
.catch(AuthenticationFailed, () => {})
.catch(err => console.error(err))
}
this.update = () => {
this.updater.update()
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message
}))
}
this.upgrade = () => {
this.updater.upgrade()
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message
}))
}
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
}))
.finally(() => this.update())
}
this.valid = trial => {
return trial && trial.end && Date.now() < trial.end
}
})
.name

View File

@@ -0,0 +1,128 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-refresh(style="color: #e25440;")
| Update
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-globe
| Status
.panel-body
p(ng-if = '!ctrl.updater.state')
a.btn.btn-warning: i.fa.fa-question-circle(ng-if = '!ctrl.updater.state', tooltip = 'No update information available')
| &nbsp;No update information available&nbsp;
a.btn.btn-default(ng-class = '{disabled: ctrl.updater.isConnected}', ng-click = 'ctrl.update()')
i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.isConnected}')
.form-group(ng-if = 'ctrl.updater.state && ctrl.updater.state === "registerNeeded"')
a.btn.btn-warning(ng-if = 'ctrl.updater.state === "registerNeeded"'): i.fa.fa-bell-slash(tooltip = 'Your XOA is not registered for updates')
| &nbsp;Registration needed&nbsp;
button.btn.btn-default(ng-if = 'ctrl.updater.registerState === "registered"', ng-click = 'ctrl.updater.update()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}'): i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.updating || ctrl.updater.upgrading}')
.form-group(ng-if = 'ctrl.updater.state && ctrl.updater.state !== "registerNeeded"')
a.btn.btn-info(ng-if = 'ctrl.updater.state === "connected"'): i.fa.fa-question-circle(tooltip = 'Update information may be available')
a.btn.btn-success(ng-if = 'ctrl.updater.state === "upToDate"'): i.fa.fa-check(tooltip = 'Your XOA is up-to-date')
a.btn.btn-primary(ng-if = 'ctrl.updater.state === "upgradeNeeded"'): i.fa.fa-bell(tooltip = 'You need to update your XOA (new version is available)')
a.btn.btn-danger(ng-if = 'ctrl.updater.state === "error"'): i.fa.fa-exclamation-triangle(tooltip = 'Can\'t fetch update information')
| &nbsp;
button#update.btn.btn-info(type = 'button', ng-click = 'ctrl.update()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}')
| Check for updates&nbsp;
i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.updating}')
| &nbsp;
button#upgrade.btn.btn-primary(ng-if = 'ctrl.updater.state === "upgradeNeeded"', type = 'button', ng-click = 'ctrl.upgrade()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}')
| Upgrade&nbsp;
i.fa.fa-cog(ng-class = '{"fa-spin": ctrl.updater.upgrading}')
div
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(style = 'word-wrap: break-word;', ng-bind-html = 'entry.message | ansitohtml')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-pencil
| Registration
.panel-body.text-center
.text-warning(ng-if = '!ctrl.updater.state || ctrl.updater.registerState === "unknown"')
| No registration information available.
br
span.big-stat
i.fa.fa-exclamation-triangle.text-warning
div(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "error"')
.text-danger Can't fetch registration information.
br
span.big-stat
i.fa.fa-exclamation-triangle.text-danger
br
.text-danger {{ ctrl.updater.registerError }}
br
button.btn.btn-default(type = 'button', ng-click = 'ctrl.updater.isRegistered()')
i.fa.fa-refresh
| Refresh
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
span.input-group-addon: i.fa.fa-envelope-o.fa-fw
label.sr-only(for = 'regEmail') Email
input#regEmail.form-control(type = 'email', placeholder = 'Email', ng-model = 'ctrl.regEmail', required)
.form-group
.input-group
span.input-group-addon: i.fa.fa-key.fa-fw
label.sr-only(for = 'regPwd') Email
input#regPwd.form-control(type = 'password', placeholder = 'Password', ng-model = 'ctrl.regPwd', required)
.form-group
button.btn.btn-primary(type = 'submit')
i.fa.fa-check
| Register
p.form-static-control.text-danger {{ ctrl.updater.registerError }}
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
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
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyUser', placeholder = 'User name', required)
| &nbsp;
.form-group
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,35 +1,36 @@
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import uiSelect from 'angular-ui-select';
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'
import filter from 'lodash.filter';
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import xoApi from 'xo-api';
import xoServices from 'xo-services';
import view from './view';
import view from './view'
export default angular.module('settings.users', [
uiRouter,
uiSelect,
uiEvent,
xoApi,
xoServices,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.users', {
controller: 'SettingsUsers as ctrl',
url: '/users',
resolve: {
users(xo) {
return xo.user.getAll();
},
users (xo) {
return xo.user.getAll()
}
},
template: view,
});
template: view
})
})
.controller('SettingsUsers', function ($scope, $interval, users, xoApi, xo) {
this.users = users;
this.users = users
this.permissions = [
{
label: 'User',
@@ -39,71 +40,93 @@ export default angular.module('settings.users', [
label: 'Admin',
value: 'admin'
}
];
const selected = this.selectedUsers = {};
const newUsers = this.newUsers = [];
]
const refreshUsers = () => {
xo.user.getAll().then(users => {
this.users = users;
});
};
const selected = this.selectedUsers = {}
this.newUsers = []
const interval = $interval(refreshUsers, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.addUser = () => {
newUsers.push({
// Fake (unique) id needed by Angular.JS
id: Math.random(),
permission: 'none'
});
};
this.addUser();
this.saveUsers = () => {
const newUsers = this.newUsers;
const users = this.users;
const updateUsers = [];
for (let i = 0, len = users.length; i < len; i++) {
const user = users[i];
const {id} = user;
if (selected[id]) {
delete selected[id];
xo.user.delete(id);
const refreshUsers = () => {
if (!this._editingUser) {
xo.user.getAll().then(users => {
this.users = users
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
})
}
else {
if (!user.password) {
delete user.password;
}
const interval = $interval(() => {
refreshUsers()
}, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.addUser = () => {
this.newUsers.push({
// Fake (unique) id needed by Angular.JS
id: Math.random(),
permission: 'none'
})
}
this.addUser()
this.saveUsers = () => {
const newUsers = this.newUsers
const users = this.users
const updateUsers = []
for (let i = 0, len = users.length; i < len; i++) {
const user = users[i]
const {id} = user
if (selected[id]) {
delete selected[id]
xo.user.delete(id)
} else {
if (!user.password) {
delete user.password
}
xo.user.set(user)
delete user.password
updateUsers.push(user)
}
xo.user.set(user);
delete user.password;
updateUsers.push(user);
}
}
for (let i = 0, len = newUsers.length; i < len; i++) {
const user = newUsers[i];
const {email, permission, password} = user;
if (!email) {
continue;
for (let i = 0, len = newUsers.length; i < len; i++) {
const user = newUsers[i]
const {email, permission, password} = user
if (!email) {
continue
}
xo.user.create({
email,
permission,
password
}).then(function (id) {
user.id = id
})
delete user.password
updateUsers.push(user)
}
xo.user.create({
email,
permission,
password,
}).then(function(id) {
user.id = id;
});
delete user.password;
updateUsers.push(user);
this.users = updateUsers
this.newUsers.length = 0
this.userEmails = Object.create(null)
this.users.forEach(user => {
this.userEmails[user.id] = user.email
})
this.addUser()
}
this.editingUser = editing => {
this._editingUser = editing
}
this.generatePassword = (user) => {
// Generate password of 8 letters/numbers/underscore
user.password = passwordGenerator(8, false)
}
this.users = updateUsers;
this.newUsers.length = 0;
this.addUser();
};
})
.name
;

View File

@@ -1,55 +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('email') track by user.id")
td
input.form-control(type="text", ng-model="user.email")
td
select.form-control(ng-options="p.value as p.label for p in ctrl.permissions", ng-model="user.permission")
td
input.form-control(type="password", ng-model="user.password", placeholder="Fill to change the password")
td.text-center
input(type="checkbox", ng-model="ctrl.selectedUsers[user.id]")
tr(ng-repeat="user in ctrl.newUsers")
td
input.form-control(
type = "text"
ng-model = "user.email"
placeholder = "email"
)
td
select.form-control(
ng-options = "p.value as p.label for p in ctrl.permissions"
ng-model = "user.permission"
ng-required = "user.email"
)
td
input.form-control(
type = "password"
ng-model = "user.password"
ng-required = "user.email"
placeholder = "password"
)
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,20 +1,28 @@
.grid
//- 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
| Servers
span.menu-entry Servers
li
a(ui-sref = '.users')
i.fa.fa-fw.fa-user.fa-menu
| Users
//- a.disabled(ui-sref = '.groups')
//- i.fa.fa-fw.fa-users.fa-menu
//- | Groups
i.xo-icon-user.fa-fw.fa-menu
span.menu-entry Users
li
a(ui-sref = '.groups')
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
| ACLs
//- Content
div.settings-content(ui-view = '')
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
.side-content(ui-view = '')

View File

@@ -1,189 +1,263 @@
import angular from 'angular';
import isEmpty from 'isempty';
import uiRouter from 'angular-ui-router';
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 Bluebird from 'bluebird'
import view from './view';
import xoTag from 'tag'
//====================================================================
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.sr', [
uiRouter,
xoTag
])
.config(function ($stateProvider) {
$stateProvider.state('SRs_view', {
url: '/srs/:id',
controller: 'SrCtrl',
template: view,
});
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
$window.bytesToSize = bytesToSizeFilter; // FIXME dirty workaround to custom a Chart.js tooltip template
$scope.units = ['MiB', 'GiB', 'TiB']
let {get} = xoApi;
$scope.currentLogPage = 1
$scope.currentVDIPage = 1
let {get} = xoApi
$scope.$watch(() => xoApi.get($stateParams.id), function (SR) {
$scope.SR = 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;
let {SR} = $scope
let {name_label, name_description} = $data
$data = {
id: SR.UUID,
};
id: SR.id
}
if (name_label !== SR.name_label) {
$data.name_label = name_label;
$data.name_label = name_label
}
if (name_description !== SR.name_description) {
$data.name_description = name_description;
$data.name_description = name_description
}
return xoApi.call('sr.set', $data);
};
return xoApi.call('sr.set', $data)
}
$scope.deleteVDI = function (UUID) {
console.log('Delete VDI', UUID);
$scope.deleteVDI = function (id) {
console.log('Delete VDI', id)
return modal.confirm({
title: 'VDI deletion',
message: 'Are you sure you want to delete this VDI? This operation is irreversible.',
message: 'Are you sure you want to delete this VDI? This operation is irreversible.'
}).then(function () {
return xo.vdi.delete(UUID);
});
};
return xo.vdi.delete(id)
})
}
$scope.disconnectVBD = function (UUID) {
console.log('Disconnect VBD', UUID);
$scope.disconnectVBD = function (id) {
console.log('Disconnect VBD', id)
return xoApi.call('vbd.disconnect', {id: UUID});
};
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 (UUID) {
console.log('Connect PBD', UUID);
$scope.connectPBD = function (id) {
console.log('Connect PBD', id)
return xoApi.call('pbd.connect', {id: UUID});
};
return xo.pbd.connect(id)
}
$scope.disconnectPBD = function (UUID) {
console.log('Disconnect PBD', UUID);
$scope.disconnectPBD = function (id) {
console.log('Disconnect PBD', id)
return xoApi.call('pbd.disconnect', {id: UUID});
};
return xo.pbd.disconnect(id)
}
$scope.reconnectAllHosts = function () {
// TODO: return a Bluebird.all(promises).
for (let id of $scope.SR.$PBDs) {
let pbd = xoApi.get(id);
let pbd = xoApi.get(id)
xoApi.call('pbd.connect', {id: pbd.ref});
xoApi.call('pbd.connect', {id: pbd.id})
}
};
}
$scope.disconnectAllHosts = function () {
return modal.confirm({
title: 'Disconnect hosts',
message: 'Are you sure you want to disconnect all hosts to this SR?',
message: 'Are you sure you want to disconnect all hosts to this SR?'
}).then(function () {
for (let id of $scope.SR.$PBDs) {
let pbd = xoApi.get(id);
let pbd = xoApi.get(id)
xoApi.call('pbd.disconnect', {id: pbd.ref});
console.log(pbd.ref)
xoApi.call('pbd.disconnect', {id: pbd.id})
console.log(pbd.id)
}
});
};
})
}
$scope.rescanSr = function (UUID) {
console.log('Rescan SR', UUID);
$scope.rescanSr = function (id) {
console.log('Rescan SR', id)
return xoApi.call('sr.scan', {id: UUID});
};
return xoApi.call('sr.scan', {id: id})
}
$scope.removeSR = function (UUID) {
console.log('Remove SR', UUID);
$scope.removeSR = function (id) {
console.log('Remove SR', id)
return modal.confirm({
title: 'SR deletion',
message: 'Are you sure you want to delete this SR? This operation is irreversible.',
message: 'Are you sure you want to delete this SR? This operation is irreversible.'
}).then(function () {
return Bluebird.map($scope.SR.$PBDs, pbdId => {
let pbd = xoApi.get(pbdId);
let pbd = xoApi.get(pbdId)
return xoApi.call('pbd.disconnect', { id: pbd.id });
});
return xoApi.call('pbd.disconnect', { id: pbd.id })
})
}).then(function () {
return xoApi.call('sr.destroy', {id: UUID});
return xoApi.call('sr.destroy', {id: id})
}).then(function () {
$state.go('index');
$state.go('index')
notify.info({
title: 'SR remove',
message: 'SR is removed',
});
});
};
message: 'SR is removed'
})
})
}
$scope.forgetSR = function (UUID) {
console.log('Forget SR', UUID);
$scope.forgetSR = function (id) {
console.log('Forget SR', id)
return modal.confirm({
title: 'SR forget',
message: 'Are you sure you want to forget this SR? No VDI on this SR will be removed.',
message: 'Are you sure you want to forget this SR? No VDI on this SR will be removed.'
}).then(function () {
return Bluebird.map($scope.SR.$PBDs, pbdId => {
let pbd = xoApi.get(pbdId);
let pbd = xoApi.get(pbdId)
return xoApi.call('pbd.disconnect', { id: pbd.id });
});
return xoApi.call('pbd.disconnect', { id: pbd.id })
})
}).then(function () {
return xoApi.call('sr.forget', {id: UUID});
return xoApi.call('sr.forget', {id: id})
}).then(function () {
$state.go('index');
$state.go('index')
notify.info({
title: 'SR forget',
message: 'SR is forgotten',
});
});
};
message: 'SR is forgotten'
})
})
}
$scope.saveDisks = function (data) {
// Group data by disk.
let disks = {};
angular.forEach(data, function (value, key) {
let i = key.indexOf('/');
let disks = {}
let sizeChanges = false
forEach(data, function (value, key) {
let i = key.indexOf('/')
let id = key.slice(0, i);
let prop = key.slice(i + 1);
let id = key.slice(0, i)
let prop = key.slice(i + 1)
(disks[id] || (disks[id] = {}))[prop] = value;
});
;(disks[id] || (disks[id] = {}))[prop] = value
})
let promises = [];
angular.forEach(disks, function (attributes, id) {
// Keep only changed attributes.
let disk = get(id);
angular.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));
forEach(disks, function (attributes, id) {
let disk = get(id)
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)
})
}
})
// A module exports its name.
.name
;

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 Edit", 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,16 +26,13 @@
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")
| Yes (
a(ui-sref="pools_view({id: container.UUID})") {{container.name_label}}
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
| )
dd(ng-if="'host' === container.type") No
dt Size
@@ -51,99 +50,139 @@
| Save
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-stats(style="color: #e25440;")
i.xo-icon-stats
| Stats
.panel-body
.grid
.grid-cell
p.stat-name Physical Alloc:
canvas(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) %>"}')
.grid-cell
p.stat-name Virtual Alloc:
canvas(id="doughnut", class="chart chart-doughnut", data="[(SR.usage), (SR.size - SR.usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
.grid-cell
.row
.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
p.stat-name VDIs:
p.center.big-stat {{SR.VDIs.length}}
//- 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.UUID)")
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.UUID)")
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.UUID)")
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.UUID", role="progressbar", aria-valuemin="0", aria-valuenow="{{VDI.size}}", aria-valuemax="{{SR.size}}", style="width: {{[VDI.size, SR.size] | %}}", tooltip="{{VDI.name_label}} ({{[VDI.size, SR.size] | %}})")
.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(tooltip="Rescan", ng-click="rescanSr(SR.UUID)")
span.quick-edit(
ng-if="disksForm.$visible"
tooltip="Cancel Edit"
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')")
td
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.UUID}}/name_label'
e-name = '{{VDI.id}}/name_label'
)
| {{VDI.name_label}} &nbsp;
span.label.label-info(ng-if="VDI.$snapshot_of") snapshot
td
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"
e-name = '{{VDI.UUID}}/name_description'
e-name = '{{VDI.id}}/name_description'
)
| {{VDI.name_description}}
td
//- FIXME: should be editable, but the server needs first
//- to accept a human readable string.
| {{VDI.size | bytesToSize}}
td {{((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.UUID)")
i.fa.fa-unlink.fa-lg(tooltip="Disconnect this disk")
a(ng-if="!(VDI.$VBD | resolve).attached", xo-click="deleteVDI(VDI.UUID)")
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.
.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(
@@ -165,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
@@ -173,31 +212,33 @@
table.table.table-hover
th Name
th Status
tr(ng-repeat="PBD in SR.$PBDs | resolve", xo-sref="hosts_view({id: (PBD.host | resolve).UUID})")
tr(ng-repeat="PBD in SR.$PBDs | resolve", xo-sref="hosts_view({id: (PBD.host | resolve).id})")
td {{(PBD.host | resolve).name_label}}
td(ng-if="PBD.attached")
span.label.label-success Connected
span.pull-right.btn-group.quick-buttons
a(xo-click="disconnectPBD(PBD.UUID)")
i.fa.fa-unlink.fa-lg(tooltip="Disconnect to this host")
a(xo-click="disconnectPBD(PBD.id)")
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
a(xo-click="connectPBD(PBD.UUID)")
a(xo-click="connectPBD(PBD.id)")
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.length") No recent logs
table.table.table-hover(ng-if="SR.messages.length")
p.center(ng-if="SR.messages | isEmpty") No recent logs
table.table.table-hover(ng-if="SR.messages | isNotEmpty")
th.col-md-1 Date
th.col-md-1 Name
tr(ng-repeat="message in SR.messages | resolve | orderBy:'-time' track by message.UUID")
tr(ng-repeat="message in SR.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
td {{message.time*1e3 | date:"medium"}}
td
| {{message.name}}
a.quick-remove(tooltip="Remove log")
i.fa.fa-trash-o.fa-fw
.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,350 @@
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 jsonSchema from 'json-schema'
import mapValues from 'lodash.mapvalues'
import trim from 'lodash.trim'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import Bluebird from 'bluebird'
Bluebird.longStackTraces()
import view from './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
}
/**
* 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
}
export default angular.module('xoWebApp.taskscheduler.job', [
jsonSchema,
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()
const loadActions = () => xoApi.call('system.getMethodsInfo')
.then(response => {
const actions = []
for (let method in response) {
if (includes(jobCompliantMethods, method)) {
let [group, command] = method.split('.')
const properties = response[method].params
response[method].properties = properties
response[method].type = 'object'
delete response[method].params
for (const key in properties) {
const property = properties[key]
const type = getType(property)
if (type === 'string') {
if (group === 'acl') {
if (key === 'object') {
property.$type = 'XoObject'
} else if (key === 'action') {
property.$type = 'XoRole'
} else if (key === 'subject') {
property.$type = 'XoEntity'
}
} else if (group === 'host' && key === 'id') {
property.$type = 'Host'
} else if (group === 'vm' && key === 'id') {
property.$type = 'Vm'
} else {
if (includes(['pool', 'pool_id', 'target_pool_id'], key)) {
property.$type = 'Pool'
} else if (includes(['sr', 'sr_id', 'target_sr_id'], key)) {
property.$type = 'Sr'
} else if (includes(['host', 'host_id', 'target_host_id'], key)) {
property.$type = 'Host'
} else if (includes(['vm'], key)) {
property.$type = 'Vm'
}
}
}
}
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])
}
})
// A module exports its name.
.name

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