Compare commits

...

990 Commits

Author SHA1 Message Date
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
Julien Fontanet
b41c66c4af 3.9.1 2015-04-21 11:09:27 +02:00
Julien Fontanet
d6b7388c2e Merge branch 'next-release' 2015-04-21 11:09:20 +02:00
Julien Fontanet
b0dc681d48 Hide link to not implemented Groups page. 2015-04-21 11:04:43 +02:00
Julien Fontanet
c20b460f2d Fix link in ChangeLog. 2015-04-21 11:04:19 +02:00
Julien Fontanet
cd0a46fd7f ChangeLog 2015-04-21 10:53:56 +02:00
Julien Fontanet
0a9c868678 ngAnnotate is broken due to Babel. 2015-04-20 20:17:33 +02:00
Olivier Lambert
027d1e8cb1 notify when connect/disconnect a server 2015-04-20 19:59:11 +02:00
Olivier Lambert
a5c9880318 do not fetch stat if the VM has current operations 2015-04-20 19:59:11 +02:00
Julien Fontanet
e09988e1d7 3.9.0 2015-04-20 17:38:44 +02:00
Julien Fontanet
21a1629dfa Merge branch 'next-release' 2015-04-20 17:38:28 +02:00
Julien Fontanet
7644c626c8 ChangeLog 2015-04-20 17:25:32 +02:00
Julien Fontanet
2c91b73016 Update deps. 2015-04-20 16:21:39 +02:00
Olivier Lambert
ae54b8eba8 Remove useless notifications for VM import 2015-04-19 17:20:48 +02:00
Fabrice Marsaud
47bcaa8659 stat graph tooltip radius fixed 2015-04-15 18:46:16 +02:00
Olivier Lambert
73dc6fb050 fix URL for source versions 2015-04-15 16:44:33 +02:00
Olivier Lambert
a4ced510c6 refresh 10secs only and login page with icons 2015-04-15 13:36:09 +02:00
Olivier Lambert
40d9fe873c UI fixes for settings view 2015-04-14 23:36:44 +02:00
Olivier Lambert
d06fe4a7c7 settings view 2015-04-14 18:31:01 +02:00
Olivier Lambert
52d71e5c4a refactoring of Settings view. WIP 2015-04-14 17:27:28 +02:00
Olivier Lambert
d86c2b64f2 UI fixes in settings page 2015-04-14 00:04:03 +02:00
Olivier Lambert
84449f4e2e UI reworks for setting page. Work in progress 2015-04-13 19:00:43 +02:00
Julien Fontanet
835dbac81a Explicit the license. 2015-04-10 12:06:46 +02:00
Olivier Lambert
4d190b91a5 minor UI fix 2015-04-08 20:34:39 +02:00
Olivier Lambert
c4cddca84a UI fixes for graphs and various other minor fixes 2015-04-08 19:38:57 +02:00
Olivier Lambert
bfe9598ce4 ui fixes 2015-04-08 14:38:58 +02:00
Fabrice Marsaud
3421222c2f Fixed stat tooltip problems 2015-04-08 13:07:43 +02:00
Fabrice Marsaud
3ca95feb7c Stats ok when VM starts/stops/suspends/resumes/reboots 2015-04-08 13:07:42 +02:00
Olivier Lambert
903bc364ba just add xvd before disk letter to have the full disk name 2015-04-08 13:07:42 +02:00
Fabrice Marsaud
16fd498c29 stat icons for fun 2015-04-08 13:07:42 +02:00
Fabrice Marsaud
ca45776739 Trying to handle VM stop. Not satisfiying yet 2015-04-08 13:07:42 +02:00
Fabrice Marsaud
c849f06ce8 Stats are fetched without delay, with a UI message 2015-04-08 13:07:42 +02:00
Fabrice Marsaud
77aa74e9c3 better vm stat refresh implementation 2015-04-08 13:07:42 +02:00
Fabrice Marsaud
8dc0a92c52 fibonacci retries if VM stats fetch fails 2015-04-08 13:07:05 +02:00
Fabrice Marsaud
30b7cfb53d VM stats are not fetched if VM is not running 2015-04-08 13:06:15 +02:00
Olivier Lambert
0caeb71603 minor UI fixes 2015-04-08 13:05:31 +02:00
Fabrice Marsaud
a40ad2eab9 Vm stat graphs can be clicked for closer focus 2015-04-08 13:05:31 +02:00
Fabrice Marsaud
1fba0cd31f Readable time on stat tooltips 2015-04-08 13:04:18 +02:00
Fabrice Marsaud
e6ce672bc5 First delivery for #228 stat graphs 2015-04-08 13:04:18 +02:00
Olivier Lambert
1418f0b697 allow mulitples and variables series 2015-04-08 13:04:18 +02:00
Olivier Lambert
81aa972108 reorg code 2015-04-08 13:04:18 +02:00
Olivier Lambert
c36ac49cca major improvements 2015-04-08 13:04:18 +02:00
Olivier Lambert
5bf069f307 start to work on RRDs 2015-04-08 13:01:54 +02:00
Olivier Lambert
1837d3d291 fixes #225 2015-04-01 21:59:31 +02:00
Olivier Lambert
8fe3a788d7 add auto power. Fixes #224 2015-04-01 15:40:46 +02:00
Olivier Lambert
c622f9a295 add local LVM SR creation. Fixes #219 2015-03-30 12:02:50 +02:00
Olivier Lambert
ba72a48e02 add display of Vgpus and Gpus. Need feedback from Gpus users 2015-03-29 14:25:31 +02:00
Olivier Lambert
b1bd74c5b7 initial work on PCI attach 2015-03-29 01:49:02 +01:00
Julien Fontanet
8f3a5780a9 3.8.1 2015-03-27 15:56:37 +01:00
Julien Fontanet
177143b57c Merge branch 'next-release' 2015-03-27 15:56:31 +01:00
Julien Fontanet
36dcfc1517 Missing dep. 2015-03-27 15:56:04 +01:00
Julien Fontanet
50d8cb0b5d Do not use gulp-plumber in browserify in production.
This can cause problem as `npm run build` no longer return a non-zero
exit code if there was a problem.
2015-03-27 15:53:31 +01:00
Julien Fontanet
c7845c4009 3.8.0 2015-03-27 15:16:04 +01:00
Julien Fontanet
4ab23e4303 Merge branch 'next-release' 2015-03-27 15:15:57 +01:00
Julien Fontanet
2b42a44bc4 Typo. 2015-03-27 15:10:42 +01:00
Julien Fontanet
225d7501d3 Link ChangeLog to blog post. 2015-03-27 15:10:42 +01:00
Olivier Lambert
ab7af6a254 fix remove all logs in Pool view 2015-03-27 15:01:09 +01:00
Olivier Lambert
968f1027a7 hide VM delete button when the VM is not halted 2015-03-27 15:01:09 +01:00
Julien Fontanet
46258d5481 ChangeLog 2015-03-27 14:59:22 +01:00
Fabrice Marsaud
250eff22b5 Interface creation GUI Fix 2015-03-26 13:38:57 +01:00
Fabrice Marsaud
ec73dca5c4 Order VM/interfaces by # 2015-03-26 13:33:04 +01:00
Fabrice Marsaud
e66e11d9e8 Minor design touch on VM/Interface create 2015-03-26 12:53:22 +01:00
Fabrice Marsaud
61d4f740b0 Network creation for VMs #216. A connection bug remains 2015-03-26 12:45:43 +01:00
Olivier Lambert
f93d756190 small UI change in VM view 2015-03-25 23:27:35 +01:00
Olivier Lambert
e93a9ba7ba add destroy task 2015-03-25 17:07:24 +01:00
Olivier Lambert
5e90912b0c remove useless label 2015-03-25 15:50:57 +01:00
Fabrice Marsaud
64a0e2d951 GUI tweaks on VM View 2015-03-25 15:09:36 +01:00
Fabrice Marsaud
76e41067da VM Disk managment ok for #82 #215 2015-03-25 14:09:38 +01:00
Fabrice Marsaud
906a9bda11 CD drive for VMs, attachDisk enhanced 2015-03-25 14:09:38 +01:00
Fabrice Marsaud
340ab06825 Stop using deprecated VDI.$VBD for VDI.$VBDs 2015-03-25 14:09:38 +01:00
Fabrice Marsaud
2fd0a76142 Attach disk is OK 2015-03-25 14:08:41 +01:00
Fabrice Marsaud
e457d95b7e vm.attchDisk first implementation. Various problems lingering 2015-03-25 14:08:40 +01:00
Julien Fontanet
274f401d92 Update deps. 2015-03-19 14:21:45 +01:00
Fabrice Marsaud
e60f009ea2 Human readable tooltips for charts 2015-03-19 10:37:43 +01:00
Olivier Lambert
cc2b660dfc bigger numbers for stats in pool view 2015-03-18 23:27:44 +01:00
Olivier Lambert
b3e264aa33 graph on SR page 2015-03-18 23:23:40 +01:00
Olivier Lambert
857bd4d932 add vCPUs/CPUs graph 2015-03-18 23:14:48 +01:00
Fabrice Marsaud
f784162da8 Tiny chart try integrated in the host view 2015-03-18 20:45:33 +01:00
Julien Fontanet
6030de647e Removes paths from the build. 2015-03-12 18:19:16 +01:00
Julien Fontanet
0b09d672d2 Close VM console during reboot (fix #208). 2015-03-10 12:42:28 +01:00
Julien Fontanet
d045b1af9b Update to latest novnc-node. (fix #205) 2015-03-10 10:19:07 +01:00
Julien Fontanet
b3a2e21bc8 Always copy args to avoid race conditions. (fix #206) 2015-03-09 13:30:50 +01:00
Julien Fontanet
328ade2f0a 3.7.0 2015-03-07 18:19:42 +01:00
Julien Fontanet
1dd1a25b31 Merge branch 'next-release' 2015-03-07 18:18:43 +01:00
Julien Fontanet
6458c0dde9 More ChangeLog. 2015-03-06 17:22:25 +01:00
Julien Fontanet
4bdcefca8f Fix issues in VMs deletion. 2015-03-06 17:22:15 +01:00
Julien Fontanet
62399f78b9 ChangeLog. 2015-03-06 16:32:13 +01:00
Fabrice Marsaud
26e364a5d6 Issue #179 fixed anticipated submit form protection 2015-03-06 16:29:02 +01:00
Olivier Lambert
de011857be minor UI fix 2015-03-06 14:06:38 +01:00
Julien Fontanet
8bc9880951 Do not show admins in ACLs view. 2015-03-06 13:58:58 +01:00
Olivier Lambert
a51cd95c60 simplify ACL view 2015-03-06 13:43:16 +01:00
Olivier Lambert
013ae0e254 first work on Changelog 2015-03-06 12:12:03 +01:00
Olivier Lambert
90acba21ab better error message and removing link to pool sr creation which is not ready yet 2015-03-05 19:15:27 +01:00
Olivier Lambert
130024c18d minor UI fixes in SR add view 2015-03-05 18:45:00 +01:00
Julien Fontanet
02f7e77b48 get() & byTypes only available from xoApi. 2015-03-05 18:13:03 +01:00
Julien Fontanet
a97cd91950 Remove unused permissions. 2015-03-05 17:55:31 +01:00
Julien Fontanet
fd543ca614 Remove an uncessary property declaration. 2015-03-05 17:55:31 +01:00
Julien Fontanet
6330eac5f8 xoApi.get() can optionally check the type. 2015-03-05 17:55:31 +01:00
Fabrice Marsaud
39bc6bff2f sr.reattachIso Integration for Issue #179 2015-03-05 17:22:27 +01:00
Fabrice Marsaud
5012adf8f9 newSR template enhancement 2015-03-05 15:29:30 +01:00
Fabrice Marsaud
a27f91d8f2 lodash.indexof 2015-03-05 15:24:27 +01:00
Fabrice Marsaud
39c206be91 SR Creation code enhanced 2015-03-05 15:01:10 +01:00
Fabrice Marsaud
d19d297ea4 Reattaching feature for Issue #179 2015-03-05 12:06:14 +01:00
Julien Fontanet
cfc8cbd08b Update to latest xo-lib which fixes many issues. 2015-03-04 18:27:18 +01:00
Fabrice Marsaud
32cd323b54 #179 progress 2015-03-04 18:19:50 +01:00
Fabrice Marsaud
e0af48955a Design touch for Issue #179 2015-03-04 16:31:03 +01:00
Fabrice Marsaud
08a334812b Issue #179 Iso SRs 2015-03-04 16:19:11 +01:00
Olivier Lambert
56fc278c73 fix bad icon 2015-03-04 15:43:59 +01:00
Fabrice Marsaud
d52f168e09 NFS & iSCSI for Issue #179 2015-03-04 15:42:39 +01:00
Julien Fontanet
3c9026ce83 Remove unused log in form in navbar. 2015-03-04 14:39:04 +01:00
Julien Fontanet
67577d8ec5 Workaround floatdrop/gulp-watch#141. 2015-03-04 14:06:49 +01:00
Julien Fontanet
edc5fd9500 Update deps. 2015-03-04 14:06:48 +01:00
Olivier Lambert
8af0272588 new logo included 2015-03-04 14:06:18 +01:00
Olivier Lambert
c6ec04bcf6 SR management operationnal 2015-03-04 13:49:09 +01:00
Olivier Lambert
7ef41adc26 fix minor race condition 2015-03-03 18:22:58 +01:00
Olivier Lambert
a161b63663 VM icon color when pending action 2015-03-03 18:12:10 +01:00
Julien Fontanet
5b65a31d24 Fix race condition on page load. 2015-03-03 17:40:18 +01:00
Julien Fontanet
6f4115d160 Users can only view tasks on hosts they have access too. 2015-03-03 17:39:17 +01:00
Julien Fontanet
b5de07de9d Ensure a clean production build. 2015-03-03 16:16:32 +01:00
Julien Fontanet
5bec09cfdc Remove incorrect npm test command. 2015-03-03 16:16:08 +01:00
Julien Fontanet
c3a8a89c61 Remove unused gulp tasks. 2015-03-03 16:14:14 +01:00
Julien Fontanet
e1a0988440 There are not tests currently. 2015-03-03 16:14:14 +01:00
Olivier Lambert
115dfb58f6 initial work on new SR 2015-03-02 10:33:59 +01:00
Olivier Lambert
8e689e9e9e do not display the pool menu for a pool without name, i.e single host pool 2015-03-02 10:27:51 +01:00
Olivier Lambert
ff0e79536a minor fix for VM creation page 2015-03-02 10:26:16 +01:00
Olivier Lambert
34c53f5ba4 fix panel for welcome message and add a button for new server 2015-03-02 10:25:17 +01:00
Julien Fontanet
15c5fc7e4b angular 1.3.14 packages have proper exports 2015-02-27 11:34:37 +01:00
Olivier Lambert
fe7c8679b9 fix new panel style for VM creation 2015-02-26 19:37:00 +01:00
Julien Fontanet
75840445c5 Remove dead code. 2015-02-24 11:34:57 +01:00
Julien Fontanet
2aa7448f76 Improve xoApi.get(). 2015-02-24 11:34:47 +01:00
Julien Fontanet
0f7a0f77f2 Filter out unauthorized objects in list view. 2015-02-24 09:46:30 +01:00
Julien Fontanet
0771ffeb71 Remove a console.log 2015-02-24 09:44:27 +01:00
Julien Fontanet
f410403107 Pages not allowed by ACLs cannot be opened. 2015-02-23 18:39:11 +01:00
Julien Fontanet
74e9c86b61 Fix novnc-node commit to avoid issue with last release. 2015-02-23 17:56:43 +01:00
Olivier Lambert
712ea98e41 fix UI in console view 2015-02-23 17:22:28 +01:00
Olivier Lambert
849f5bcd0e more space after logout button in the navbar 2015-02-23 17:03:18 +01:00
Olivier Lambert
22a36dc6a8 minor style fix on ACL page 2015-02-23 16:59:06 +01:00
Olivier Lambert
f02c19f637 panel for login 2015-02-23 16:49:30 +01:00
Julien Fontanet
cafc82c3a5 Remove a console.log. 2015-02-23 16:43:39 +01:00
Julien Fontanet
9bc2f4100f Fix master selector on tree view. 2015-02-23 16:43:39 +01:00
Julien Fontanet
c9735c034f Restore notifications on sign in errors. 2015-02-23 16:43:39 +01:00
Julien Fontanet
a455af71af Use a panel for admin/acls heading. 2015-02-23 16:43:39 +01:00
Olivier Lambert
c5a90545c8 panelization almost finished 2015-02-23 16:33:06 +01:00
Olivier Lambert
7da7904093 fix css bugs 2015-02-23 15:32:16 +01:00
Olivier Lambert
bd7653f682 remove useless style 2015-02-23 15:15:48 +01:00
Olivier Lambert
30335a1ce9 panelization continue 2015-02-23 15:10:50 +01:00
Olivier Lambert
c6f1250abc panelization of object views 2015-02-23 15:05:38 +01:00
Julien Fontanet
4c495f3828 Use variable directly instead of getters. 2015-02-23 14:49:24 +01:00
Julien Fontanet
c31fbc723d Move ACLs title. 2015-02-23 12:49:34 +01:00
Julien Fontanet
60e11055d2 Update xo-lib to 0.6. 2015-02-23 12:47:27 +01:00
Julien Fontanet
fbbc765460 Task tooltip shown where mouseover anywhere on the task. 2015-02-23 12:47:27 +01:00
Olivier Lambert
4e759eaa92 minor UI fixes 2015-02-20 18:07:13 +01:00
Julien Fontanet
60e6dfb8e5 Work around xo-lib issue. 2015-02-20 17:33:37 +01:00
Julien Fontanet
cf33726652 Minor changes. 2015-02-20 17:33:24 +01:00
Julien Fontanet
eb87c7c591 Improve ACLs page. 2015-02-19 18:24:38 +01:00
Julien Fontanet
902d137d8e Fix page rename. 2015-02-19 18:10:46 +01:00
Julien Fontanet
0a04d43514 Expose all objects. 2015-02-19 17:57:26 +01:00
Julien Fontanet
07e0a68249 Minor changes. 2015-02-19 17:57:02 +01:00
Julien Fontanet
5ede3e18fb Expose current ACLs. 2015-02-19 17:56:52 +01:00
Julien Fontanet
c1e8e0c84c Remove tasks from the list view. 2015-02-19 17:56:32 +01:00
Julien Fontanet
dbb980cd45 Initial ACLs view. 2015-02-19 17:56:18 +01:00
Julien Fontanet
aab7c5c398 Enable strict deps injection. 2015-02-19 14:58:44 +01:00
Julien Fontanet
8916b5db54 Update ui-select. 2015-02-19 14:44:28 +01:00
Julien Fontanet
ed2ce4b999 6to5 has been renamed Babel. 2015-02-19 13:03:51 +01:00
Julien Fontanet
0cfbc31241 Do not build on install, only publish. 2015-02-17 15:06:59 +01:00
Julien Fontanet
1368b747be Update to latest xo-lib. 2015-02-11 15:02:20 +01:00
Julien Fontanet
92d44f4061 Remove the token if incorrectly encoded (fix #192). 2015-02-11 12:01:32 +01:00
Julien Fontanet
f280717cb3 Avoid flicker when sorting unnamed pools. 2015-02-11 10:59:26 +01:00
Olivier Lambert
fdb3ff0afd display host hardware manufacturer and model 2015-02-10 23:33:48 +01:00
Julien Fontanet
66dde2fb36 Minor fix. 2015-02-10 19:12:59 +01:00
Julien Fontanet
7ffcd3282b Remove deprecated entry. 2015-02-10 19:06:58 +01:00
Julien Fontanet
f0a3fb4cbb Fix tasks list in navbar. 2015-02-10 18:27:06 +01:00
Julien Fontanet
416417dfc6 New xoApi service buid on top of xo-lib. 2015-02-10 18:23:47 +01:00
Olivier Lambert
acc4130347 running task in the navbar 2015-02-06 13:04:20 +01:00
Olivier Lambert
69cf3c99f4 better icons used and more sass usage 2015-02-05 16:39:17 +01:00
Olivier Lambert
3c11444302 add model for cancelling a task 2015-02-05 13:41:10 +01:00
Olivier Lambert
dd32ded29b add the task cancel action 2015-02-05 12:16:48 +01:00
Olivier Lambert
effa95fb31 better animation for pending tasks 2015-02-05 12:12:21 +01:00
Olivier Lambert
dc46475e4e display nicelly a task in the host view 2015-02-05 12:05:28 +01:00
Olivier Lambert
8b81de1141 switch RAM usage of host from numbers to a progress bar 2015-02-05 00:37:33 +01:00
Olivier Lambert
cf0fcad340 progress bar on task is in green for the sake of clarity 2015-02-05 00:37:12 +01:00
Olivier Lambert
a769906330 putting tasks in first 2015-02-05 00:05:40 +01:00
Olivier Lambert
d49a935598 another whitespace missing 2015-02-05 00:04:10 +01:00
Olivier Lambert
522d02d529 fix missing whitespaces during html to jade conversion 2015-02-04 23:55:25 +01:00
Olivier Lambert
7246e48428 filter on active tasks 2015-02-04 23:52:43 +01:00
Olivier Lambert
3a847c9115 add tasks display in list view and their progress in live. TODO: filter non success or pending tasks 2015-02-04 23:30:01 +01:00
Julien Fontanet
db3006acaf Use novnc-node instead of no-vnc. 2015-02-04 17:44:29 +01:00
Julien Fontanet
03a7de8fe7 Minor refactoring. 2015-02-04 16:35:07 +01:00
Julien Fontanet
67e0c5af02 Various small changes. 2015-02-04 14:14:53 +01:00
Julien Fontanet
6658077bda noVnc directive in its own module. 2015-02-04 12:55:47 +01:00
Julien Fontanet
929fce6adb Console: HTTPS support. 2015-02-04 10:21:14 +01:00
Julien Fontanet
cd1fc7308e Consoles: fallback to standard ports if undefined. 2015-02-04 09:58:21 +01:00
Julien Fontanet
c0a5e9fdcc Use the new console proxy. 2015-02-03 17:31:15 +01:00
Julien Fontanet
a3e6a580d9 Fix incorrect redirection after log in. 2015-02-03 14:46:30 +01:00
Julien Fontanet
5d01c9f56a Minor fixes. 2015-02-02 20:07:21 +01:00
Julien Fontanet
9cf291f5a3 More CoffeeScript to ES6. 2015-02-01 22:35:38 +01:00
Julien Fontanet
d8738ae581 Navbar fix (related to the list view. 2015-02-01 15:00:42 +01:00
Julien Fontanet
4a4effe5cf Fix user display in navbar. 2015-02-01 12:51:31 +01:00
Julien Fontanet
a2a2f5541f Minor fix. 2015-02-01 12:47:20 +01:00
Julien Fontanet
499e3c3456 Move ./directives and ./filters to xo-directives and xo-filters. 2015-02-01 11:33:42 +01:00
Julien Fontanet
b66187a24f Main module: CoffeeScript → ES6. 2015-02-01 11:32:26 +01:00
Julien Fontanet
f2615e93b7 Fix weird behavior when not logged in. 2015-01-31 22:43:17 +01:00
Julien Fontanet
c722dc9328 Do not load unused view (new SR). 2015-01-31 22:35:27 +01:00
Julien Fontanet
27a45d6b8c More CoffeeScript → ES6. 2015-01-31 22:34:54 +01:00
Julien Fontanet
46849590c5 Remove completely browserify-shim. 2015-01-31 18:54:01 +01:00
Julien Fontanet
c9e7e4af8f Do not use browserify-shim for angular-animate and angular-cookies. 2015-01-31 18:45:37 +01:00
Julien Fontanet
28887d68c2 delete-vms: CoffeeScript to ES6 2015-01-31 18:32:36 +01:00
Julien Fontanet
3322a90320 Move ./services to xo-services. 2015-01-31 18:30:06 +01:00
Julien Fontanet
3d679d3810 Do not use browserify-shim for angular-notify-toaster. 2015-01-31 18:29:19 +01:00
Julien Fontanet
b52269ab34 Missing deps. 2015-01-30 14:03:32 +01:00
Julien Fontanet
8632cc2ff0 More deps updates. 2015-01-30 13:34:30 +01:00
Julien Fontanet
f42faf8e97 angular-ui-select 2015-01-30 13:31:34 +01:00
Julien Fontanet
567de44508 More (home → index) fixes. 2015-01-30 13:31:06 +01:00
Julien Fontanet
a6d2505211 Fix “tree” state URL. 2015-01-30 10:56:51 +01:00
Julien Fontanet
e3d641b999 Initial admin zone. 2015-01-30 10:55:55 +01:00
Julien Fontanet
d0f88db388 Default state depends from user.permission (tree or list). 2015-01-30 10:54:50 +01:00
Julien Fontanet
14de6b0075 Module home renamed to tree. 2015-01-30 10:53:53 +01:00
Julien Fontanet
0092edc16f Login: Coffee → JS. 2015-01-30 10:38:48 +01:00
Julien Fontanet
04de57cf9f Bootstrap SASS via npm. 2015-01-29 17:31:50 +01:00
Julien Fontanet
d15a46bd15 Various fixes concerning FontAwesome and CSS. 2015-01-29 17:05:59 +01:00
Julien Fontanet
2e992dca67 Downgrade gulp-watch due to a bug. 2015-01-29 16:44:01 +01:00
Julien Fontanet
57dbbaad78 Install UiBootstrap & FontAwesome via npm. 2015-01-29 14:55:15 +01:00
Julien Fontanet
3ec6cd7fee Dont setup livereload server if not used. 2015-01-29 14:54:09 +01:00
Julien Fontanet
049e5ac6ac Enable ES6. 2015-01-27 13:08:25 +01:00
Julien Fontanet
2ead2465c8 Update deps. 2015-01-27 13:08:04 +01:00
Olivier Lambert
e3ce6c05bb remove double entry 2015-01-15 13:59:31 +01:00
Olivier Lambert
730c9fcec8 fix issue #187 2015-01-15 13:55:00 +01:00
Olivier Lambert
85925c027a add bulk action for suspend/resume on issue #186 2015-01-12 14:02:57 +01:00
Olivier Lambert
9256e5c3ea manage VM suspend/resume 2015-01-12 13:59:48 +01:00
Julien Fontanet
44f4c3a22d Disable debug data to improve performance. 2015-01-09 18:39:12 +01:00
Julien Fontanet
fa390f4374 Should fix console on IE. 2015-01-08 13:19:47 +01:00
Olivier Lambert
a282acd137 better feedback and actions for hosts consecutive to issues #181 #182 2015-01-08 11:54:34 +01:00
Olivier Lambert
8bd167033f disable host for resolving issue #182 2015-01-08 11:39:37 +01:00
Olivier Lambert
4afcf4900a fix issue #181 about enabling host 2015-01-08 10:35:24 +01:00
Olivier Lambert
4a8c47dffb add available space for live VDI migration like in issue #180 requested 2015-01-07 20:04:38 +01:00
Olivier Lambert
fada93bae3 show available space, fix #180 2015-01-07 16:22:25 +01:00
Julien Fontanet
5d6ae5ce5e Fix SR change detection. 2015-01-05 14:28:03 +01:00
Julien Fontanet
db42fcc583 Fix disk migration in VM page (fix #177). 2015-01-05 12:55:42 +01:00
Olivier Lambert
e904714202 start to address issue https://github.com/vatesfr/xo-web/issues/177 but need to finish the job in the UI 2015-01-02 16:57:16 +01:00
Olivier Lambert
0ca59704f9 fix issue https://github.com/vatesfr/xo-web/issues/178 2015-01-02 16:55:10 +01:00
Olivier Lambert
c7865613e1 Fix typo 2014-12-01 12:29:13 +01:00
Julien Fontanet
eb4dfd9dde Update CHANGELOG.md 2014-11-28 20:06:31 +01:00
Julien Fontanet
28eb595a56 3.6.0 2014-11-28 17:23:53 +01:00
Julien Fontanet
037091f155 Merge branch 'next-release' 2014-11-28 17:21:29 +01:00
Julien Fontanet
eecffb1782 ChangeLog 3.6 2014-11-28 17:18:23 +01:00
Julien Fontanet
912f5294d6 Adds missing noVNC dep. 2014-11-28 16:36:32 +01:00
Julien Fontanet
f6e410e461 Scroll view to top on page change. 2014-11-28 16:24:37 +01:00
Olivier Lambert
ddddebfe27 comment non working feature 2014-11-25 18:28:46 +01:00
Olivier Lambert
57930e2c90 remove useless editable panel 2014-11-24 23:29:55 +01:00
Olivier Lambert
81238c78fe indentation fix 2014-11-24 23:27:20 +01:00
Olivier Lambert
7e511313a3 remove interface edit, see issue #84 2014-11-24 23:23:35 +01:00
Olivier Lambert
56f4f1ca03 VDI name/description edition are now working in the SR view 2014-11-24 23:13:39 +01:00
Olivier Lambert
b25ef4bcc3 start to work on SR page for VDI edit 2014-11-24 22:59:56 +01:00
Julien Fontanet
aaeac9c6eb Minor code cleanup. 2014-11-24 22:33:10 +01:00
Julien Fontanet
8bccb82f5d Basic edition of VM disks. 2014-11-24 22:22:53 +01:00
Olivier Lambert
b7437d6a74 add PIF connect/disconnect in host view 2014-11-24 21:01:29 +01:00
Olivier Lambert
fbd2f519e6 VIF connect/disconnect/forget management 2014-11-24 20:42:35 +01:00
Olivier Lambert
e1b3e67b7a pdb management in host view 2014-11-24 19:36:00 +01:00
Olivier Lambert
dd9374fd50 better UI for handling disk management in SR view 2014-11-24 19:07:35 +01:00
Olivier Lambert
c1f3b53ed7 add reconnect all host in SR view 2014-11-24 18:44:06 +01:00
Olivier Lambert
627b11abde pbd plug and unplug 2014-11-24 18:27:43 +01:00
Julien Fontanet
d68caf85d0 Remove unused data-toggle attrs. 2014-11-24 18:09:39 +01:00
Julien Fontanet
79857a0ed4 Fix dropdowns due to UI Bootstrap breaking change. 2014-11-24 18:05:13 +01:00
Julien Fontanet
1688adbcbe Minor changes. 2014-11-24 17:56:02 +01:00
Julien Fontanet
096e722a74 UI Bootstrap 0.12 (fix #163). 2014-11-24 15:40:09 +01:00
Olivier Lambert
9f361e441c add patch list in host view. Fix #156 2014-11-24 13:54:31 +01:00
Olivier Lambert
47a5d39354 fix typo copy/past in host and pool view for logs 2014-11-24 13:53:54 +01:00
Olivier Lambert
ec92b79a3d better UI in various views 2014-11-21 16:12:46 +01:00
Olivier Lambert
a55b6055c2 fix wrong icon usage 2014-11-21 14:29:50 +01:00
Olivier Lambert
de8ad9e08a better UI for actions on disks and interfaces 2014-11-21 14:28:21 +01:00
Olivier Lambert
9bd662d6d1 better UI in VM view 2014-11-21 12:42:23 +01:00
Olivier Lambert
c855fb8c6a add delete all logs in Vm view 2014-11-21 12:42:12 +01:00
Olivier Lambert
07d5c85886 fix issue #167 2014-11-21 11:40:19 +01:00
Olivier Lambert
c9424f2f54 only display clone button if the VM is halted 2014-11-21 11:22:44 +01:00
Olivier Lambert
cdc0887895 get feedback when an action is started with notifications 2014-11-20 17:05:17 +01:00
Olivier Lambert
6b15e866f8 do not display incompatible button with the current vm power state on console View 2014-11-20 16:43:34 +01:00
Olivier Lambert
5192358dce do not display incompatible button with the current vm power state on the VM view 2014-11-20 16:40:57 +01:00
Olivier Lambert
5758e1c550 remove useless message 2014-11-20 15:21:08 +01:00
Olivier Lambert
2b5857affe add vbd connect, disconnect and delete 2014-11-19 20:28:44 +01:00
Olivier Lambert
556fadc9f5 WIP: start to work on UI for managing disks 2014-11-19 20:01:13 +01:00
Olivier Lambert
1d04d33981 remove ng init when possible 2014-11-19 20:00:43 +01:00
Olivier Lambert
52b81ccb16 remove ng init call to avoid display problems 2014-11-19 15:49:38 +01:00
Olivier Lambert
58e841e5c7 fix issue #164 2014-11-19 15:40:29 +01:00
Olivier Lambert
30beb81d9a fix SR view with linked VBD 2014-11-14 18:28:07 +01:00
Olivier Lambert
8da6ac7cfb do not display VM address field if there is none 2014-11-14 12:47:58 +01:00
Julien Fontanet
4b7effe3ee Drop IE < 9 support. 2014-11-14 09:50:11 +01:00
Julien Fontanet
ea1ca34644 Update deps + Less Bower packages. 2014-11-14 09:50:11 +01:00
Julien Fontanet
b1dbb47890 Work around UI Router bug. 2014-11-12 13:29:36 +01:00
Julien Fontanet
80d9cd8fa2 Fix Angular version conflict in Bower. 2014-11-12 13:29:34 +01:00
Olivier Lambert
b50ed96f2e change ha name and fix a ng repeat bug 2014-11-10 17:29:34 +01:00
Olivier Lambert
32f45e9ab7 add HA checkbox and display it if the pool has HA also activated 2014-11-10 17:19:00 +01:00
Olivier Lambert
2b2e07e767 display if HA is enabled on a VM 2014-11-10 16:31:36 +01:00
Olivier Lambert
bdb40f801e display on which pool the VM is running if it has a name 2014-11-10 16:31:11 +01:00
Olivier Lambert
644a54d6fe fix identation bug causing modal appearing without any reason 2014-11-08 14:11:28 +01:00
Julien Fontanet
28bbe17822 Angular 1.3 jqLite does not provide .get(). 2014-10-25 18:21:20 +02:00
Julien Fontanet
79344c2793 Bower bug again. 2014-10-20 15:46:46 +02:00
Julien Fontanet
da6c18e45b Update deps. 2014-10-20 15:08:46 +02:00
Olivier Lambert
f0de0514bf display pseudo pools for patching them too 2014-10-01 18:08:27 +02:00
Olivier Lambert
c48429888e use the word create for VM in place of Add 2014-10-01 16:35:30 +02:00
Olivier Lambert
e96486e130 add patch from pool view 2014-10-01 15:45:29 +02:00
Olivier Lambert
3058e37d45 add basic patch upload 2014-10-01 14:00:40 +02:00
Olivier Lambert
169241875b add import on main view and small fixes 2014-09-26 22:33:53 +02:00
Julien Fontanet
cc0f9acccd Initial VM import. 2014-09-26 18:53:15 +02:00
Julien Fontanet
b36c2f1729 Fix access to pool from VM. 2014-09-26 18:52:31 +02:00
Olivier Lambert
ea6df8b653 add export VM in vm view 2014-09-26 17:31:35 +02:00
Olivier Lambert
e720f6449d add explicit alert when start async actions 2014-09-23 19:58:46 +02:00
Julien Fontanet
abb2039312 Minify images for the production build. 2014-09-19 00:06:26 +02:00
Julien Fontanet
7e69c0b7be Sets item id when known. 2014-09-12 20:58:24 +02:00
Julien Fontanet
379c1bbd3d Add a free slot of server/user in the settings page on saving. 2014-09-12 14:14:37 +02:00
Julien Fontanet
6fd41b15dc Require a password for a new user (fix #143). 2014-09-12 13:37:53 +02:00
Julien Fontanet
b240562356 Require user/password for a new server (fix #144). 2014-09-12 13:37:16 +02:00
Julien Fontanet
44fd81773d Only use unbuffered comments in Jade. 2014-09-12 13:35:57 +02:00
Olivier Lambert
6d529a7498 fix method name 2014-09-03 16:36:15 +02:00
Olivier Lambert
c5e7e0b3fb add XenStorage motion in main view 2014-09-03 16:18:22 +02:00
Olivier Lambert
576bfed15f propose Xen Storage Motion if Xen Motion fails 2014-09-03 16:16:00 +02:00
Julien Fontanet
2471b4b155 Update gulpfile.js. 2014-08-31 18:06:11 +02:00
Julien Fontanet
238339373b Work around UI Router bug. 2014-08-31 17:53:03 +02:00
Olivier Lambert
da0218a3ce change link to pricing for the new website 2014-08-28 19:27:26 +02:00
Olivier Lambert
e44072e5a7 remove old code 2014-08-28 14:21:21 +02:00
Julien Fontanet
5d2bbb61f8 Refresh autofilled values on login. 2014-08-28 14:19:52 +02:00
Olivier Lambert
96dcd90005 add start host (wol) 2014-08-27 20:27:49 +02:00
Julien Fontanet
b749122076 Update deps. 2014-08-27 16:01:50 +02:00
Julien Fontanet
ccde455de1 Fix Scss indentation. 2014-08-27 15:19:04 +02:00
Julien Fontanet
a980513cba Bower seem to not handle well + in tags. 2014-08-27 15:09:21 +02:00
Olivier Lambert
7d5cd8b00f better login form 2014-08-23 20:16:09 +02:00
Julien Fontanet
a7c70b0b50 Use Bootstrap 3.2 2014-08-19 15:43:20 +02:00
Julien Fontanet
b5a995656d 3.5.1 2014-08-19 15:19:11 +02:00
Julien Fontanet
8b4c7ff521 Merge branch 'next-release' 2014-08-19 15:18:59 +02:00
Julien Fontanet
c243fbc011 ChangeLog 2014-08-19 15:18:00 +02:00
Olivier Lambert
493e4497c8 add missing space in pool view 2014-08-19 15:17:38 +02:00
Julien Fontanet
84967b717c Fix pool view (fix #139). 2014-08-19 14:02:24 +02:00
Julien Fontanet
21493424fe Minor fixes. 2014-08-19 14:01:21 +02:00
Julien Fontanet
2f73d528f8 Bypass Angular.js exception mangling. 2014-08-19 13:38:24 +02:00
Julien Fontanet
b91f6314bd Remove useless package. 2014-08-16 18:17:34 +02:00
Julien Fontanet
92aefa79e3 Update Bower deps. 2014-08-16 16:32:57 +02:00
Julien Fontanet
411e5dc35a Use node-sass instead of Ruby sass (fix #138). 2014-08-14 18:06:20 +02:00
Julien Fontanet
0159fffab9 Minor fix. 2014-08-14 17:18:45 +02:00
Julien Fontanet
c98c2f93e2 ChangeLog. 2014-08-14 17:12:32 +02:00
Julien Fontanet
1d2cacbb02 3.5.0 2014-08-14 16:29:02 +02:00
Olivier Lambert
2d57958f6f remove doc (moved to XO repo) 2014-08-14 13:02:01 +02:00
Olivier Lambert
88c36aa272 Update README.md 2014-08-14 13:01:13 +02:00
Olivier Lambert
05ae37cb4d Update README.md 2014-08-14 12:23:49 +02:00
Julien Fontanet
96fdaff82a Various updates. 2014-08-14 11:13:35 +02:00
Olivier Lambert
07b64465c6 move version number in the title panel 2014-08-14 00:33:01 +02:00
Julien Fontanet
366e9be4de Show the package version in the about page. 2014-08-13 17:27:10 +02:00
Julien Fontanet
261c9e2240 3.5.0-alpha2 2014-08-13 16:59:27 +02:00
Olivier Lambert
c03973867c fix message in main view when you aren't connected to any host 2014-08-13 15:31:15 +02:00
Olivier Lambert
ca6984f4a5 remove events displayed in console 2014-08-13 15:21:39 +02:00
Julien Fontanet
4fbc2680ba Jade comment. 2014-08-13 14:54:35 +02:00
Julien Fontanet
9a5b0a8104 CSS autoprefixer & CSS minification. 2014-08-13 14:53:44 +02:00
Julien Fontanet
ad998f7a63 Jade comments. 2014-08-13 14:53:30 +02:00
Olivier Lambert
91043e4b52 CSS fixes 2014-08-13 14:06:12 +02:00
Julien Fontanet
8f3bd0145b 3.5.0-alpha1 2014-08-12 15:36:39 +02:00
Julien Fontanet
ace0051147 Update deps and remove gulp-coffeelint.
- Update to latest Browserify & Watchify.
- Use ngAnnotate instead of ngMin.
- Remove gulp-coffeelint which failed to install.
2014-08-12 15:33:47 +02:00
Olivier Lambert
83720c6673 Update README.md 2014-08-12 15:24:27 +02:00
Olivier Lambert
f1ce4de1eb Update README.md
Add XOA details
2014-08-12 15:24:15 +02:00
Olivier Lambert
013b23c918 fix a dropdown menu and add a caret for better UI 2014-08-12 15:22:16 +02:00
Olivier Lambert
d03debf59c Update README.md 2014-08-12 14:32:40 +02:00
Olivier Lambert
00b55d893a Update README.md 2014-08-12 14:31:22 +02:00
Olivier Lambert
085b92df9c Update README.md 2014-08-12 14:27:36 +02:00
Olivier Lambert
8516ff7cb3 add basic actions buttons in console view, fix #121 2014-08-11 17:42:51 +02:00
Julien Fontanet
56d64d8adc Minor fix. 2014-08-11 17:18:15 +02:00
Julien Fontanet
5d68e48312 Minor fix. 2014-08-11 16:53:15 +02:00
Julien Fontanet
8b0be0a8c5 Required radios in VM creation. 2014-08-11 16:50:47 +02:00
Julien Fontanet
d6e4d6d927 Required selects in VM creation. 2014-08-11 16:43:47 +02:00
Julien Fontanet
bbfe60ffd2 All pages are now in modules. 2014-08-11 16:30:57 +02:00
Julien Fontanet
c9cc0b5ce3 Do not use shrinkwrap anymore. 2014-08-11 15:22:06 +02:00
Julien Fontanet
f0bf19f286 Still more modules. 2014-08-11 15:15:34 +02:00
Julien Fontanet
6492e55954 More modules. 2014-08-11 14:49:38 +02:00
Julien Fontanet
c9fd1d5833 Fix dropdown menus. 2014-08-11 14:17:10 +02:00
Julien Fontanet
e8f73cae6c Fix generic modal. 2014-08-11 13:42:43 +02:00
Olivier Lambert
7d436b40d0 use directly host power_state 2014-08-11 13:17:30 +02:00
Olivier Lambert
55cd6d2a74 fix small display glitch on the top of vm list in main view 2014-08-05 12:48:56 +02:00
Olivier Lambert
01a2585edf propperly display state of a host in the main view. Also remove RAM bar when host is disabled or halted 2014-08-05 12:42:13 +02:00
Olivier Lambert
bb329b73ef fix issue #132 2014-07-21 12:28:54 +02:00
Julien Fontanet
9b4008f39c Deps update. 2014-07-15 12:36:12 +02:00
Julien Fontanet
ef52f20993 Update deps. 2014-07-07 13:48:10 +02:00
Olivier Lambert
f2a207e790 add fedora in linux icon: fix #131 2014-06-28 13:45:32 +02:00
Julien Fontanet
40f502492b Initial browserify. 2014-06-19 15:45:28 +02:00
Julien Fontanet
c5f1cf94ac Minor updates. 2014-06-10 15:53:10 +02:00
Julien Fontanet
2e286d4a50 Minor fix. 2014-06-10 12:34:59 +02:00
Julien Fontanet
94a27fa79e Build updates and fix index page. 2014-06-10 12:16:55 +02:00
Julien Fontanet
0adec1dbe2 Update to gulp 3.8 2014-06-10 10:07:47 +02:00
Julien Fontanet
c38c53957d Update deps. 2014-06-09 15:39:57 +02:00
Olivier Lambert
498eb0134e add suse in linux distrib list for propper display of OS icon 2014-06-02 20:01:48 +02:00
Olivier Lambert
d34f9182c2 escape sharp char 2014-05-30 14:59:44 +02:00
Olivier Lambert
324f21cf3d finish jadifying all views 2014-05-30 13:19:13 +02:00
Olivier Lambert
200c0110fd jadify modal windows 2014-05-30 13:10:56 +02:00
Olivier Lambert
5694b0054b jadify main view 2014-05-30 13:05:55 +02:00
Olivier Lambert
8825fdcdaf jadify navbar 2014-05-30 12:29:55 +02:00
Olivier Lambert
7805bc172e jadify add vm view 2014-05-30 12:25:19 +02:00
Olivier Lambert
aa321b4b22 fix missing space in SR and jadify pool view 2014-05-30 12:21:17 +02:00
Olivier Lambert
c74df7abc5 fix missing space between buttons and jadify settings view 2014-05-30 12:15:34 +02:00
Olivier Lambert
c6ffe8978d fix missing spaces and jadify sr view 2014-05-30 12:00:32 +02:00
Olivier Lambert
6761e0df15 jadify about page 2014-05-30 11:55:28 +02:00
Olivier Lambert
cc47225f24 jadify list view 2014-05-30 11:52:50 +02:00
Olivier Lambert
58c47a325f jadify host view 2014-05-30 11:48:34 +02:00
Olivier Lambert
706f7f3b7b converted vm view to a Jade template 2014-05-30 11:12:57 +02:00
Julien Fontanet
4d67f8bd0b Process for releasing a new version. 2014-05-29 16:20:16 +01:00
Julien Fontanet
d5227e00ff Prepare npm package. 2014-05-29 16:06:23 +01:00
Julien Fontanet
62fa1d570c Minor updates. 2014-05-27 20:41:10 +01:00
Olivier Lambert
899b4ab29a restore connected stuff on VM disk 2014-05-27 20:16:08 +02:00
Olivier Lambert
a2801f0b7b remove useless checkbox 2014-05-27 20:13:35 +02:00
Olivier Lambert
84694fedef fix typo 2014-05-27 20:07:05 +02:00
Olivier Lambert
c0005741bd remove req field in VM view 2014-05-27 19:03:59 +02:00
Olivier Lambert
150a341510 add recipes 2014-05-26 20:08:42 +02:00
Olivier Lambert
e486713026 add link to the bottom of each doc part 2014-05-26 10:59:08 +02:00
Olivier Lambert
0201dfa8e3 reduce image 2014-05-26 10:55:18 +02:00
Olivier Lambert
78269a1b7b doc reorg 2014-05-26 10:52:15 +02:00
Olivier Lambert
d1bf60ab17 end doc on VM 2014-05-26 10:33:06 +02:00
Olivier Lambert
6c368dc8cb re re fix broken links 2014-05-25 14:18:34 +02:00
Olivier Lambert
10ac67c1e0 re fix broken links 2014-05-25 14:17:46 +02:00
Olivier Lambert
ba3b38d941 fix broken links 2014-05-25 14:17:07 +02:00
Olivier Lambert
47044d93d3 add ToC 2014-05-25 14:15:33 +02:00
Olivier Lambert
423ac4ea04 fix icon placement 2014-05-25 14:10:03 +02:00
Olivier Lambert
b114b40f88 add snapshot doc 2014-05-25 14:09:05 +02:00
Olivier Lambert
030ce18d65 add template doc 2014-05-25 13:55:36 +02:00
Julien Fontanet
44116b3559 Minor fix. 2014-05-25 13:49:54 +02:00
Olivier Lambert
78332c7bfd add more doc 2014-05-25 13:47:30 +02:00
Julien Fontanet
dd8ac28240 Minor fix. 2014-05-25 13:46:19 +02:00
Julien Fontanet
81818222e5 Fixes. 2014-05-25 13:44:01 +02:00
Olivier Lambert
4a25af03f4 add console icon 2014-05-25 13:32:09 +02:00
Olivier Lambert
6515d73534 add console 2014-05-25 13:27:12 +02:00
Olivier Lambert
006fc9acd3 continue to add doc 2014-05-25 13:21:15 +02:00
Olivier Lambert
1aa878b3a6 fix typo 2014-05-24 17:17:32 +02:00
Olivier Lambert
23dc475905 add doc for usage 2014-05-24 17:16:37 +02:00
Olivier Lambert
a827ab6ffd oops forgot two images 2014-05-24 17:01:50 +02:00
Olivier Lambert
f55784ce6a add action image 2014-05-24 17:00:26 +02:00
Olivier Lambert
f7110ac472 add usage doc 2014-05-24 16:58:47 +02:00
Olivier Lambert
52a94e0f47 finish layout section 2014-05-24 16:47:22 +02:00
Olivier Lambert
c25eb678a8 fix typo 2014-05-24 16:42:08 +02:00
Olivier Lambert
5128efbbf4 add more doc stuff 2014-05-24 16:41:03 +02:00
Olivier Lambert
c73ee3c531 add doc 2014-05-24 16:26:24 +02:00
Olivier Lambert
54ba67f882 add admin doc 2014-05-24 16:04:41 +02:00
Julien Fontanet
395bfe6eb5 Only admins can access the setting page (fix #77). 2014-05-24 16:00:40 +02:00
Julien Fontanet
e0ecf65aac Better integration of the setting pages. 2014-05-24 15:49:33 +02:00
Olivier Lambert
a7c651c98c fix bad image name 2014-05-24 15:49:17 +02:00
Olivier Lambert
ed473f457a add images 2014-05-24 15:48:21 +02:00
Olivier Lambert
a572c1132d more doc 2014-05-24 15:47:26 +02:00
Olivier Lambert
ba314c7135 small changes in doc 2014-05-24 15:38:43 +02:00
Olivier Lambert
a0788ab050 fix minor stuff 2014-05-24 15:34:13 +02:00
Olivier Lambert
8885f0be10 add doc on config file 2014-05-24 15:33:31 +02:00
Julien Fontanet
9d433ed1d0 Go back to the homepage after deleting a VM. (fix #56). 2014-05-24 15:27:14 +02:00
Olivier Lambert
0125c9445e add doc stuff again 2014-05-24 15:13:15 +02:00
Olivier Lambert
a3454d4143 add doc stuff 2014-05-24 14:42:07 +02:00
Olivier Lambert
ad3c224b10 oops forgot images 2014-05-24 14:36:34 +02:00
Olivier Lambert
43d186d464 add doc 2014-05-24 14:34:56 +02:00
Olivier Lambert
a3671e776d start to add doc 2014-05-24 12:01:11 +02:00
Julien Fontanet
862ce07f8d 3.4.0 2014-05-22 11:43:23 +02:00
Julien Fontanet
a78bc7d195 Typos. 2014-05-22 11:39:08 +02:00
Julien Fontanet
b0ce4404ea Markdown fix. 2014-05-22 11:37:38 +02:00
Julien Fontanet
cd5641d2f3 ChangeLog. 2014-05-22 11:36:48 +02:00
Julien Fontanet
51dd9f0419 Redirect to wanted page after login. 2014-05-22 10:51:15 +02:00
Julien Fontanet
6d0a76756d Fix autofill AMAP. 2014-05-21 20:27:00 +02:00
Julien Fontanet
6e8342fbf4 Minor fix. 2014-05-21 20:12:09 +02:00
Julien Fontanet
8d227724d7 Merge branch 'events' into next-release 2014-05-21 20:00:27 +02:00
Olivier Lambert
eab91ffa83 fix icon for stopped vm 2014-05-21 19:57:23 +02:00
Julien Fontanet
6488198a06 Minor fixes. 2014-05-21 19:50:05 +02:00
Olivier Lambert
07b4c67b5c add space after the login icon 2014-05-21 18:44:45 +02:00
Julien Fontanet
70077a1eac Hide mockup. 2014-05-21 18:23:20 +02:00
Julien Fontanet
03763a6556 Merge branch 'acl' into next-release 2014-05-21 18:15:24 +02:00
Julien Fontanet
3cb8d77977 Switch to the log in page on log out. 2014-05-21 18:13:53 +02:00
Olivier Lambert
c8d7940451 fix vm description 2014-05-21 18:04:00 +02:00
Julien Fontanet
2253aa1229 Mandatory log in page. 2014-05-21 17:43:54 +02:00
Julien Fontanet
d5a16855fb Events are ok. 2014-05-21 15:25:12 +02:00
Olivier Lambert
1aec909c10 better style for connection status 2014-05-21 14:53:18 +02:00
Olivier Lambert
7b60980959 add icon for login 2014-05-21 13:50:28 +02:00
Olivier Lambert
097424e2ee add login view 2014-05-21 13:46:58 +02:00
Olivier Lambert
a99b17d240 fix ugly empty parentheses when host is not in a labelled pool 2014-05-18 18:35:50 +02:00
Olivier Lambert
7526230b69 mockup for user and group in VM view 2014-05-14 18:11:45 +02:00
Julien Fontanet
6b8f9a6319 Various updates. 2014-05-12 19:55:36 +02:00
Julien Fontanet
8e74556613 Dependencies update. 2014-05-12 18:50:50 +02:00
Julien Fontanet
26a3086b81 WIP. 2014-05-09 17:29:32 +02:00
Julien Fontanet
6dd70137db Do not use a symlink for gulp. 2014-04-25 12:19:50 +02:00
Julien Fontanet
94ab475a31 Update deps. 2014-04-25 12:19:25 +02:00
Olivier Lambert
e77b6f3b85 add convert to template in vm view 2014-04-18 13:15:47 +02:00
Olivier Lambert
4000b44d90 add possibility to clone with CoW 2014-04-16 18:00:00 +02:00
Olivier Lambert
ad09d98d81 add basic cloning 2014-04-16 17:31:51 +02:00
Julien Fontanet
567d5e7676 Merge branch 'master' into next-release 2014-04-15 18:17:20 +02:00
Julien Fontanet
123b0a331e Minor fix. 2014-04-15 17:53:58 +02:00
Julien Fontanet
11f1f3d132 Minor fix. 2014-04-15 17:50:05 +02:00
Julien Fontanet
a44ce0c538 Installation method is optional. 2014-04-15 17:45:55 +02:00
Olivier Lambert
23348c5d8c Merge pull request #115 from devopsconsulting/isNan
use standard js isNaN function.
2014-04-10 14:34:32 +02:00
Lars van de Kerkhof
176c6524a2 use standard js isNaN function. 2014-04-10 12:37:41 +02:00
Julien Fontanet
6765b0218a Merge branch 'next-release' 2014-03-28 17:00:35 +01:00
Julien Fontanet
b7fd209987 3.3.1 2014-03-28 16:58:20 +01:00
Julien Fontanet
e11f0595d8 ChangeLog. 2014-03-28 16:57:36 +01:00
Olivier Lambert
a01410f287 Merge pull request #110 from RhubarbSin/master
correct minor misspelling
2014-03-25 22:55:33 +01:00
Rhubarb Sin
6e53682854 correct minor misspelling 2014-03-25 17:47:36 -04:00
Olivier Lambert
e5a19d73e1 fix #109 2014-03-15 15:28:58 +01:00
Julien Fontanet
42d72a7786 Console page rewritten (in Jade) (see #92). 2014-03-13 16:13:14 +01:00
Julien Fontanet
30a29b179b Do not compile Jade files starting with an underscore. 2014-03-13 15:13:21 +01:00
Julien Fontanet
6a7ae710c5 Updates Bower to latest version (should limit install errors). 2014-03-13 12:28:22 +01:00
Julien Fontanet
7622fa88cb Minor fix in build. 2014-03-13 12:18:15 +01:00
Julien Fontanet
2f7aee02cc Merge branch 'next-release' 2014-03-07 16:04:31 +01:00
Julien Fontanet
75848f0d6e Bumps version number. 2014-03-07 16:04:12 +01:00
Julien Fontanet
c81c4ffc97 Links to issues in ChangeLog. 2014-03-07 16:00:25 +01:00
Julien Fontanet
5bbd8e7102 ChangeLog. 2014-03-07 15:57:33 +01:00
Julien Fontanet
f8fb69ac03 Uses gulp-load-plugins. 2014-03-03 16:00:30 +01:00
Julien Fontanet
a2b703d450 Explicitely declares angular-animate dependency to fix its version. 2014-02-28 11:21:26 +01:00
Julien Fontanet
25739d9f6f Bower and gulp.js share some of their configuration. 2014-02-28 11:20:51 +01:00
Olivier Lambert
1b12be55b2 add log deletion for pool and host 2014-02-27 19:33:08 +01:00
Olivier Lambert
ae7bfa6ee2 fix #99 and #100. Also minor fixes in HTML for VM view 2014-02-27 00:55:52 +01:00
Julien Fontanet
c012149b63 Tweaks HTMLhint settings. 2014-02-27 00:08:30 +01:00
Julien Fontanet
0210d87573 CoffeeScript linting added to gulp. 2014-02-27 00:00:57 +01:00
Olivier Lambert
35b8cf3769 fix #98 2014-02-24 19:06:16 +01:00
Olivier Lambert
a1bd76d5fd add action for host in main view, thus fix #97 2014-02-24 18:46:13 +01:00
Julien Fontanet
c0551eb5dc Initial command to check HTML & JS. 2014-02-24 17:28:43 +01:00
Olivier Lambert
f9e6118bfa small fix due to edit in wrong file 2014-02-24 17:06:30 +01:00
Olivier Lambert
69052c1de9 fix #95 2014-02-24 16:35:04 +01:00
Olivier Lambert
050dd7b47c implement bulk snapshots in main view, thus fix #96 2014-02-24 16:35:04 +01:00
Julien Fontanet
7d5e1009cb Uses ngmin for now but we should not depend on it. 2014-02-24 14:11:31 +01:00
Julien Fontanet
9b3b0d22f8 This file is now useless. 2014-02-24 14:03:24 +01:00
Julien Fontanet
d2035a0609 Grunt is replaced by gulp.js (fix #91). 2014-02-21 13:05:32 +01:00
190 changed files with 15942 additions and 9323 deletions

14
.babelrc Normal file
View File

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

View File

@@ -1,3 +1,3 @@
{
"directory": "app/bower_components"
"directory": "dist/bower_components"
}

View File

@@ -1,21 +1,65 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
# http://EditorConfig.org
#
# Julien Fontanet's configuration
# https://gist.github.com/julien-f/8096213
# Top-most EditorConfig file.
root = true
# Common config.
[*]
# Change these settings to your own preference
indent_style = space
indent_size = 2
# We recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespaces = true
[*.md]
trim_trailing_whitespace = false
# CoffeeScript
#
# https://github.com/polarmobile/coffeescript-style-guide/blob/master/README.md
[*.{,lit}coffee]
indent_size = 2
indent_style = space
# Markdown
[*.{md,mdwn,mdown,markdown}]
indent_size = 4
indent_style = space
# Package.json
#
# This indentation style is the one used by npm.
[/package.json]
indent_size = 2
indent_style = space
# Jade
[*.jade]
indent_size = 2
indent_style = space
# JavaScript
#
# Two spaces seems to be the standard most common style, at least in
# Node.js (http://nodeguide.com/style.html#tabs-vs-spaces).
[*.js]
indent_size = 2
indent_style = space
# Less
[*.less]
indent_size = 2
indent_style = space
# Sass
#
# Style used for http://libsass.com
[*.s[ac]ss]
indent_size = 2
indent_style = space
# YAML
#
# Only spaces are allowed.
[*.yaml]
indent_size = 2
indent_style = space

1
.gitattributes vendored
View File

@@ -1 +0,0 @@
* text=auto

15
.gitignore vendored
View File

@@ -1,5 +1,10 @@
node_modules
dist
.tmp
.sass-cache
app/bower_components
/bower_components/
/dist/
/node_modules/*
!/node_modules/*.js
/node_modules/*.js/
jsconfig.json
.idea
npm-debug.log

View File

@@ -1,24 +0,0 @@
{
"node": true,
"browser": true,
"esnext": true,
"bitwise": true,
"camelcase": true,
"curly": true,
"eqeqeq": true,
"immed": true,
"indent": 2,
"latedef": true,
"newcap": true,
"noarg": true,
"quotmark": "single",
"regexp": true,
"undef": true,
"unused": true,
"strict": true,
"trailing": true,
"smarttabs": true,
"globals": {
"angular": false
}
}

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

View File

@@ -1,7 +0,0 @@
language: node_js
node_js:
- '0.8'
- '0.10'
before_script:
- 'npm install -g bower grunt-cli'
- 'bower install'

View File

@@ -1,38 +1,409 @@
# ChangeLog
## **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
- 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))
- do not fetch stats if the VM state is transitioning ([a5c9880](https://github.com/vatesfr/xo-web/commit/a5c98803182792d2fe5ceb840ae1e23a8b767923))
- fix broken Angular due to new version of Babel which breaks ngAnnotate ([0a9c868](https://github.com/vatesfr/xo-web/commit/0a9c868678d239e5b3e54b4d2bc3bd14b5400120))
- feedback when connecting/disconnecting a server ([027d1e8](https://github.com/vatesfr/xo-web/commit/027d1e8cb1f2431e67042e1eec51690b2bc54ad7))
- clearer error message if a server is unreachable ([06ca007](https://github.com/vatesfr/xo-server/commit/06ca0079b321e757aaa4112caa6f92a43193e35d))
## **3.9.0** (2015-04-20)
[Blog post of this release](https://xen-orchestra.com/blog/xen-orchestra-3-9).
### 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))
- better feedback when connecting to a server ([xo-web#210](https://github.com/vatesfr/xo-web/issues/210))
- ability to add a local LVM SR ([xo-web#219](https://github.com/vatesfr/xo-web/issues/219))
- display virtual GPUs in VM view ([xo-web#223](https://github.com/vatesfr/xo-web/issues/223))
- ability to automatically start a VM with its host ([xo-web#224](https://github.com/vatesfr/xo-web/issues/224))
- ability to create networks ([xo-web#225](https://github.com/vatesfr/xo-web/issues/225))
- live charts for a VM CPU/disk/network & RAM ([xo-web#228](https://github.com/vatesfr/xo-web/issues/228) & [xo-server#51](https://github.com/vatesfr/xo-server/issues/51))
- remove VM import progress notifications (redundant with the tasks list) ([xo-web#235](https://github.com/vatesfr/xo-web/issues/235))
- 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
- 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))
- disconnected servers are properly removed from the interface ([xo-web#234](https://github.com/vatesfr/xo-web/issues/234))
## **3.8.0** (2015-03-27)
[Blog post of this release](https://xen-orchestra.com/blog/xen-orchestra-3-8).
### 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))
- LDAP authentication plugin for XO-Server ([xo-server#40](https://github.com/vatesfr/xo-server/issues/40))
- disk creation on the VM page ([xo-web#215](https://github.com/vatesfr/xo-web/issues/215))
- 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
- 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))
- fix user editing on the settings page ([xo-web#206](https://github.com/vatesfr/xo-web/issues/206))
## **3.7.0** (2015-03-06)
*Highlights in this release are the [initial ACLs implementation](https://xen-orchestra.com/blog/xen-orchestra-3-7-is-out-acls-in-early-access), [live migration for VDIs](https://xen-orchestra.com/blog/moving-vdi-in-live) and the ability to [create a new storage repository](https://xen-orchestra.com/blog/create-a-storage-repository-with-xen-orchestra/).*
### Enhancements
- ability to live migrate a VM between hosts with different CPUs ([xo-web#126](https://github.com/vatesfr/xo-web/issues/126))
- ability to live migrate a VDI ([xo-web#177](https://github.com/vatesfr/xo-web/issues/177))
- display a notification on VM creation ([xo-web#178](https://github.com/vatesfr/xo-web/issues/178))
- ability to create/attach a iSCSI/NFS/ISO SR ([xo-web#179](https://github.com/vatesfr/xo-web/issues/179))
- display SR available space on VM creation ([xo-web#180](https://github.com/vatesfr/xo-web/issues/180))
- ability to enable and disable host on the tree view ([xo-web#181](https://github.com/vatesfr/xo-web/issues/181) & [xo-web#182](https://github.com/vatesfr/xo-web/issues/182))
- ability to suspend/resume a VM ([xo-web#186](https://github.com/vatesfr/xo-web/issues/186))
- display Linux icon for SUSE Linux Enterprise Server distribution ([xo-web#187](https://github.com/vatesfr/xo-web/issues/187))
- correctly handle incorrectly formated token in cookies ([xo-web#192](https://github.com/vatesfr/xo-web/issues/192))
- display host manufacturer in host view ([xo-web#195](https://github.com/vatesfr/xo-web/issues/195))
- only display task concerning authorized objects ([xo-web#197](https://github.com/vatesfr/xo-web/issues/197))
- better welcome message ([xo-web#199](https://github.com/vatesfr/xo-web/issues/199))
- initial ACLs ([xo-web#202](https://github.com/vatesfr/xo-web/issues/202))
- display an action panel to rescan, remove, attach and forget a SR ([xo-web#203](https://github.com/vatesfr/xo-web/issues/203))
- display current active tasks in navbar ([xo-web#204](https://github.com/vatesfr/xo-web/issues/204))
### Bug fixes
- implements a proxy which fixes consoles over HTTPs ([xo#14](https://github.com/vatesfr/xo/issues/14))
- fix tasks listing in host view ([xo-server#43](https://github.com/vatesfr/xo-server/issues/43))
- fix console view on IE ([xo-web#184](https://github.com/vatesfr/xo-web/issues/184))
- fix out of sync objects in XO-Web ([xo-web#142](https://github.com/vatesfr/xo-web/issues/142))
- fix incorrect connection status displayed in login view ([xo-web#193](https://github.com/vatesfr/xo-web/issues/193))
- fix *flickering* tree view ([xo-web#194](https://github.com/vatesfr/xo-web/issues/194))
- single host pools should not have a dropdown menu in tree view ([xo-web#198](https://github.com/vatesfr/xo-web/issues/198))
## **3.6.0** (2014-11-28)
### Enhancements
- upload and apply patches to hosts/pools ([xo-web#150](https://github.com/vatesfr/xo-web/issues/150))
- import VMs ([xo-web#151](https://github.com/vatesfr/xo-web/issues/151))
- export VMs ([xo-web#152](https://github.com/vatesfr/xo-web/issues/152))
- migrate VMs to another pool ([xo-web#153](https://github.com/vatesfr/xo-web/issues/153))
- display pool even for single host ([xo-web#155](https://github.com/vatesfr/xo-web/issues/155))
- start halted hosts with wake-on-LAN ([xo-web#154](https://github.com/vatesfr/xo-web/issues/154))
- list of uploaded/applied patches ([xo-web#156](https://github.com/vatesfr/xo-web/issues/156))
- use Angular 1.3 from npm ([xo-web#157](https://github.com/vatesfr/xo-web/issues/157) & [xo-web#160](https://github.com/vatesfr/xo-web/issues/160))
- more feedbacks on actions ([xo-web#165](https://github.com/vatesfr/xo-web/issues/165))
- only buttons compatible with VM states are displayed ([xo-web#166](https://github.com/vatesfr/xo-web/issues/166))
- export VM snapshot ([xo-web#167](https://github.com/vatesfr/xo-web/issues/167))
- plug/unplug a SR to a host ([xo-web#169](https://github.com/vatesfr/xo-web/issues/169))
- plug a SR to all available hosts ([xo-web#170](https://github.com/vatesfr/xo-web/issues/170))
- disks editing on SR page ([xo-web#171](https://github.com/vatesfr/xo-web/issues/171))
- export of running VMs ([xo-server#36](https://github.com/vatesfr/xo-server/issues/36))
### Bug fixes
- disks editing on VM page should work ([xo-web#86](https://github.com/vatesfr/xo-web/issues/86))
- dropdown menus should close after selecting an item ([xo-web#140](https://github.com/vatesfr/xo-web/issues/140))
- user creation should require a password ([xo-web#143](https://github.com/vatesfr/xo-web/issues/143))
- server connection should require a user and a password ([xo-web#144](https://github.com/vatesfr/xo-web/issues/144))
- snapshot deletion should work ([xo-server#147](https://github.com/vatesfr/xo-server/issues/147))
- VM console should work in Chrome ([xo-web#149](https://github.com/vatesfr/xo-web/issues/149))
- tooltips should work ([xo-web#163](https://github.com/vatesfr/xo-web/issues/163))
- disk plugged status should be automatically refreshed ([xo-web#164](https://github.com/vatesfr/xo-web/issues/164))
- deleting users without tokens should not trigger an error ([xo-server#34](https://github.com/vatesfr/xo-server/issues/34))
- live pool master change should work ([xo-server#20](https://github.com/vatesfr/xo-server/issues/20))
## **3.5.1** (2014-08-19)
### Bug fixes
- pool view works again ([#139](https://github.com/vatesfr/xo-web/issues/139))
## **3.5.0** (2014-08-14)
*[XO-Web](https://www.npmjs.org/package/xo-web) and [XO-Server](https://www.npmjs.org/package/xo-server) are now available as npm packages!*
### Enhancements
- XO-Server published on npm ([#26](https://github.com/vatesfr/xo-server/issues/26))
- XO-Server config is now in `/etc/xo-server/config.yaml` ([#33](https://github.com/vatesfr/xo-server/issues/33))
- paths in XO-Server's config are now relative to the config file ([#19](https://github.com/vatesfr/xo-server/issues/19))
- use the Linux icon for Fedora ([#131](https://github.com/vatesfr/xo-web/issues/131))
- start/stop/reboot buttons on console page ([#121](https://github.com/vatesfr/xo-web/issues/121))
- settings page now only accessible to admin ([#77](https://github.com/vatesfr/xo-web/issues/77))
- redirection to the home page when a VM is deleted from its own page ([#56](https://github.com/vatesfr/xo-web/issues/56))
- XO-Web published on npm ([#123](https://github.com/vatesfr/xo-web/issues/123))
- buid process now use Browserify (([#125](https://github.com/vatesfr/xo-web/issues/125), [#135](https://github.com/vatesfr/xo-web/issues/135)))
- view are now written in Jade instead of HTML ([#124](https://github.com/vatesfr/xo-web/issues/124))
- CSS autoprefixer to improve compatibility ([#132](https://github.com/vatesfr/xo-web/issues/132), [#137](https://github.com/vatesfr/xo-web/issues/137))
### Bug fixes
- force shutdown does not attempt a clean shutdown first anymore ([#29](https://github.com/vatesfr/xo-server/issues/29))
- shutdown hosts are now correctly reported as such ([#31](https://github.com/vatesfr/xo-web/issues/31))
- incorrect VM metrics ([#54](https://github.com/vatesfr/xo-web/issues/54), [#68](https://github.com/vatesfr/xo-web/issues/68), [#108](https://github.com/vatesfr/xo-web/issues/108))
- an user cannot delete itself ([#104](https://github.com/vatesfr/xo-web/issues/104))
- in VM creation, required fields are now marked as such ([#113](https://github.com/vatesfr/xo-web/issues/113), [#114](https://github.com/vatesfr/xo-web/issues/114))
## **3.4.0** (2014-05-22)
*Highlight in this release is the new events system between XO-Web
and XO-Server which results in less bandwidth consumption as well as
better performance and reactivity.*
### Enhancements
- events system between XO-Web and XO-Server ([#52](https://github.com/vatesfr/xo-web/issues/52))
- ability to clone/copy a VM ([#116](https://github.com/vatesfr/xo-web/issues/116))
- mandatory log in page ([#120](https://github.com/vatesfr/xo-web/issues/120))
### Bug fixes
- failure in VM creation ([#111](https://github.com/vatesfr/xo-web/issues/111))
## **3.3.1** (2014-03-28)
### Enhancements
- console view is now prettier ([#92](https://github.com/vatesfr/xo-web/issues/92))
### Bug fixes
- VM creation fails to incorrect dependencies ([xo-server/#24](https://github.com/vatesfr/xo-server/issues/24))
- VDIs list in SR view is blinking ([#109](https://github.com/vatesfr/xo-web/issues/109))
## **3.3.0** (2014-03-07)
### Enhancements
- [Grunt](http://gruntjs.com/) has been replaced by [gulp.js](http://gulpjs.com/) ([#91](https://github.com/vatesfr/xo-web/issues/91))
- a host can be detached from a pool ([#98](https://github.com/vatesfr/xo-web/issues/98))
- snapshots management in VM view ([#99](https://github.com/vatesfr/xo-web/issues/99))
- log deletion in VM view ([#100](https://github.com/vatesfr/xo-web/issues/100))
### Bug fixes
- *Snapshot* not working in VM view ([#95](https://github.com/vatesfr/xo-web/issues/95))
- Host *Reboot*/*Restart toolstack*/*Shutdown* not working in main view ([#97](https://github.com/vatesfr/xo-web/issues/97))
- Bower cannot install `angular` automatically due to a version conflict ([#101](https://github.com/vatesfr/xo-web/issues/101))
- Bower installs an incorrect version of `angular-animate` ([#102](https://github.com/vatesfr/xo-web/issues/102))
## **3.2.0** (2014-02-21)
### Enhancements
- dependencies' versions should be fixed to ease deployment (#93)
- badges added to the README to see whether dependencies are up to date (#90)
- an error notification has been added when the connection to XO-Server failed (#89)
- in host view, there is now a link to the host console (#87)
- in VM view, deleting a disk requires a confirmation (#85)
- the VM and console icons are now different (#80)
- dependencies' versions should be fixed to ease deployment ([#93](https://github.com/vatesfr/xo-web/issues/93))
- badges added to the README to see whether dependencies are up to date ([#90](https://github.com/vatesfr/xo-web/issues/90))
- an error notification has been added when the connection to XO-Server failed ([#89](https://github.com/vatesfr/xo-web/issues/89))
- in host view, there is now a link to the host console ([#87](https://github.com/vatesfr/xo-web/issues/87))
- in VM view, deleting a disk requires a confirmation ([#85](https://github.com/vatesfr/xo-web/issues/85))
- the VM and console icons are now different ([#80](https://github.com/vatesfr/xo-web/issues/80))
### Bug fixes
- consoles now work in Google Chrome \o/ (#46)
- in host view, many buttons were not working (#79)
- in main view, incorrect icons were fixes (#81)
- MAC addresses should not be ignored during VM creation (#94)
- consoles now work in Google Chrome \o/ ([#46](https://github.com/vatesfr/xo-web/issues/46))
- in host view, many buttons were not working ([#79](https://github.com/vatesfr/xo-web/issues/79))
- in main view, incorrect icons were fixes ([#81](https://github.com/vatesfr/xo-web/issues/81))
- MAC addresses should not be ignored during VM creation ([#94](https://github.com/vatesfr/xo-web/issues/94))
## **3.1.0** (2014-02-14)
### Enhancements
- in VM view, interfaces' network should be displayed (#64)
- middle-click or `Ctrl`+click should open new windows (even on pseudo-links) (#66)
- lists should use natural sorting (e.g. *VM 2* before *VM 10*) (#69)
- in VM view, interfaces' network should be displayed ([#64](https://github.com/vatesfr/xo-web/issues/64))
- middle-click or `Ctrl`+click should open new windows (even on pseudo-links) ([#66](https://github.com/vatesfr/xo-web/issues/66))
- lists should use natural sorting (e.g. *VM 2* before *VM 10*) ([#69](https://github.com/vatesfr/xo-web/issues/69))
### Bug fixes
- consoles are not implemented for hosts (#57)
- consoles are not implemented for hosts ([#57](https://github.com/vatesfr/xo-web/issues/57))
- it makes no sense to remove a stand-alone host from a pool (58)
- in VM view, the migrate button is not working (#59)
- pool and host names overflow their box in the main view (#63)
- in host view, interfaces incorrectly named *networks* and VLAN not shown (#70)
- VM suspended state is not properly handled (#71)
- unauthenticated users should not be able to access to consoles (#73)
- incorrect scroll (under the navbar) when the view changes (#74)
- in VM view, the migrate button is not working ([#59](https://github.com/vatesfr/xo-web/issues/59))
- pool and host names overflow their box in the main view ([#63](https://github.com/vatesfr/xo-web/issues/63))
- in host view, interfaces incorrectly named *networks* and VLAN not shown ([#70](https://github.com/vatesfr/xo-web/issues/70))
- VM suspended state is not properly handled ([#71](https://github.com/vatesfr/xo-web/issues/71))
- unauthenticated users should not be able to access to consoles ([#73](https://github.com/vatesfr/xo-web/issues/73))
- incorrect scroll (under the navbar) when the view changes ([#74](https://github.com/vatesfr/xo-web/issues/74))

View File

@@ -1,410 +0,0 @@
// Generated on 2013-11-12 using generator-angular 0.6.0-rc.1
'use strict';
// # Globbing
// for performance reasons we're only matching one level down:
// 'test/spec/{,*/}*.js'
// use this if you want to recursively match all subfolders:
// 'test/spec/**/*.js'
module.exports = function (grunt) {
require('load-grunt-tasks')(grunt);
require('time-grunt')(grunt);
grunt.initConfig({
// Defines some variables.
yeoman: {
// configurable paths
app: require('./bower.json').appPath || 'app',
dist: 'dist'
},
// Runs appropriate tasks whenever a file changes.
watch: {
coffee: {
files: ['<%= yeoman.app %>/scripts/{,*/}*.coffee'],
tasks: ['coffee:dist']
},
coffeeTest: {
files: ['test/spec/{,*/}*.coffee'],
tasks: ['coffee:test']
},
compass: {
files: ['<%= yeoman.app %>/styles/{,*/}*.{scss,sass}'],
tasks: ['compass:server', 'autoprefixer']
},
styles: {
files: ['<%= yeoman.app %>/styles/{,*/}*.css'],
tasks: ['copy:styles', 'autoprefixer']
},
livereload: {
options: {
livereload: '<%= connect.options.livereload %>'
},
files: [
'<%= yeoman.app %>/{,*/}*.html',
'.tmp/styles/{,*/}*.css',
'{.tmp,<%= yeoman.app %>}/scripts/{,*/}*.js',
'<%= yeoman.app %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}'
]
}
},
// Automatically prefixes CSS rules.
autoprefixer: {
options: ['last 1 version'],
dist: {
files: [{
expand: true,
cwd: '.tmp/styles/',
src: '{,*/}*.css',
dest: '.tmp/styles/'
}]
}
},
// Starts an HTTP server.
connect: {
options: {
port: 9000,
// Change this to '0.0.0.0' to access the server from outside.
hostname: 'localhost',
livereload: 35729
},
livereload: {
options: {
base: [
'.tmp',
'<%= yeoman.app %>'
]
}
},
test: {
options: {
port: 9001,
base: [
'.tmp',
'test',
'<%= yeoman.app %>'
]
}
},
dist: {
options: {
base: '<%= yeoman.dist %>'
}
}
},
// Deletes dirty files before build.
clean: {
dist: {
files: [{
dot: true,
src: [
'.tmp',
'<%= yeoman.dist %>/*',
'!<%= yeoman.dist %>/.git*'
]
}]
},
server: '.tmp'
},
// Checks JavaScript files for errors and potential problems.
jshint: {
options: {
jshintrc: '.jshintrc',
reporter: require('jshint-stylish')
},
all: [
'Gruntfile.js',
'<%= yeoman.app %>/scripts/{,*/}*.js'
]
},
// Compiles CoffeeScript files to JavaScript.
coffee: {
options: {
sourceMap: true,
sourceRoot: ''
},
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.app %>/scripts',
src: '{,*/}*.coffee',
dest: '.tmp/scripts',
ext: '.js'
}]
},
test: {
files: [{
expand: true,
cwd: 'test/spec',
src: '{,*/}*.coffee',
dest: '.tmp/spec',
ext: '.js'
}]
}
},
// Compiles SASS/SCSS files to CSS using Compass.
compass: {
options: {
sassDir: '<%= yeoman.app %>/styles',
cssDir: '.tmp/styles',
generatedImagesDir: '.tmp/images/generated',
imagesDir: '<%= yeoman.app %>/images',
javascriptsDir: '<%= yeoman.app %>/scripts',
fontsDir: '<%= yeoman.app %>/fonts',
importPath: '<%= yeoman.app %>/bower_components',
httpImagesPath: '/images',
httpGeneratedImagesPath: '/images/generated',
httpFontsPath: '/fonts',
relativeAssets: false
},
dist: {},
server: {
options: {
debugInfo: true
}
}
},
// not used since Uglify task does concat,
// but still available if needed
/*concat: {
dist: {}
},*/
// Adds assets hash to their filename (useful for caching).
rev: {
dist: {
files: {
src: [
'<%= yeoman.dist %>/scripts/{,*/}*.js',
'<%= yeoman.dist %>/styles/{,*/}*.css',
'<%= yeoman.dist %>/images/{,*/}*.{png,jpg,jpeg,gif,webp,svg}',
'<%= yeoman.dist %>/styles/fonts/*'
]
}
}
},
//
useminPrepare: {
html: '<%= yeoman.app %>/index.html',
options: {
dest: '<%= yeoman.dist %>'
}
},
//
usemin: {
html: ['<%= yeoman.dist %>/{,*/}*.html'],
css: ['<%= yeoman.dist %>/styles/{,*/}*.css'],
options: {
assetsDirs: ['<%= yeoman.dist %>']
}
},
// Compresses PNG and JPG images.
imagemin: {
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.app %>/images',
src: '{,*/}*.{png,jpg,jpeg}',
dest: '<%= yeoman.dist %>/images'
}]
}
},
// Compresses SVG images.
svgmin: {
dist: {
files: [{
expand: true,
cwd: '<%= yeoman.app %>/images',
src: '{,*/}*.svg',
dest: '<%= yeoman.dist %>/images'
}]
}
},
// Compresses CSS stylesheets.
cssmin: {
// By default, your `index.html` <!-- Usemin Block --> will take care of
// minification. This option is pre-configured if you do not wish to use
// Usemin blocks.
// dist: {
// files: {
// '<%= yeoman.dist %>/styles/main.css': [
// '.tmp/styles/{,*/}*.css',
// '<%= yeoman.app %>/styles/{,*/}*.css'
// ]
// }
// }
},
// Compresses HTML pages.
htmlmin: {
dist: {
options: {
/*removeCommentsFromCDATA: true,
// https://github.com/yeoman/grunt-usemin/issues/44
//collapseWhitespace: true,
collapseBooleanAttributes: true,
removeAttributeQuotes: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeOptionalTags: true*/
},
files: [{
expand: true,
cwd: '<%= yeoman.app %>',
src: ['*.html', 'views/*.html'],
dest: '<%= yeoman.dist %>'
}]
}
},
// Put files not handled in other tasks here
copy: {
dist: {
files: [{
expand: true,
dot: true,
cwd: '<%= yeoman.app %>',
dest: '<%= yeoman.dist %>',
src: [
'*.{ico,png,txt}',
'.htaccess',
'bower_components/**/*',
'images/{,*/}*.{gif,webp}',
'fonts/*'
]
}, {
expand: true,
cwd: '.tmp/images',
dest: '<%= yeoman.dist %>/images',
src: [
'generated/*'
]
}]
},
styles: {
expand: true,
cwd: '<%= yeoman.app %>/styles',
dest: '.tmp/styles/',
src: '{,*/}*.css'
}
},
// Runs some tasks concurrently.
concurrent: {
server: [
'coffee:dist',
'compass:server',
'copy:styles'
],
test: [
'coffee',
'compass',
'copy:styles'
],
dist: [
'coffee',
'compass:dist',
'copy:styles',
'imagemin',
'svgmin',
'htmlmin'
]
},
// Runs the test configuration.
karma: {
unit: {
configFile: 'karma.conf.js',
singleRun: true
}
},
// Updates HTML pages to use Google CDN for assets when possible.
cdnify: {
dist: {
html: ['<%= yeoman.dist %>/*.html']
}
},
// Ensures the Angular code will still work after minification.
ngmin: {
dist: {
files: [{
expand: true,
cwd: '.tmp/concat/scripts',
src: '*.js',
dest: '.tmp/concat/scripts'
}]
}
},
// Minifies JavaScript files.
uglify: {
dist: {
files: {
'<%= yeoman.dist %>/scripts/scripts.js': [
'<%= yeoman.dist %>/scripts/scripts.js'
]
}
}
}
});
grunt.registerTask('server', 'Runs a web server.', function (target) {
if (target === 'dist') {
return grunt.task.run(['build', 'connect:dist:keepalive']);
}
grunt.task.run([
'clean:server',
'concurrent:server',
'autoprefixer',
'connect:livereload',
'watch'
]);
});
grunt.registerTask('test', 'Runs the application\'s tests.', [
'clean:server',
'concurrent:test',
'autoprefixer',
'connect:test',
'karma'
]);
grunt.registerTask('build', 'Builds the application for distribution.', [
'clean:dist',
'useminPrepare',
'concurrent:dist',
'autoprefixer',
'concat',
'ngmin',
'copy:dist',
'cdnify',
'cssmin',
'uglify',
'rev',
'usemin'
]);
grunt.registerTask('default', 'Tests and build the application.', [
'jshint',
'test',
'build'
]);
};

661
LICENSE Normal file
View File

@@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.

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).
@@ -11,10 +13,57 @@ ___
## Installation
Manual install procedure is [available here](https://github.com/vatesfr/xo/blob/master/installation.md)
XOA or manual install procedure is [available here](https://github.com/vatesfr/xo/blob/master/doc/installation/README.md)
## Compilation
Production build:
```
$ npm run build
```
Development build:
```
$ npm run dev
```
## How to report a bug?
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
# Fetches latest changes.
git pull --ff-only
# Merge changes of the next-release branch.
git merge next-release
# Increment the version (patch, minor or major).
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
# next-release.
git merge --ff-only master
# Push the changes on git.
git push --follow-tags origin master next-release
# Publish this release to npm.
npm publish
```
## License
AGPL3 © [Vates SAS](http://vates.fr)

View File

@@ -1 +0,0 @@
*.coffee

View File

@@ -1,543 +0,0 @@
# Apache Configuration File
# (!) Using `.htaccess` files slows down Apache, therefore, if you have access
# to the main server config file (usually called `httpd.conf`), you should add
# this logic there: http://httpd.apache.org/docs/current/howto/htaccess.html.
# ##############################################################################
# # CROSS-ORIGIN RESOURCE SHARING (CORS) #
# ##############################################################################
# ------------------------------------------------------------------------------
# | Cross-domain AJAX requests |
# ------------------------------------------------------------------------------
# Enable cross-origin AJAX requests.
# http://code.google.com/p/html5security/wiki/CrossOriginRequestSecurity
# http://enable-cors.org/
# <IfModule mod_headers.c>
# Header set Access-Control-Allow-Origin "*"
# </IfModule>
# ------------------------------------------------------------------------------
# | CORS-enabled images |
# ------------------------------------------------------------------------------
# Send the CORS header for images when browsers request it.
# https://developer.mozilla.org/en/CORS_Enabled_Image
# http://blog.chromium.org/2011/07/using-cross-domain-images-in-webgl-and.html
# http://hacks.mozilla.org/2011/11/using-cors-to-load-webgl-textures-from-cross-domain-images/
<IfModule mod_setenvif.c>
<IfModule mod_headers.c>
<FilesMatch "\.(gif|ico|jpe?g|png|svg|svgz|webp)$">
SetEnvIf Origin ":" IS_CORS
Header set Access-Control-Allow-Origin "*" env=IS_CORS
</FilesMatch>
</IfModule>
</IfModule>
# ------------------------------------------------------------------------------
# | Web fonts access |
# ------------------------------------------------------------------------------
# Allow access from all domains for web fonts
<IfModule mod_headers.c>
<FilesMatch "\.(eot|font.css|otf|ttc|ttf|woff)$">
Header set Access-Control-Allow-Origin "*"
</FilesMatch>
</IfModule>
# ##############################################################################
# # ERRORS #
# ##############################################################################
# ------------------------------------------------------------------------------
# | 404 error prevention for non-existing redirected folders |
# ------------------------------------------------------------------------------
# Prevent Apache from returning a 404 error for a rewrite if a directory
# with the same name does not exist.
# http://httpd.apache.org/docs/current/content-negotiation.html#multiviews
# http://www.webmasterworld.com/apache/3808792.htm
Options -MultiViews
# ------------------------------------------------------------------------------
# | Custom error messages / pages |
# ------------------------------------------------------------------------------
# You can customize what Apache returns to the client in case of an error (see
# http://httpd.apache.org/docs/current/mod/core.html#errordocument), e.g.:
ErrorDocument 404 /404.html
# ##############################################################################
# # INTERNET EXPLORER #
# ##############################################################################
# ------------------------------------------------------------------------------
# | Better website experience |
# ------------------------------------------------------------------------------
# Force IE to render pages in the highest available mode in the various
# cases when it may not: http://hsivonen.iki.fi/doctype/ie-mode.pdf.
<IfModule mod_headers.c>
Header set X-UA-Compatible "IE=edge"
# `mod_headers` can't match based on the content-type, however, we only
# want to send this header for HTML pages and not for the other resources
<FilesMatch "\.(appcache|crx|css|eot|gif|htc|ico|jpe?g|js|m4a|m4v|manifest|mp4|oex|oga|ogg|ogv|otf|pdf|png|safariextz|svg|svgz|ttf|vcf|webapp|webm|webp|woff|xml|xpi)$">
Header unset X-UA-Compatible
</FilesMatch>
</IfModule>
# ------------------------------------------------------------------------------
# | Cookie setting from iframes |
# ------------------------------------------------------------------------------
# Allow cookies to be set from iframes in IE.
# <IfModule mod_headers.c>
# Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT\""
# </IfModule>
# ------------------------------------------------------------------------------
# | Screen flicker |
# ------------------------------------------------------------------------------
# Stop screen flicker in IE on CSS rollovers (this only works in
# combination with the `ExpiresByType` directives for images from below).
# BrowserMatch "MSIE" brokenvary=1
# BrowserMatch "Mozilla/4.[0-9]{2}" brokenvary=1
# BrowserMatch "Opera" !brokenvary
# SetEnvIf brokenvary 1 force-no-vary
# ##############################################################################
# # MIME TYPES AND ENCODING #
# ##############################################################################
# ------------------------------------------------------------------------------
# | Proper MIME types for all files |
# ------------------------------------------------------------------------------
<IfModule mod_mime.c>
# Audio
AddType audio/mp4 m4a f4a f4b
AddType audio/ogg oga ogg
# JavaScript
# Normalize to standard type (it's sniffed in IE anyways):
# http://tools.ietf.org/html/rfc4329#section-7.2
AddType application/javascript js jsonp
AddType application/json json
# Video
AddType video/mp4 mp4 m4v f4v f4p
AddType video/ogg ogv
AddType video/webm webm
AddType video/x-flv flv
# Web fonts
AddType application/font-woff woff
AddType application/vnd.ms-fontobject eot
# Browsers usually ignore the font MIME types and sniff the content,
# however, Chrome shows a warning if other MIME types are used for the
# following fonts.
AddType application/x-font-ttf ttc ttf
AddType font/opentype otf
# Make SVGZ fonts work on iPad:
# https://twitter.com/FontSquirrel/status/14855840545
AddType image/svg+xml svg svgz
AddEncoding gzip svgz
# Other
AddType application/octet-stream safariextz
AddType application/x-chrome-extension crx
AddType application/x-opera-extension oex
AddType application/x-shockwave-flash swf
AddType application/x-web-app-manifest+json webapp
AddType application/x-xpinstall xpi
AddType application/xml atom rdf rss xml
AddType image/webp webp
AddType image/x-icon ico
AddType text/cache-manifest appcache manifest
AddType text/vtt vtt
AddType text/x-component htc
AddType text/x-vcard vcf
</IfModule>
# ------------------------------------------------------------------------------
# | UTF-8 encoding |
# ------------------------------------------------------------------------------
# Use UTF-8 encoding for anything served as `text/html` or `text/plain`.
AddDefaultCharset utf-8
# Force UTF-8 for certain file formats.
<IfModule mod_mime.c>
AddCharset utf-8 .atom .css .js .json .rss .vtt .webapp .xml
</IfModule>
# ##############################################################################
# # URL REWRITES #
# ##############################################################################
# ------------------------------------------------------------------------------
# | Rewrite engine |
# ------------------------------------------------------------------------------
# Turning on the rewrite engine and enabling the `FollowSymLinks` option is
# necessary for the following directives to work.
# If your web host doesn't allow the `FollowSymlinks` option, you may need to
# comment it out and use `Options +SymLinksIfOwnerMatch` but, be aware of the
# performance impact: http://httpd.apache.org/docs/current/misc/perf-tuning.html#symlinks
# Also, some cloud hosting services require `RewriteBase` to be set:
# http://www.rackspace.com/knowledge_center/frequently-asked-question/why-is-mod-rewrite-not-working-on-my-site
<IfModule mod_rewrite.c>
Options +FollowSymlinks
# Options +SymLinksIfOwnerMatch
RewriteEngine On
# RewriteBase /
</IfModule>
# ------------------------------------------------------------------------------
# | Suppressing / Forcing the "www." at the beginning of URLs |
# ------------------------------------------------------------------------------
# The same content should never be available under two different URLs especially
# not with and without "www." at the beginning. This can cause SEO problems
# (duplicate content), therefore, you should choose one of the alternatives and
# redirect the other one.
# By default option 1 (no "www.") is activated:
# http://no-www.org/faq.php?q=class_b
# If you'd prefer to use option 2, just comment out all the lines from option 1
# and uncomment the ones from option 2.
# IMPORTANT: NEVER USE BOTH RULES AT THE SAME TIME!
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Option 1: rewrite www.example.com → example.com
<IfModule mod_rewrite.c>
RewriteCond %{HTTPS} !=on
RewriteCond %{HTTP_HOST} ^www\.(.+)$ [NC]
RewriteRule ^ http://%1%{REQUEST_URI} [R=301,L]
</IfModule>
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Option 2: rewrite example.com → www.example.com
# Be aware that the following might not be a good idea if you use "real"
# subdomains for certain parts of your website.
# <IfModule mod_rewrite.c>
# RewriteCond %{HTTPS} !=on
# RewriteCond %{HTTP_HOST} !^www\..+$ [NC]
# RewriteRule ^ http://www.%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
# </IfModule>
# ##############################################################################
# # SECURITY #
# ##############################################################################
# ------------------------------------------------------------------------------
# | Content Security Policy (CSP) |
# ------------------------------------------------------------------------------
# You can mitigate the risk of cross-site scripting and other content-injection
# attacks by setting a Content Security Policy which whitelists trusted sources
# of content for your site.
# The example header below allows ONLY scripts that are loaded from the current
# site's origin (no inline scripts, no CDN, etc). This almost certainly won't
# work as-is for your site!
# To get all the details you'll need to craft a reasonable policy for your site,
# read: http://html5rocks.com/en/tutorials/security/content-security-policy (or
# see the specification: http://w3.org/TR/CSP).
# <IfModule mod_headers.c>
# Header set Content-Security-Policy "script-src 'self'; object-src 'self'"
# <FilesMatch "\.(appcache|crx|css|eot|gif|htc|ico|jpe?g|js|m4a|m4v|manifest|mp4|oex|oga|ogg|ogv|otf|pdf|png|safariextz|svg|svgz|ttf|vcf|webapp|webm|webp|woff|xml|xpi)$">
# Header unset Content-Security-Policy
# </FilesMatch>
# </IfModule>
# ------------------------------------------------------------------------------
# | File access |
# ------------------------------------------------------------------------------
# Block access to directories without a default document.
# Usually you should leave this uncommented because you shouldn't allow anyone
# to surf through every directory on your server (which may includes rather
# private places like the CMS's directories).
<IfModule mod_autoindex.c>
Options -Indexes
</IfModule>
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Block access to hidden files and directories.
# This includes directories used by version control systems such as Git and SVN.
<IfModule mod_rewrite.c>
RewriteCond %{SCRIPT_FILENAME} -d [OR]
RewriteCond %{SCRIPT_FILENAME} -f
RewriteRule "(^|/)\." - [F]
</IfModule>
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Block access to backup and source files.
# These files may be left by some text editors and can pose a great security
# danger when anyone has access to them.
<FilesMatch "(^#.*#|\.(bak|config|dist|fla|inc|ini|log|psd|sh|sql|sw[op])|~)$">
Order allow,deny
Deny from all
Satisfy All
</FilesMatch>
# ------------------------------------------------------------------------------
# | Secure Sockets Layer (SSL) |
# ------------------------------------------------------------------------------
# Rewrite secure requests properly to prevent SSL certificate warnings, e.g.:
# prevent `https://www.example.com` when your certificate only allows
# `https://secure.example.com`.
# <IfModule mod_rewrite.c>
# RewriteCond %{SERVER_PORT} !^443
# RewriteRule ^ https://example-domain-please-change-me.com%{REQUEST_URI} [R=301,L]
# </IfModule>
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Force client-side SSL redirection.
# If a user types "example.com" in his browser, the above rule will redirect him
# to the secure version of the site. That still leaves a window of opportunity
# (the initial HTTP connection) for an attacker to downgrade or redirect the
# request. The following header ensures that browser will ONLY connect to your
# server via HTTPS, regardless of what the users type in the address bar.
# http://www.html5rocks.com/en/tutorials/security/transport-layer-security/
# <IfModule mod_headers.c>
# Header set Strict-Transport-Security max-age=16070400;
# </IfModule>
# ------------------------------------------------------------------------------
# | Server software information |
# ------------------------------------------------------------------------------
# Avoid displaying the exact Apache version number, the description of the
# generic OS-type and the information about Apache's compiled-in modules.
# ADD THIS DIRECTIVE IN THE `httpd.conf` AS IT WILL NOT WORK IN THE `.htaccess`!
# ServerTokens Prod
# ##############################################################################
# # WEB PERFORMANCE #
# ##############################################################################
# ------------------------------------------------------------------------------
# | Compression |
# ------------------------------------------------------------------------------
<IfModule mod_deflate.c>
# Force compression for mangled headers.
# http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping
<IfModule mod_setenvif.c>
<IfModule mod_headers.c>
SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding
RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding
</IfModule>
</IfModule>
# Compress all output labeled with one of the following MIME-types
# (for Apache versions below 2.3.7, you don't need to enable `mod_filter`
# and can remove the `<IfModule mod_filter.c>` and `</IfModule>` lines
# as `AddOutputFilterByType` is still in the core directives).
<IfModule mod_filter.c>
AddOutputFilterByType DEFLATE application/atom+xml \
application/javascript \
application/json \
application/rss+xml \
application/vnd.ms-fontobject \
application/x-font-ttf \
application/x-web-app-manifest+json \
application/xhtml+xml \
application/xml \
font/opentype \
image/svg+xml \
image/x-icon \
text/css \
text/html \
text/plain \
text/x-component \
text/xml
</IfModule>
</IfModule>
# ------------------------------------------------------------------------------
# | Content transformations |
# ------------------------------------------------------------------------------
# Prevent some of the mobile network providers from modifying the content of
# your site: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.5.
# <IfModule mod_headers.c>
# Header set Cache-Control "no-transform"
# </IfModule>
# ------------------------------------------------------------------------------
# | ETag removal |
# ------------------------------------------------------------------------------
# Since we're sending far-future expires headers (see below), ETags can
# be removed: http://developer.yahoo.com/performance/rules.html#etags.
# `FileETag None` is not enough for every server.
<IfModule mod_headers.c>
Header unset ETag
</IfModule>
FileETag None
# ------------------------------------------------------------------------------
# | Expires headers (for better cache control) |
# ------------------------------------------------------------------------------
# The following expires headers are set pretty far in the future. If you don't
# control versioning with filename-based cache busting, consider lowering the
# cache time for resources like CSS and JS to something like 1 week.
<IfModule mod_expires.c>
ExpiresActive on
ExpiresDefault "access plus 1 month"
# CSS
ExpiresByType text/css "access plus 1 year"
# Data interchange
ExpiresByType application/json "access plus 0 seconds"
ExpiresByType application/xml "access plus 0 seconds"
ExpiresByType text/xml "access plus 0 seconds"
# Favicon (cannot be renamed!)
ExpiresByType image/x-icon "access plus 1 week"
# HTML components (HTCs)
ExpiresByType text/x-component "access plus 1 month"
# HTML
ExpiresByType text/html "access plus 0 seconds"
# JavaScript
ExpiresByType application/javascript "access plus 1 year"
# Manifest files
ExpiresByType application/x-web-app-manifest+json "access plus 0 seconds"
ExpiresByType text/cache-manifest "access plus 0 seconds"
# Media
ExpiresByType audio/ogg "access plus 1 month"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType video/mp4 "access plus 1 month"
ExpiresByType video/ogg "access plus 1 month"
ExpiresByType video/webm "access plus 1 month"
# Web feeds
ExpiresByType application/atom+xml "access plus 1 hour"
ExpiresByType application/rss+xml "access plus 1 hour"
# Web fonts
ExpiresByType application/font-woff "access plus 1 month"
ExpiresByType application/vnd.ms-fontobject "access plus 1 month"
ExpiresByType application/x-font-ttf "access plus 1 month"
ExpiresByType font/opentype "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
</IfModule>
# ------------------------------------------------------------------------------
# | Filename-based cache busting |
# ------------------------------------------------------------------------------
# If you're not using a build process to manage your filename version revving,
# you might want to consider enabling the following directives to route all
# requests such as `/css/style.12345.css` to `/css/style.css`.
# To understand why this is important and a better idea than `*.css?v231`, read:
# http://stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring
# <IfModule mod_rewrite.c>
# RewriteCond %{REQUEST_FILENAME} !-f
# RewriteCond %{REQUEST_FILENAME} !-d
# RewriteRule ^(.+)\.(\d+)\.(js|css|png|jpg|gif)$ $1.$3 [L]
# </IfModule>
# ------------------------------------------------------------------------------
# | File concatenation |
# ------------------------------------------------------------------------------
# Allow concatenation from within specific CSS and JS files, e.g.:
# Inside of `script.combined.js` you could have
# <!--#include file="libs/jquery.js" -->
# <!--#include file="plugins/jquery.idletimer.js" -->
# and they would be included into this single file.
# <IfModule mod_include.c>
# <FilesMatch "\.combined\.js$">
# Options +Includes
# AddOutputFilterByType INCLUDES application/javascript application/json
# SetOutputFilter INCLUDES
# </FilesMatch>
# <FilesMatch "\.combined\.css$">
# Options +Includes
# AddOutputFilterByType INCLUDES text/css
# SetOutputFilter INCLUDES
# </FilesMatch>
# </IfModule>
# ------------------------------------------------------------------------------
# | Persistent connections |
# ------------------------------------------------------------------------------
# Allow multiple requests to be sent over the same TCP connection:
# http://httpd.apache.org/docs/current/en/mod/core.html#keepalive.
# Enable if you serve a lot of static content but, be aware of the
# possible disadvantages!
# <IfModule mod_headers.c>
# Header set Connection Keep-Alive
# </IfModule>

View File

@@ -1,157 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Page Not Found :(</title>
<style>
::-moz-selection {
background: #b3d4fc;
text-shadow: none;
}
::selection {
background: #b3d4fc;
text-shadow: none;
}
html {
padding: 30px 10px;
font-size: 20px;
line-height: 1.4;
color: #737373;
background: #f0f0f0;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
}
html,
input {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
body {
max-width: 500px;
_width: 500px;
padding: 30px 20px 50px;
border: 1px solid #b3b3b3;
border-radius: 4px;
margin: 0 auto;
box-shadow: 0 1px 10px #a7a7a7, inset 0 1px 0 #fff;
background: #fcfcfc;
}
h1 {
margin: 0 10px;
font-size: 50px;
text-align: center;
}
h1 span {
color: #bbb;
}
h3 {
margin: 1.5em 0 0.5em;
}
p {
margin: 1em 0;
}
ul {
padding: 0 0 0 40px;
margin: 1em 0;
}
.container {
max-width: 380px;
_width: 380px;
margin: 0 auto;
}
/* google search */
#goog-fixurl ul {
list-style: none;
padding: 0;
margin: 0;
}
#goog-fixurl form {
margin: 0;
}
#goog-wm-qt,
#goog-wm-sb {
border: 1px solid #bbb;
font-size: 16px;
line-height: normal;
vertical-align: top;
color: #444;
border-radius: 2px;
}
#goog-wm-qt {
width: 220px;
height: 20px;
padding: 5px;
margin: 5px 10px 0 0;
box-shadow: inset 0 1px 1px #ccc;
}
#goog-wm-sb {
display: inline-block;
height: 32px;
padding: 0 10px;
margin: 5px 0 0;
white-space: nowrap;
cursor: pointer;
background-color: #f5f5f5;
background-image: -webkit-linear-gradient(rgba(255,255,255,0), #f1f1f1);
background-image: -moz-linear-gradient(rgba(255,255,255,0), #f1f1f1);
background-image: -ms-linear-gradient(rgba(255,255,255,0), #f1f1f1);
background-image: -o-linear-gradient(rgba(255,255,255,0), #f1f1f1);
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
*overflow: visible;
*display: inline;
*zoom: 1;
}
#goog-wm-sb:hover,
#goog-wm-sb:focus {
border-color: #aaa;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
background-color: #f8f8f8;
}
#goog-wm-qt:hover,
#goog-wm-qt:focus {
border-color: #105cb6;
outline: 0;
color: #222;
}
input::-moz-focus-inner {
padding: 0;
border: 0;
}
</style>
</head>
<body>
<div class="container">
<h1>Not found <span>:(</span></h1>
<p>Sorry, but the page you were trying to view does not exist.</p>
<p>It looks like this was the result of either:</p>
<ul>
<li>a mistyped address</li>
<li>an out-of-date link</li>
</ul>
<script>
var GOOG_FIXURL_LANG = (navigator.language || '').slice(0,2),GOOG_FIXURL_SITE = location.host;
</script>
<script src="//linkhelp.clients.google.com/tbproxy/lh/wm/fixurl.js"></script>
</div>
</body>
</html>

172
app/app.js Normal file
View File

@@ -0,0 +1,172 @@
// Must be loaded before angular.
import 'angular-file-upload'
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 xoDirectives from 'xo-directives'
import xoFilters from 'xo-filters'
import xoServices from 'xo-services'
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 navbarState from './modules/navbar'
import newSrState from './modules/new-sr'
import newVmState from './modules/new-vm'
import poolState from './modules/pool'
import schedulerState from './modules/scheduler'
import settingsState from './modules/settings'
import dashboardState from './modules/dashboard'
import srState from './modules/sr'
import treeState from './modules/tree'
import updater from './modules/updater'
import vmState from './modules/vm'
import '../dist/bower_components/angular-chart.js/dist/angular-chart.js'
// ===================================================================
console.log('done')
export default angular.module('xoWebApp', [
uiBootstrap,
uiIndeterminate,
uiRouter,
uiSelect,
naturalSort,
xeditable,
xoDirectives,
xoFilters,
xoServices,
aboutState,
consoleState,
dashboardState,
deleteVmsState,
genericModalState,
hostState,
listState,
navbarState,
newSrState,
newVmState,
poolState,
schedulerState,
settingsState,
srState,
treeState,
updater,
vmState,
'chart.js'
])
// Prevent Angular.js from mangling exception stack (interfere with
// source maps).
.factory('$exceptionHandler', () => function (exception) {
throw exception
})
.config(function (
$compileProvider,
$stateProvider,
$urlRouterProvider,
$tooltipProvider,
uiSelectConfig
) {
// Disable debug data to improve performance.
//
// In case of a bug, simply use `angular.reloadWithDebugInfo()` in
// the console.
//
// See https://docs.angularjs.org/guide/production
$compileProvider.debugInfoEnabled(false)
// Redirect to default state.
$stateProvider.state('index', {
url: '/',
controller: function ($state, xoApi) {
let isAdmin = xoApi.user && (xoApi.user.permission === 'admin')
$state.go(isAdmin ? 'tree' : 'list')
}
})
// Redirects unmatched URLs to `/`.
$urlRouterProvider.otherwise('/')
// Changes the default settings for the tooltips.
$tooltipProvider.options({
appendToBody: true,
placement: 'bottom'
})
uiSelectConfig.theme = 'bootstrap'
uiSelectConfig.resetSearchInput = true
})
.run(function (
$anchorScroll,
$rootScope,
$state,
editableOptions,
editableThemes,
notify,
updater,
xoApi
) {
$rootScope.$watch(() => xoApi.user, (user, previous) => {
// The user just signed in.
if (user && !previous) {
$state.go('index')
}
})
$rootScope.$on('$stateChangeStart', function (event, state, stateParams) {
const {user} = xoApi
if (!user) {
event.preventDefault()
return
}
if (user.permission === 'admin') {
return
}
// Some pages requires the admin permission.
if (state.data && state.data.requireAdmin) {
event.preventDefault()
notify.error({
title: 'Restricted area',
message: 'You do not have the permission to view this page'
})
}
let {id} = stateParams
if (id && !xoApi.canAccess(id)) {
event.preventDefault()
notify.error({
title: 'Restricted area',
message: 'You do not have the permission to view this page'
})
}
})
// Work around UI Router bug (https://github.com/angular-ui/ui-router/issues/1509)
$rootScope.$on('$stateChangeSuccess', function () {
$anchorScroll()
})
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/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
app/images/logo_small.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -1,140 +0,0 @@
<!-- HTML 5 Doctype -->
<!doctype html>
<!-- The “no-js” class will be automatically removed if JavaScript is
- available.
-->
<html lang="en" dir="ltr" class="no-js">
<head>
<meta charset="utf-8">
<!--
- This file is a part of Xen Orchestra Web.
-
- Xen Orchestra Web is free software: you can redistribute it and/or
- modify it under the terms of the GNU Affero General Public License
- as published by the Free Software Foundation, either version 3 of
- the License, or (at your option) any later version.
-
- Xen Orchestra Web is distributed in the hope that it will be
- useful, but WITHOUT ANY WARRANTY; without even the implied warranty
- of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with Xen Orchestra Web. If not, see
- <http://www.gnu.org/licenses/>.
-
- @author Olivier Lambert <olivier.lambert@vates.fr>
- @license http://www.gnu.org/licenses/agpl-3.0-standalone.html GNU AGPLv3
-
- @package Xen Orchestra Web
-->
<!-- Makes sure IE is using the last engine available. -->
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<!-- Replaces the “no-js” class by the “js” class if JavaScript
- is available.
-->
<script>!function(d){d.className=d.className.replace(/\bno-js\b/,'js')}(document.documentElement)</script>
<!-- (To confirm.) For smartphones and tablets: sets the page
- width to the device width and prevents the page from being
- zoomed in when going to landscape mode.
-->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Xen Orchestra</title>
<meta name="description" content="Web interface for XenServer/XAPI Hosts" />
<meta name="author" content="Vates SAS" />
<!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
<!-- build:css(.tmp) styles/main.css -->
<link rel="stylesheet" href="styles/main.css">
<!-- endbuild -->
<!-- Angular-xeditable CSS -->
<link rel="stylesheet" href="bower_components/angular-xeditable/dist/css/xeditable.css">
<!-- Angular UI Select -->
<link rel="stylesheet" href="bower_components/select2/select2.css">
<link rel="stylesheet" href="bower_components/angular-notify-toaster/toaster.css">
</head>
<body ng-app="xoWebApp">
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<!--[if lt IE 9]>
<script src="bower_components/es5-shim/es5-shim.js"></script>
<script src="bower_components/json3/lib/json3.min.js"></script>
<![endif]-->
<toaster-container></toaster-container>
<!-- Navigation bar. -->
<div ng-controller="NavBarCtrl">
<ng-include src="'views/nav-bar.html'"></ng-include>
</div>
<!-- Main content (managed by the router). -->
<div class="view-main" ui-view></div>
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-animate/angular-animate.js"></script>
<script src="bower_components/angular-cookies/angular-cookies.js"></script>
<!-- build:js scripts/plugins.js -->
<script src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.js"></script>
<script src="bower_components/angular-ui-router/release/angular-ui-router.js"></script>
<script src="bower_components/angular-ui-utils/ui-utils.js"></script>
<script src="bower_components/angular-notify-toaster/toaster.js"></script>
<!-- endbuild -->
<!-- build:js scripts/modules.js -->
<script src="bower_components/angular-xeditable/dist/js/xeditable.js"></script>
<script src="bower_components/select2/select2.js"></script>
<script src="bower_components/angular-ui-select2/src/select2.js"></script>
<script src="bower_components/angularjs-naturalsort/index.js"></script>
<!-- endbuild -->
<!-- noVNC -->
<script src="bower_components/noVNC/include/util.js"></script>
<script>
var INCLUDE_URI = 'bower_components/noVNC/include/';
Util.load_scripts([
'webutil.js',
'base64.js',
'websock.js',
'des.js',
'keysymdef.js',
'keyboard.js',
'input.js',
'display.js',
'jsunzip.js',
'rfb.js',
]);
</script>
<!-- /noVNC -->
<!-- build:js({.tmp,app}) scripts/scripts.js -->
<script src="scripts/app.js"></script>
<script src="scripts/directives.js"></script>
<script src="scripts/filters.js"></script>
<script src="scripts/services.js"></script>
<script src="scripts/controllers/main.js"></script>
<script src="scripts/controllers/nav-bar.js"></script>
<script src="scripts/controllers/list.js"></script>
<script src="scripts/controllers/host.js"></script>
<script src="scripts/controllers/sr.js"></script>
<script src="scripts/controllers/pool.js"></script>
<script src="scripts/controllers/vm.js"></script>
<script src="scripts/controllers/console.js"></script>
<script src="scripts/controllers/settings.js"></script>
<script src="scripts/controllers/new_vm.js"></script>
<script src="scripts/controllers/new_sr.js"></script>
<script src="scripts/controllers/delete_vms.js"></script>
<script src="scripts/controllers/generic_modal.js"></script>
<!-- endbuild -->
</body>
</html>

62
app/index.jade Normal file
View File

@@ -0,0 +1,62 @@
//- HTML 5 Doctype
doctype html
//- The “no-js” class will be automatically removed if JavaScript is
//- available.
html.no-js(lang="en", dir="ltr")
head
meta(charset="utf-8")
//- This file is a part of Xen Orchestra Web.
//-
//- Xen Orchestra Web is free software: you can redistribute it and/or
//- modify it under the terms of the GNU Affero General Public License
//- as published by the Free Software Foundation, either version 3 of
//- the License, or (at your option) any later version.
//-
//- Xen Orchestra Web is distributed in the hope that it will be
//- useful, but WITHOUT ANY WARRANTY; without even the implied warranty
//- of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
//- Affero General Public License for more details.
//-
//- You should have received a copy of the GNU Affero General Public License
//- along with Xen Orchestra Web. If not, see
//- <http://www.gnu.org/licenses/>.
//-
//- @author Olivier Lambert <olivier.lambert@vates.fr>
//- @license http://www.gnu.org/licenses/agpl-3.0-standalone.html GNU AGPLv3
//-
//- @package Xen Orchestra Web
//- Makes sure IE is using the last engine available.
meta(http-equiv="X-UA-Compatible", content="IE=edge,chrome=1")
//- Replaces the “no-js” class by the “js” class if JavaScript is
//- available.
script.
!function(d){d.className=d.className.replace(/\\bno-js\b/,'js')}(document.documentElement)
//- (To confirm.) For smartphones and tablets: sets the page
//- width to the device width and prevents the page from being
//- zoomed in when going to landscape mode.
meta(name="viewport", content="width=device-width, initial-scale=1.0")
title Xen Orchestra
meta(name="description", content="Web interface for XenServer/XAPI Hosts")
meta(name="author", content="Vates SAS")
//- Place favicon.ico and apple-touch-icon.png in the root directory
link(rel="stylesheet", href="styles/main.css")
body(
ng-app = 'xoWebApp'
)
toaster-container
//- Navigation bar.
navbar
//- 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

@@ -0,0 +1,22 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import pkg from '../../../package'
// ===================================================================
export default angular.module('xoWebApp.about', [
uiRouter
])
.config(function ($stateProvider) {
$stateProvider.state('about', {
url: '/about',
controller: 'AboutCtrl',
template: require('./view')
})
})
.controller('AboutCtrl', function ($scope) {
$scope.pkg = pkg
})
// A module exports its name.
.name

View File

@@ -0,0 +1,50 @@
//- TODO: lots of stuff.
.grid-sm
.panel.panel-default
p.page-title About Xen Orchestra
p.text-center ({{pkg.name}} {{pkg.version}})
.grid-sm
//- Vates
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-lightbulb-o
| Vates
.panel-body
p.text-center
| We are the team behind Xen Orchestra, we are Vates! We create Open Source products and we offer commercial support for Xen and Xen Orchestra. Want to know more about us? Go to our website!
p.text-center
img(src="images/arrow.png")
br
p.text-center
a.btn.btn-success(href="https://vates.fr")
i.fa.fa-hand-o-right
| Our website
//- Open Source
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-thumbs-up
| Open Source
.panel-body
p.text-center
| This project is Open Source (AGPL), everyone is welcome aboard! You want a specific feature in XO? Report a bug? Go to our project website, read the FAQ and get involved in the project!
p.text-center
img(src="images/opensource.png")
br
p.text-center
a.btn.btn-info(href="https://xen-orchestra.com")
i.fa.fa-flask
| Project website
//- Pro support
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-truck
| Pro Support Delivered
.panel-body
p.text-center
| Our XO Appliance can be delivered with professional support: stay relaxed, we got your back! You can also have assitance for deploying or upgrade your virtualized infrastructure through our deep understanding of Xen.
p.text-center
img(src="images/support.png")
p.text-center
a.btn.btn-primary(href="https://vates.fr/services.html")
i.fa.fa-envelope
| Get services

View File

@@ -0,0 +1,93 @@
angular = require 'angular'
forEach = require('lodash.foreach')
includes = require('lodash.includes')
isoDevice = require('iso-device')
#=====================================================================
module.exports = angular.module 'xoWebApp.console', [
require 'angular-ui-router'
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, xoHideUnauthorizedFilter, modal) ->
{id} = $stateParams
{get} = xoApi
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
(VM) ->
$scope.consoleUrl = null
unless xoApi.user
$scope.VDIs = []
return
$scope.VM = VM
return unless (
VM? and
VM.power_state is 'Running' and
not includes(VM.current_operations, 'clean_reboot')
)
pool = get VM.$poolId
return unless pool
$scope.consoleUrl = "./api/consoles/#{id}"
host = get VM.$container # host because the VM is running.
)
$scope.startVM = xo.vm.start
$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
# A module exports its name.
.name

View File

@@ -0,0 +1,54 @@
.container: .panel.panel-default
//- Title
p.page-title
span.fa-stack
i.fa.fa-square-o.fa-stack-2x
i.xo-icon-console.fa-stack-1x(class = 'xo-color-{{VM.power_state | lowercase}}')
| &nbsp;
a(
class = 'xo-color-{{VM.power_state | lowercase}}'
ui-sref = 'VMs_view({id: VM.id})'
) {{VM.name_label}}
.list-group
//- Toolbar
.list-group-item: .row.text-center
.col-sm-6: iso-device(ng-if = 'VM && SRs', vm = 'VM', srs = 'SRs')
.col-sm-3: button.btn.btn-default(
ng-click = 'vncRemote.sendCtrlAltDel()'
)
i.fa.fa-keyboard-o
| &nbsp;
| Ctrl+Alt+Del
//- Action panel
.col-sm-3
.btn-group
button.btn.btn-default.inversed(
ng-if = "VM.power_state == ('Running' || 'Paused')"
tooltip = "Stop VM"
type = "button"
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.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.id)"
)
i.fa.fa-refresh.fa-fw
//- Console
.list-group-item
no-vnc(
url = '{{consoleUrl}}'
remote-control = 'vncRemote'
)

View File

@@ -0,0 +1,347 @@
'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 xoParallelD3 from 'xo-parallel-d3'
import xoSunburstD3 from 'xo-sunburst-d3'
import xoWeekHeatmap from'xo-week-heatmap'
import view from './view'
export default angular.module('dashboard.dataviz', [
uiRouter,
uiSelect,
xoApi,
xoParallelD3,
xoSunburstD3,
xoWeekHeatmap
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard.dataviz', {
controller: 'Dataviz as ctrl',
data: {
requireAdmin: true
},
url: '/dataviz',
template: view
})
})
.filter('type', () => {
return function (objects, type) {
if (!type) {
return objects
}
return filter(objects, object => object.type === type)
}
})
.controller('Dataviz', function ($scope, xoApi, xoAggregate, xo, $timeout) {
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()
xoApi.onUpdate(function () {
debouncedPopulate()
})
})
.controller('DatavizStorageHierarchical', function DatavizStorageHierarchical (xoApi, $scope, $timeout, $interval, $state, bytesToSizeFilter) {
$scope.charts = {
selected: {},
data: {
name: 'storage',
children: []
},
click: function (d) {
if (d.non_clickable) {
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',
non_clickable: true
}
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',
non_clickable: 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',
non_clickable: !pool.name_label
}
pool_shared_storage = {
name: 'Shared',
id: 'Shared' + pool_id,
children: [],
size: 0,
type: 'host',
non_clickable: 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()
xoApi.onUpdate(function () {
debouncedPopulate()
})
})
.controller('DatavizRamHierarchical', function DatavizRamHierarchical (xoApi, $scope, $timeout, $state, bytesToSizeFilter) {
$scope.charts = {
selected: {},
data: {
name: 'ram',
children: []
},
click: function (d) {
if (d.non_clickable) {
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',
non_clickable: !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',
non_clickable: 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()
xoApi.onUpdate(function () {
debouncedPopulate()
})
})
// A module exports its name.
.name

View File

@@ -0,0 +1,45 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-pie-chart
| Dataviz
.grid-sm
.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
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| VMs properties
.panel-body.text-center(
)
parallel-chart(
click="charts.click(d)"
chart-labels="charts.labels"
chart-data="charts.data"
)

View File

@@ -0,0 +1,327 @@
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 view from './view'
export default angular.module('dashboard.health', [
uiRouter,
xoApi,
xoHorizon,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('dashboard.health', {
controller: 'Health as bigController',
data: {
requireAdmin: true
},
url: '/health',
template: view
})
})
.filter('type', () => {
return function (objects, type) {
if (!type) {
return objects
}
return filter(objects, object => object.type === type)
}
})
.controller('Health', function () {})
.controller('HealthHeatmap', 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)
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, 2) // 2: week granularity (7 * 24 hours)
.then(result => ({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()
const averageCPU = averageMetrics['All CPUs'] && averageMetrics['All CPUs'].values || []
forEach(result.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.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.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.vifs, (values, metricKey) => {
metricKey = 'Network ' + Math.floor(metricKey / 2) + ' ' + (metricKey % 2 ? 'out' : 'in')
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.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.pifs, (values, metricKey) => {
metricKey = 'NIC ' + Math.floor(metricKey / 2) + ' ' + (metricKey % 2 ? 'out' : 'in')
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.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.xvds, (values, metricKey) => {
metricKey = 'Disk ' + String.fromCharCode(Math.floor(metricKey / 2) + 65) + ' ' + (metricKey % 2 ? 'write' : 'read')
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.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.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.load, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value,
date: +result.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.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.memoryUsed, (value, key) => {
if (mapValues[key] === undefined) { // first value
mapValues.push({
value: +value * (object.type === 'host' ? 1024 : 1),
date: +result.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('HealthHorizons', 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)
ctrl.typeFilter = type
}
this.prepareMetrics = function (objects) {
ctrl.chosen = objects
ctrl.selectedMetric = null
ctrl.loadingMetrics = true
xoAggregate
.refreshStats(ctrl.chosen, 2)
.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-heartbeat
| Health
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-fire
| Weekly Heatmap
.panel-body(ng-controller='HealthHeatmap 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:$select.search | orderBy:["type", "name_label"] track by object.id')
div
i(class = 'xo-icon-{{ object.type | lowercase }}')
| {{ object.name_label }}
span(ng-if='(object.type === "SR" || object.type === "VM") && object.$container')
| ({{ (object.$container | resolve).name_label }})
//- br
.btn-group(role = 'group')
button.btn.btn-default(ng-click = 'heatmap.selected = []', tooltip = 'Clear selection')
i.fa.fa-times
button.btn.btn-default(ng-click = 'heatmap.selectAll("VM")', tooltip = 'Choose all VMs')
i.xo-icon-vm
button.btn.btn-default(ng-click = 'heatmap.selectAll("host")', tooltip = 'Choose all hosts')
i.xo-icon-host
button.btn.btn-success(ng-click = 'heatmap.prepareMetrics(heatmap.selected)', tooltip = 'Load metrics')
i.fa.fa-check
| &nbsp;Select
.grid-cell.grid--gutters
.container-fluid
span(ng-if = 'heatmap.loadingMetrics')
| Loading metrics ...&nbsp;
i.fa.fa-circle-o-notch.fa-spin
.form-group(ng-if = 'heatmap.metrics')
ui-select(ng-model = 'heatmap.selectedMetric')
ui-select-match(placeholder = 'Choose a metric') {{ $select.selected.key }}
ui-select-choices(repeat = 'metric in heatmap.metrics | filter:$select.search | orderBy:["key"]') {{ metric.key }}
br
p.text-center(ng-if = 'heatmap.chosen.length')
span(ng-repeat = 'object in heatmap.chosen', ng-class = '{"text-danger": object._ignored}')
i(class = 'xo-icon-{{ object.type | lowercase }}')
| &nbsp;
span(ng-if = '!object._ignored') {{ object.name_label }}
del(ng-if = 'object._ignored') {{ object.name_label }}
| &nbsp;&ensp;
weekheatmap(ng-if = 'heatmap.selectedMetric', chart-data='heatmap.selectedMetric')
.grid-sm
.grid-cell
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-fire
| Weekly Charts
.panel-body(ng-controller="HealthHorizons 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:$select.search | orderBy:["type", "name_label"] track by object.id')
div
i(class = 'xo-icon-{{ object.type | lowercase }}')
| {{ object.name_label }}
span(ng-if='(object.type === "SR" || object.type === "VM") && object.$container')
| ({{ (object.$container | resolve).name_label }})
//- br
.btn-group(role = 'group')
button.btn.btn-default(ng-click = 'horizons.selected = []', tooltip = 'Clear selection')
i.fa.fa-times
button.btn.btn-default(ng-click = 'horizons.selectAll("VM")', tooltip = 'Choose all VMs')
i.xo-icon-vm
button.btn.btn-default(ng-click = 'horizons.selectAll("host")', tooltip = 'Choose all hosts')
i.xo-icon-host
button.btn.btn-success(ng-click = 'horizons.prepareMetrics(horizons.selected)', tooltip = 'Load metrics')
i.fa.fa-check
| &nbsp;Select
.grid-cell.grid--gutters
.container-fluid
span(ng-if = 'horizons.loadingMetrics')
| Loading metrics ...&nbsp;
i.fa.fa-circle-o-notch.fa-spin
.form-group(ng-if = 'horizons.metrics && !horizons.loadingMetrics')
ui-select(ng-model = 'horizons.selectedMetric',ng-change='horizons.prepareStat()')
ui-select-match(placeholder = 'Choose a metric') {{ $select.selected }}
ui-select-choices(repeat = 'metric in horizons.metrics | filter:$select.search | orderBy:["key"]') {{ metric }}
br
button.btn.btn-primary.pull-right(
tooltip="Desynchronize Scale",
ng-click="horizons.toggleSynchronizeScale()"
ng-if='horizons.synchronizescale && horizons.selectedMetric'
)
i.fa.fa-balance-scale
button.btn.btn-default.pull-right(
tooltip="Synchronize Scale",
ng-click="horizons.toggleSynchronizeScale()"
ng-if='!horizons.synchronizescale && horizons.selectedMetric'
)
i.fa.fa-balance-scale
br
p.text-center(ng-if = 'horizons.chosen.length')
span(ng-repeat = 'object in horizons.chosen', ng-class = '{"text-danger": object._ignored}')
i(class = 'xo-icon-{{ object.type | lowercase }}')
| &nbsp;
span(ng-if = '!object._ignored') {{ object.name_label }}
del(ng-if = 'object._ignored') {{ object.name_label }}
| &nbsp;&ensp;
div(
ng-repeat='(label,stat) in horizons.stats'
ng-if='!horizons.loadingMetrics'
style='position:relative'
)
horizon(
ng-if='$first'
chart-data='stat'
show-axis='true'
axis-orientation='top'
selected='horizons.selectedDate'
extent='horizons.extents'
label='{{label}}'
)
horizon(
ng-if='$middle'
chart-data='stat'
selected='horizons.selectedDate'
extent='horizons.extents'
label='{{label}}'
)
horizon(
ng-if='$last && !$first'
chart-data='stat'
show-axis='true'
axis-orientation='bottom'
selected='horizons.selectedDate'
extent='horizons.extents'
label='{{label}}'
)

View File

@@ -0,0 +1,41 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import dataviz from './dataviz'
import filter from 'lodash.filter'
import health from './health'
import overview from './overview'
import view from './view'
export default angular.module('dashboard', [
uiRouter,
dataviz,
health,
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,137 @@
'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) {
$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: []
})
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
pools = xoApi.getView('pools')
srsByContainer = xoApi.getIndex('srsByContainer')
vmsByContainer = xoApi.getIndex('vmsByContainer')
hostsByPool = xoApi.getIndex('hostsByPool')
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.$CPUs], [xoApi.stats.$vCPUs]]
}
const debouncedPopulate = debounce(populateChartsData, 300, {leading: true, trailing: true})
debouncedPopulate()
xoApi.onUpdate(function () {
debouncedPopulate()
})
})
.name

View File

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

View File

@@ -0,0 +1,16 @@
.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')
i.fa.fa-fw.fa-pie-chart.fa-menu
span.menu-entry Dataviz
li
a(ui-sref = '.health')
i.fa.fa-fw.fa-heartbeat.fa-menu
span.menu-entry Health
.side-content(ui-view = '')

View File

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

View File

@@ -0,0 +1,27 @@
form(ng-submit="delete()")
.modal-header
h3 VMs deletion
.modal-body
p
| You are going to delete the following VMs, this is a
strong dangerous action
| !
table.table
tr
th.col-sm-3 Name
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.id")
td {{VM.name_label}}
td {{VM.name_description}}
td
input(type="checkbox", ng-model="disks[VM.id]")
p
i.fa.fa-exclamation-triangle
| &nbsp;All snapshots will be deleted too
.modal-footer
button.btn.btn-primary(type="submit")
| Delete
button.btn.btn-warning(type="button", ng-click="$dismiss()")
| Cancel

View File

@@ -0,0 +1,39 @@
import angular from 'angular'
import uiBootstrap from 'angular-ui-bootstrap'
// ===================================================================
export default angular.module('xoWebApp.genericModal', [
uiBootstrap
])
.controller('GenericModalCtrl', function ($scope, $modalInstance, options) {
$scope.title = options.title
$scope.message = options.message
$scope.yesButtonLabel = options.yesButtonLabel || 'Ok'
$scope.noButtonLabel = options.noButtonLabel
})
.service('modal', function ($modal) {
return {
confirm: function (opts) {
const modal = $modal.open({
controller: 'GenericModalCtrl',
template: require('./view'),
resolve: {
options: function () {
return {
title: opts.title,
message: opts.message,
noButtonLabel: 'Cancel'
}
}
}
})
return modal.result
}
}
})
// A module exports its name.
.name

View File

@@ -0,0 +1,11 @@
.modal-header
h3
i.fa.fa-exclamation-triangle.text-danger
| {{title}}
.modal-body
| {{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}}

View File

@@ -0,0 +1,360 @@
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'
#=====================================================================
module.exports = angular.module 'xoWebApp.host', [
require 'angular-file-upload'
require 'angular-ui-router'
require 'tag'
]
.config ($stateProvider) ->
$stateProvider.state 'hosts_view',
url: '/hosts/:id'
controller: 'HostCtrl'
template: require './view'
.controller 'HostCtrl', (
$scope, $stateParams, $http
$upload
$window
$timeout
dateFilter
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.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?
pool = $scope.pool = xoApi.get host.$poolId
SRsToPBDs = $scope.SRsToPBDs = Object.create null
for PBD in host.$PBDs
PBD = xoApi.get PBD
# If this PBD is unknown, just skips it.
continue unless PBD
SRsToPBDs[PBD.SR] = PBD
$scope.listMissingPatches($scope.host.id)
if host.power_state is 'Running'
refreshStatControl.start()
else
refreshStatControl.stop()
)
$scope.$watch('vms', (vms) =>
$scope.vCPUs = sum(vms, (vm) => vm.CPUs.number)
)
$scope.cancelTask = (id) ->
modal.confirm({
title: 'Cancel task'
message: 'Are you sure you want to cancel this task?'
}).then ->
xo.task.cancel id
$scope.destroyTask = (id) ->
modal.confirm({
title: 'Destroy task'
message: 'Are you sure you want to destroy this task?'
}).then ->
xo.task.destroy id
$scope.disconnectPBD = xo.pbd.disconnect
$scope.removePBD = xo.pbd.delete
$scope.new_sr = xo.pool.new_sr
$scope.pool_addHost = (id) ->
xo.host.attach 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'
}).then ->
xo.host.detach 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.enableHost = (id) ->
xo.host.enable id
notify.info {
title: 'Host action'
message: 'Host is enabled'
}
$scope.disableHost = (id) ->
modal.confirm({
title: 'Disable host'
message: 'Are you sure you want to disable this host? In disabled state, no new VMs can be started and currently active VMs on the host continue to execute.'
}).then ->
xo.host.disable id
.then ->
notify.info {
title: 'Host action'
message: 'Host is disabled'
}
$scope.restartToolStack = (id) ->
modal.confirm({
title: 'Restart XAPI'
message: 'Are you sure you want to restart the XAPI toolstack?'
}).then ->
xo.host.restartToolStack id
$scope.shutdownHost = (id) ->
modal.confirm({
title: 'Shutdown host'
message: 'Are you sure you want to shutdown this host?'
}).then ->
xo.host.stop id
$scope.saveHost = ($data) ->
{host} = $scope
{name_label, name_description, enabled} = $data
$data = {
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
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
$scope.deleteAllLog = ->
modal.confirm({
title: 'Log deletion'
message: 'Are you sure you want to delete all the logs?'
}).then ->
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 = (id) ->
console.log "Connect PBD #{id}"
xoApi.call 'pbd.connect', {id: id}
$scope.disconnectPBD = (id) ->
console.log "Disconnect PBD #{id}"
xoApi.call 'pbd.disconnect', {id: id}
$scope.removePBD = (id) ->
console.log "Remove PBD #{id}"
xoApi.call 'pbd.delete', {id: id}
$scope.connectPIF = (id) ->
console.log "Connect PIF #{id}"
xoApi.call 'pif.connect', {id: id}
$scope.disconnectPIF = (id) ->
console.log "Disconnect PIF #{id}"
xoApi.call 'pif.disconnect', {id: id}
$scope.removePIF = (id) ->
console.log "Remove PIF #{id}"
xoApi.call 'pif.delete', {id: id}
$scope.importVm = ($files, id) ->
file = $files[0]
notify.info {
title: 'VM import started'
message: "Starting the VM import"
}
xo.vm.import id
.then ({ $sendTo: url }) ->
return $upload.http {
method: 'POST'
url
data: file
}
.then (result) ->
throw result.status if result.status isnt 200
notify.info
title: 'VM import'
message: 'Success'
$scope.createNetwork = (name, description, pif, mtu, vlan) ->
$scope.createNetworkWaiting = true # disables form fields
notify.info {
title: 'Network creation...'
message: 'Creating the network'
}
params = {
host: $scope.host.id
name,
}
if mtu then params.mtu = mtu
if pif then params.pif = pif
if vlan then params.vlan = vlan
if description then params.description = description
xoApi.call 'host.createNetwork', params
.then ->
$scope.creatingNetwork = false
$scope.createNetworkWaiting = false
$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.refreshStats = (id) ->
return xo.host.refreshStats id
.then (result) ->
result.cpuSeries = []
forEach result.cpus, (v,k) ->
result.cpuSeries.push 'CPU ' + k
return
result.pifSeries = []
forEach result.pifs, (v,k) ->
result.pifSeries.push '#' + Math.floor(k/2) + ' ' + if k % 2 then 'out' else 'in'
return
forEach result.date, (v,k) ->
result.date[k] = new Date(v*1000).toLocaleTimeString()
forEach result.memoryUsed, (v, k) ->
result.memoryUsed[k] = v*1024
forEach result.memory, (v, k) ->
result.memory[k] = v*1024
$scope.stats = result
$scope.statView = {
cpuOnly: false,
ramOnly: false,
netOnly: false,
loadOnly: false
}
# A module exports its name.
.name

517
app/modules/host/view.jade Normal file
View File

@@ -0,0 +1,517 @@
.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.id})") {{pool.name_label}}
| )
p.center {{host.bios_strings["system-manufacturer"]}} {{host.bios_strings["system-product-name"]}}
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| General
span.quick-edit(tooltip="Edit General settings", ng-click="hostSettings.$show()")
i.fa.fa-edit.fa-fw
.panel-body
form(editable-form="", name="hostSettings", onbeforesave="saveHost($data)")
dl.dl-horizontal
dt Name
dd
span(editable-text="host.name_label", e-name="name_label", e-form="hostSettings")
| {{host.name_label}}
dt Description
dd
span(editable-text="host.name_description", e-name="name_description", e-form="hostSettings")
| {{host.name_description}}
dt Enabled
dd
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
xo-tag(ng-if = 'host', object = 'host')
dt CPUs
dd {{host.CPUs["cpu_count"]}}x {{host.CPUs["modelname"]}}
dt Hostname
dd
| {{host.hostname}}
dt UUID
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()")
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(type="submit", ng-disabled="hostSettings.$waiting")
i.fa.fa-save
| Save
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-stats
| Stats
.panel-body(ng-if="refreshStatControl.running && stats")
div(ng-if="statView.cpuOnly", ng-click="statView.cpuOnly = false")
p.stat-name
i.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:
p.center.mid-stat {{vCPUs}}/{{host.CPUs['cpu_count']}}
.col-sm-4
p.stat-name RAM used:
p.center.mid-stat {{host.memory.usage | bytesToSize}}
.col-sm-4
p.stat-name Running VMs:
p.center.mid-stat {{vms | count}}
p.center(ng-if="refreshStatControl.running")
i.xo-icon-loading
| &nbsp; Fetching stats...
//- Action panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flash
| Actions
.panel-body.text-center
.grid-sm.grid--gutters
.grid.grid-cell
.grid-cell.btn-group
button.btn(tooltip="Add SR", type="button", style="width: 90%", xo-sref="SRs_new({container: host.id})")
i.xo-icon-sr.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Add VM", type="button", style="width: 90%", xo-sref="VMs_new({container: host.id})")
i.xo-icon-vm.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Reboot host", type="button", style="width: 90%", xo-click="rebootHost(host.id)")
i.fa.fa-refresh.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Shutdown host", type="button", style="width: 90%", xo-click="shutdownHost(host.id)")
i.fa.fa-power-off.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="host.enabled")
button.btn(tooltip="Disable host", type="button", style="width: 90%", xo-click="disableHost(host.id)")
i.fa.fa-times-circle.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="!host.enabled")
button.btn(tooltip="Enable host", type="button", style="width: 90%", xo-click="enableHost(host.id)")
i.fa.fa-check-circle.fa-2x.fa-fw
.grid.grid-cell
.grid-cell.btn-group
button.btn(tooltip="Restart toolstack", type="button", style="width: 90%", xo-click="restartToolStack(host.id)")
i.fa.fa-retweet.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="pool.name_label")
button.btn(tooltip="Remove from pool", style="width: 90%", type="button", xo-click="pool_removeHost(host.id)")
i.fa.fa-cloud-upload.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="!pool.name_label")
button.btn(tooltip="Add to pool", style="width: 90%", type="button", xo-click="pool_addHost(host.id)")
i.fa.fa-cloud-download.fa-2x.fa-fw
.grid-cell.btn-group(style="margin-bottom: 0.5em")
button.btn(
tooltip="Import VM"
type="button"
style="width: 90%"
ng-file-select = 'importVm($files, host.id)'
)
i.fa.fa-upload.fa-2x.fa-fw
.grid-cell.btn-group(style="margin-bottom: 0.5em")
button.btn(
tooltip="Host console"
type="button"
style="width: 90%"
xo-sref="consoles_view({id: controller.id})"
)
i.xo-icon-console.fa-2x.fa-fw
//- TODO: Memory panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| Memory
.panel-body.text-center
.progress
.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 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-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr
| Storage
.panel-body
table.table.table-hover
tr
th Name
th Format
th Size
th Physical/Allocated 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.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] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | percentage}}", tooltip="Allocated: {{[(SR.usage), SR.size] | percentage}}")
td
span.label.label-primary Shared
td(ng-if="SRsToPBDs[SR.id].attached")
span.label.label-success Connected
span.pull-right.btn-group.quick-buttons
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)")
i.fa.fa-unlink.fa-lg
td(ng-if="!SRsToPBDs[SR.id].attached")
span.label.label-default Disconnected
span.pull-right.btn-group.quick-buttons
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)")
i.fa.fa-link.fa-lg
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)")
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.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] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | percentage}}", tooltip="Allocated: {{[(SR.usage), SR.size] | percentage}}")
td
span.label.label-info Local
td(ng-if="SRsToPBDs[SR.id].attached")
span.label.label-success Connected
span.pull-right.btn-group.quick-buttons
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)")
i.fa.fa-unlink.fa-lg
td(ng-if="!SRsToPBDs[SR.id].attached")
span.label.label-default Disconnected
span.pull-right.btn-group.quick-buttons
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)")
i.fa.fa-link.fa-lg
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)")
i.fa.fa-ban.fa-lg
//- Networks/Interfaces panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-network
| Interfaces
.panel-body
table.table.table-hover
th.col-md-1 Device
th.col-md-1 VLAN
th.col-md-1 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.id")
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.oneliner {{PIF.IP}} ({{PIF.mode}})
td.oneliner {{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.id)")
i.fa.fa-unlink.fa-lg
td(ng-if="!PIF.attached")
span.label.label-default Disconnected
span.pull-right.btn-group.quick-buttons
a(tooltip="Connect this interface", xo-click="connectPIF(PIF.id)")
i.fa.fa-link.fa-lg
a(tooltip="Remove this interface", xo-click="removePIF(PIF.id)")
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")
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 }}')
.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
| &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
br
//- CPU and Logs panels
.grid-sm
//- Task panel
.panel.panel-default
.panel-heading.panel-title(ng-if="tasks | isNotEmpty")
i.fa.fa-spinner.fa-pulse
| Pending tasks
.panel-heading.panel-title(ng-if="tasks | isEmpty")
i.fa.fa-spinner
| Pending tasks
.panel-body
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 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.oneliner
| {{task.name_label}}
span.pull-right.btn-group.quick-buttons
a(xo-click="cancelTask(task.id)")
i.fa.fa-times.fa-lg(tooltip="Cancel this task")
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
| Logs
span.quick-edit(ng-if="host.messages | isNotEmpty", tooltip="Remove all logs", ng-click="deleteAllLog()")
i.fa.fa-trash-o.fa-fw
.panel-body
p.center(ng-if="host.messages | isEmpty") No recent logs
table.table.table-hover(ng-if="host.messages | isNotEmpty")
th Date
th Name
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.id)")
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
.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
| Patches
span.quick-edit(ng-click="listMissingPatches(host.id)", tooltip="Check for updates")
i.fa.fa-question-circle
.panel-body
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")
td.oneliner {{patch.name}}
td.oneliner
a(href="{{patch.documentationUrl}}", target="_blank") {{patch.description}}
td.oneliner {{patch.date | date:"medium"}}
td -
td
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to install the patch on this host")
span.label.label-danger Missing
tr(ng-repeat="patch in poolPatches | map | slice:(5*(currentPatchPage-1)):(5*currentPatchPage)")
td.oneliner {{patch.name_label}}
td.oneliner {{patch.name_description}}
//- TODO: use a proper function for patch date, like poolPatchToHostPatch
td.oneliner {{((patch.$host_patches[0]) | resolve).time*1e3 | date:"medium"}}
td {{patch.size | bytesToSize}}
td
span(ng-if="isPoolPatchApplied(patch)")
span.label.label-success Applied
span(ng-click="installPatch(host.id, patch.id)", ng-if="!isPoolPatchApplied(patch)", tooltip="Click to apply the patch on this host")
span.label.label-warning Not applied
//- span.pull-right.btn-group.quick-buttons
//- a(xo-click="deletePatch(patch.id)")
//- i.fa.fa-trash-o.fa-lg(tooltip="Remove this patch")
.center(ng-if = '(poolPatches | count) > 5 || 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
| 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' | 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
| 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' | 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=">>")

35
app/modules/list/index.js Normal file
View File

@@ -0,0 +1,35 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import xoTag from 'tag'
import xoApi from 'xo-api'
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.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')
})
// A module exports its name.
.name

152
app/modules/list/view.jade Normal file
View File

@@ -0,0 +1,152 @@
//- TODO: print a message when no entries.
//- If it's a (named) pool.
.grid.flat-object(ng-repeat="pool in list.pools.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by pool.id", ng-if="pool.name_label", xo-sref="pools_view({id: pool.id})")
//- Icon.
.grid-cell.flat-cell.flat-cell-type
i.xo-icon-pool
//- Properties & tags.
.grid-cell
//- Properties.
.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 &nbsp;
xo-tag(object = 'pool')
//- /Tags.
//- /Properties & tags.
//- /Pool.
//- If it's a host.
.grid.flat-object(ng-repeat="host in list.hosts.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by host.id", xo-sref="hosts_view({id: host.id})")
//- 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-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 &nbsp;
xo-tag(object = 'host')
//- /Tags.
//- /Properties & tags.
//- /Host.
//- If it's a VM.
.grid.flat-object(ng-repeat="VM in list.VMs.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by VM.id", xo-sref="VMs_view({id: VM.id})")
//- 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-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)")
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")
| (
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 &nbsp;
xo-tag(object = 'VM')
//- /Tags.
//- /Properties & tags.
//- /VM.
//- If it's a SR.
.grid.flat-object(ng-repeat="SR in list.SRs.all | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by SR.id", xo-sref="SRs_view({id: SR.id})")
//- Icon.
.grid-cell.flat-cell.flat-cell-type
i.xo-icon-sr
//- Properties & tags.
.grid-cell
//- Properties.
.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
| Usage: {{[SR.usage, SR.size] | percentage}} ({{SR.usage | bytesToSize}}/{{SR.size | bytesToSize}})
.grid-cell.flat-cell
| Type: {{SR.SR_type}}
.grid-cell.flat-cell(ng-init="container = (SR.$container | resolve)")
div(ng-if="'pool' === container.type")
strong
| Shared on:
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
div(ng-if="'host' === container.type")
| Connected to:
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 &nbsp;
xo-tag(object = 'SR')
//- /Tags.
//- /Properties & tags.
//- /SR.

View File

@@ -0,0 +1,51 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import updater from '../updater'
import xoServices from 'xo-services'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.navbar', [
uiRouter,
updater,
xoServices
])
.controller('NavbarCtrl', function ($state, xoApi, xo, $scope, updater) {
this.updater = updater
// TODO: It would make sense to inject xoApi in the scope.
Object.defineProperties(this, {
status: {
get: () => xoApi.status
},
user: {
get: () => xoApi.user
}
})
this.logIn = xoApi.logIn
this.logOut = function () {
xoApi.logOut()
}
// When a searched is entered, we must switch to the list view if
// necessary.
this.ensureListView = function () {
$state.go('list')
}
this.tasks = xoApi.getView('runningTasks')
})
.directive('navbar', function () {
return {
restrict: 'E',
controller: 'NavbarCtrl as navbar',
template: view,
scope: {}
}
})
// A module exports its name.
.name

View File

@@ -0,0 +1,140 @@
nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
//- Brand and toggle get grouped for better mobile display
.navbar-header
//- Button used to (un)collapse on mobile display.
button.navbar-toggle(type="button", ng-init="collapsed = true", ng-click="collapsed = !collapsed")
span.sr-only Toggle navigation
span.icon-bar
span.icon-bar
span.icon-bar
//- Brand name.
a.navbar-brand(ui-sref = 'index')
img.navbar-logo(src="images/logo.png")
| Xen Orchestra
//- All navbar items are collapsed on mobile display.
.collapse.navbar-collapse(ng-class="!collapsed && 'in'")
//- Search form of the navbar.
form.navbar-form.navbar-left(role="search", style="width: 250px")
//- Forced width due to issue with `input`s (https://github.com/twbs/bootstrap/issues/9950.
.input-group
input.form-control.inverse(
type = 'text'
placeholder = ''
ng-model = '$root.listFilter'
ng-change = 'navbar.ensureListView()'
)
span.input-group-btn
button.btn.btn-search(
type = 'button'
ng-click = 'navbar.ensureListView()'
)
i.fa.fa-search
//- /Search form.
ul.nav.navbar-nav
li
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
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")
i.xo-icon-error
| Disconnected from XO-Server
li.navbar-text(ng-if="'connecting' === navbar.status")
i.fa.fa-refresh.fa-spin
| Connecting to XO-Server
//- 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.size")
a.dropdown-toggle.inverse(dropdown-toggle)
i.xo-icon-task
ul.dropdown-menu.inverse
li.task-menu(
ng-repeat="task in navbar.tasks.all | orderBy:natural('name_label') track by task.id"
)
a(
ui-sref="hosts_view({id: task.$host})"
tooltip = "{{task.name_label}}"
)
//- i.fa.fa-spinner.fa-fw
//- | {{task.name_label}}
.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}}%"
)
| {{task.progress*100 | number:1}}%
//- Main menu.
li.dropdown(dropdown)
a.dropdown-toggle.inverse(dropdown-toggle)
i.fa.fa-th
ul.dropdown-menu.inverse
li(
ui-sref-active = 'active'
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
)
a(ui-sref = 'tree')
i.fa.fa-indent
| Tree view
li(ui-sref-active="active")
a(ui-sref="list")
i.fa.fa-align-justify
| Flat view
li(
ui-sref-active="active"
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
)
a(ui-sref="dashboard.index")
i.fa.fa-dashboard
| Dashboard
//- li.disabled(ui-sref-active="active")
//- a(ui-sref="graph")
//- i.fa.fa-sitemap
//- | Graphs view
li.divider
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
a(ui-sref = 'scheduler.index')
i.fa.fa-clock-o
| Scheduler
li.divider
li(
ui-sref-active = 'active'
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
)
a(ui-sref="settings.index")
i.fa.fa-cog
| Settings
li.divider
li(ui-sref-active="active")
a(ui-sref="about")
i.fa.fa-info-circle(style="color:#5bc0de")
| About
//- /Main menu.
li
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
span.hidden-sm {{navbar.user.email}}
li
a(ng-click = 'navbar.logOut()')
i.fa.fa-sign-out
| &nbsp;
| &nbsp;
//- /Right items.
//- /Navbar items.
//- /Navbar.

434
app/modules/new-sr/index.js Normal file
View File

@@ -0,0 +1,434 @@
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'
// ===================================================================
export default angular.module('xoWebApp.newSr', [
uiRouter
])
.config(function ($stateProvider) {
$stateProvider.state('SRs_new', {
url: '/srs/new/:container',
controller: 'NewSrCtrl as newSr',
template: view
})
})
.controller('NewSrCtrl', function ($scope, $state, $stateParams, xo, xoApi, notify, modal, bytesToSizeFilter) {
this.reset = function (data = {}) {
this.data = {}
delete this.lockCreation
this.lock = !(
(data.srType === 'Local') &&
(data.srPath && data.srPath.path)
)
}
this.resetLists = function () {
delete this.data.nfsList
delete this.data.scsiList
delete this.lockCreation
this.lock = true
this.resetErrors()
}
this.resetErrors = function () {
delete this.data.error
}
/*
* Loads NFS paths and iScsi iqn`s
*/
this.populateSettings = function (type, server, auth, user, password) {
this.reset()
this.loading = true
server = this._parseAddress(server)
if (type === 'NFS' || type === 'NFS_ISO') {
xoApi.call('sr.probeNfs', {
host: this.container.id,
server: server.host
})
.then(response => this.data.paths = response)
.catch(error => notify.warning({
title: 'NFS Detection',
message: error.message
}))
.finally(() => this.loading = false)
} else if (type === 'iSCSI') {
let params = {
host: this.container.id
}
if (auth) {
params.chapUser = user
params.chapPassword = password
}
params.target = server.host
if (server.port) {
params.port = server.port
}
xoApi.call('sr.probeIscsiIqns', params)
.then(response => {
if (response.length > 0) {
this.data.iqns = response
} else {
notify.warning({
title: 'iSCSI Detection',
message: 'No IQNs found'
})
}
})
.catch(error => notify.warning({
title: 'iSCSI Detection',
message: error.message
}))
.finally(() => this.loading = false)
} else {
this.loading = false
}
}
/*
* Loads iScsi LUNs
*/
this.populateIScsiIds = function (iqn, auth, user, password) {
delete this.data.iScsiIds
this.loading = true
let params = {
host: this.container.id,
target: iqn.ip,
targetIqn: iqn.iqn
}
if (auth) {
params.chapUser = user
params.chapPassword = password
}
xoApi.call('sr.probeIscsiLuns', params)
.then(response => {
forEach(response, item => {
item.display = 'LUN ' + item.id + ': ' +
item.serial + ' ' + bytesToSizeFilter(item.size) +
' (' + item.vendor + ')'
})
this.data.iScsiIds = response
})
.catch(error => notify.warning({
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 (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 params = {
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
server: server.host,
serverPath: data.srPath.path
}
return params
}
this._prepareScsiParams = function (data) {
let params = {
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
target: data.srIqn.ip,
targetIqn: data.srIqn.iqn,
scsiId: data.srIScsiId.scsiId
}
let server = this._parseAddress(data.srServer)
if (server.port) {
params.port = server.port
}
if (data.srAuth) {
params.chapUser = data.srChapUser
params.chapPassword = data.srChapPassword
}
return params
}
this.createSR = function (data) {
this.lock = true
this.creating = true
let operationToPromise
switch (data.srType) {
case 'NFS':
let nfsParams = this._prepareNfsParams(data)
operationToPromise = this._checkNfsExistence(nfsParams)
.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
case 'lvm':
let device = data.srDevice.device
operationToPromise = xoApi.call('sr.createLvm', {
host: this.container.id,
nameLabel: data.srName,
nameDescription: data.srDesc,
device
})
break
case 'NFS_ISO':
case 'Local':
let server = this._parseAddress(data.srServer || '')
let 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,
path
})
break
default:
operationToPromise = Bluebird.reject({message: 'Unhanled SR Type'})
break
}
operationToPromise
.then(id => {
$state.go('SRs_view', {id})
})
.catch(error => {
notify.error({
title: 'Storage Creation Error',
message: error.message
})
})
.finally(() => {
this.lock = false
this.creating = false
})
}
this._checkScsiExistence = function (params) {
this.resetLists()
return xoApi.call('sr.probeIscsiExists', params)
.then(response => {
if (response.length > 0) {
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?'
})
}
return true
})
}
this._checkNfsExistence = function (params) {
this.resetLists()
return xoApi.call('sr.probeNfsExists', params)
.then(response => {
if (response.length > 0) {
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?'
})
}
return true
})
}
const hostsByPool = xoApi.getIndex('hostsByPool')
const srsByContainer = xoApi.getIndex('srsByContainer')
this._gatherConnectedUuids = function () {
const srIds = []
// Shared SRs.
forEach(srsByContainer[this.container.$poolId], sr => {
srIds.push(sr.id)
})
// Local SRs.
forEach(hostsByPool[this.container.$poolId], host => {
forEach(srsByContainer[host.id], sr => {
srIds.push(sr.id)
})
})
return srIds
}
this._processSRList = function (list) {
let inUse = false
let SRs = this._gatherConnectedUuids()
forEach(list, item => {
inUse = (item.used = _indexOf(SRs, item.uuid) > -1) || inUse
})
this.lockCreation = inUse
return list
}
this.loadScsiList = function (data) {
this.resetLists()
this.loading = true
let params = this._prepareScsiParams(data)
xoApi.call('sr.probeIscsiExists', params)
.then(response => {
if (response.length > 0) {
this.data.scsiList = this._processSRList(response)
}
this.lock = !Boolean(data.srIScsiId)
})
.catch(error => {
notify.error({
title: 'iSCSI Error',
message: error.message
})
})
.finally(() => this.loading = false)
}
this.loadNfsList = function (data) {
this.resetLists()
let server = this._parseAddress(data.srServer)
xoApi.call('sr.probeNfsExists', {
host: this.container.id,
server: server.host,
serverPath: data.srPath.path
})
.then(response => {
if (response.length > 0) {
this.data.scsiList = this._processSRList(response)
}
this.lock = !Boolean(data.srPath.path)
})
.catch(error => {
notify.error({
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.reattachIScsi = function (uuid, {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' : '')
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
xoApi.call(method, {
host: this.container.id,
uuid,
nameLabel: name,
nameDescription: desc,
type
})
.then(id => {
$state.go('SRs_view', {id})
})
.catch(error => notify.error({
title: 'reattach',
message: error.message
}))
.finally(() => {
this.lock = false
this.attaching = false
})
}
}
this.reset()
$scope.$watch(() => xoApi.get($stateParams.container), container => {
this.container = container
})
})
// A module exports its name.
.name

View File

@@ -0,0 +1,183 @@
.grid
.panel.panel-default
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.id})")
| {{newSr.container.name_label}}
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
| General
.panel-body
.form-group
label.col-sm-3.control-label Type
.col-sm-9
select.form-control(ng-change = 'newSr.reset(formData)', ng-model = 'formData.srType', name = 'srType', ng-required = 'true')
option(value="") -- Choose a type of SR --
optgroup(label="VDI SR")
option(value="NFS") NFS
option(value="iSCSI") iSCSI
option(value="lvm") Local LVM
optgroup(label="ISO SR")
option(value="Local") Local
option(value="NFS_ISO") NFS ISO
.form-group(ng-class = '{"has-error": newSr.data.error.name}')
label.col-sm-3.control-label Name
.col-sm-9
input.form-control(type="text", placeholder="", name = 'srName', ng-model = 'formData.srName', ng-required = 'true')
.form-group(ng-class = '{"has-error": newSr.data.error.desc}')
label.col-sm-3.control-label Description
.col-sm-9
input.form-control(type="text", placeholder="SR Created by Xen Orchestra", name = 'srDesc', ng-model = 'formData.srDesc', ng-required = 'true')
//- Choose SR details
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| Settings
.panel-body
.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"')
| &nbsp;(auth&nbsp;
input(type = 'checkbox', ng-model = 'formData.srAuth')
| &nbsp;)
.col-sm-9
.input-group
input.form-control(type="text", placeholder='address{{ formData.srType === "iSCSI" ? "[:port]" : "" }}', name = 'srServer', ng-model = 'formData.srServer', required)
span.input-group-btn
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 Local LVM
.form-group(ng-if = 'formData.srType === "lvm"')
label.col-sm-3.control-label Device
.col-sm-9
input.form-control(
ng-if = 'formData.srType === "lvm"'
type = 'text'
name = 'srDevice'
ng-model = 'formData.srDevice.device'
placeholder = 'Device, e.g /dev/sda...'
ng-change = 'newSr.lock = !formData.srDevice.device'
required
)
.form-group(ng-if = 'newSr.data.paths || formData.srType === "Local"')
label.col-sm-3.control-label Path
.col-sm-9
//- For NFS
select.form-control(
ng-if = 'newSr.data.paths'
name = 'srPath'
ng-change = 'newSr.loadNfsList(formData)'
ng-model = 'formData.srPath'
ng-options = 'item.path for item in newSr.data.paths', required)
option(value = '', disabled) -- Choose path --
//- For Local
input.form-control(
ng-if = 'formData.srType === "Local"'
type = 'text'
name = 'srPath'
ng-model = 'formData.srPath.path'
ng-change = 'newSr.lock = !formData.srPath.path'
required
)
//- For iScsi
.form-group(ng-if = 'formData.srType === "iSCSI"')
.col-sm-9.col-sm-offset-3.form-inline(ng-if = 'formData.srAuth')
label.sr-only(for = 'chapUser') User
input#chapUser.form-control(type = 'text', ng-model = 'formData.srChapUser', placeholder = 'user', ng-required = 'formData.srAuth')
| &ensp;
label.sr-only(for = 'chapUser') Password
input#chapPassword.form-control(type = 'password', ng-model = 'formData.srChapPassword', placeholder = 'password', ng-required = 'formData.srAuth')
.form-group(ng-if = 'newSr.data.iqns')
label.col-sm-3.control-label IQN
.col-sm-9
select.form-control(ng-change = 'newSr.populateIScsiIds(formData.srIqn, formData.srAuth, formData.srChapUser, formData.srChapPassword)', name = 'srIqn', ng-model = 'formData.srIqn', ng-options = '(item.iqn + " (" + item.ip + ")") for item in newSr.data.iqns', required)
option(value = '', disabled) -- Choose IQN --
.form-group(ng-if = 'newSr.data.iScsiIds')
label.col-sm-3.control-label LUN
.col-sm-9
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.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
| NFS storage use
.panel-body
table.table.table-condensed
tr
th.text-center Storage UUID
th
tr(ng-repeat = 'nfsSr in newSr.data.nfsList')
td.text-center {{ nfsSr.uuid }}
td.text-center(ng_if = '!nfsSr.used')
button.btn.btn-sm.btn-primary(type = 'button', ng-class = '{disabled: newSr.lock}', ng-click = 'newSr.reattachNfs(nfsSr.uuid, {name: formData.srName, nameError: srForm.srName.$error.required}, {desc: formData.srDesc, descError: srForm.srDesc.$error.required}, "NFS_ISO" === formData.srType)') Reattach
td.text-center(ng_if = 'nfsSr.used', ng-class = '{disabled: newSr.lock}')
button.btn.btn-sm.btn-danger(ui-sref = 'SRs_view({id: nfsSr.uuid})', ng-class = '{disabled: newSr.lock}')
i.fa.fa-eye
| In use
p.text-center(ng-if = 'newSr.attaching')
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
| iSCSI storage use
.panel-body
table.table.table-condensed
tr
th.text-center Storage UUID
th
tr(ng-repeat = 'scsiSr in newSr.data.scsiList')
td.text-center {{ scsiSr.uuid }}
td.text-center(ng_if = '!scsiSr.used')
button.btn.btn-sm.btn-primary(type = 'button', ng-class = '{disabled: newSr.lock}', ng-click = 'newSr.reattachIScsi(scsiSr.uuid, {name: formData.srName, nameError: srForm.srName.$error.required}, {desc: formData.srDesc, descError: srForm.srDesc.$error.required})') Reattach
td.text-center(ng_if = 'scsiSr.used')
button.btn.btn-sm.btn-danger(ui-sref = 'SRs_view({id: scsiSr.uuid})', ng-class = '{disabled: newSr.lock}')
i.fa.fa-eye
| In use
p.text-center(ng-if = 'newSr.attaching')
i.xo-icon-loading
//- Summary
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flag-checkered
| Summary
.panel-body
.grid
.grid-cell
p.stat-name
| Name:
p.center.big {{formData.srName}}
.grid-cell
p.stat-name
| Type:
p.center.big {{formData.srType}}
.grid-cell
div(ng-if = 'formData.srType === "iSCSI"')
p.stat-name Size
p.center.big {{formData.srIScsiId.size | bytesToSize}}
div(ng-if = 'formData.srType === "NFS"')
p.stat-name Path
p.center.big {{formData.srPath.path}}
p.center
button.btn.btn-lg.btn-primary(type="submit", ng-disabled = 'newSr.lock || newSr.lockCreation')
i.fa.fa-play
| &nbsp;Create SR&nbsp;
i.xo-icon-loading-sm(ng-if = 'newSr.creating')

View File

@@ -1,13 +1,25 @@
'use strict'
angular = require 'angular'
cloneDeep = require 'lodash.clonedeep'
filter = require 'lodash.filter'
forEach = require 'lodash.foreach'
angular.module('xoWebApp')
.controller 'NewVmCtrl', (
#=====================================================================
module.exports = angular.module 'xoWebApp.newVm', [
require 'angular-ui-router'
]
.config ($stateProvider) ->
$stateProvider.state 'VMs_new',
url: '/vms/new/:container'
controller: 'NewVmsCtrl'
template: require './view'
.controller 'NewVmsCtrl', (
$scope, $stateParams, $state
xoApi, xo
bytesToSizeFilter, sizeToBytesFilter
notify
) ->
{get} = xo
{get} = xoApi
removeItems = do ->
splice = Array::splice.call.bind Array::splice
@@ -22,46 +34,79 @@ angular.module('xoWebApp')
result
pool = default_SR = null
host = 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]
}
})
updateSrs = () =>
srs = []
poolSrs and forEach(poolSrs, (sr) => srs.push(sr))
hostSrs and forEach(hostSrs, (sr) => srs.push(sr))
$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
updateSrs()
)
$scope.$watchCollection(
() => host and srsByContainer[host.id],
(srs) =>
hostSrs = srs
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(
-> xo.revision
->
container = $scope.container = get $stateParams.container
-> get $stateParams.container
(container) ->
$scope.container = container
# If the container was not found, no need to continue.
return unless container?
if container.type is 'host'
host = container
pool = (get container.poolRef) ? {}
pool = (get container.$poolId) ? {}
else
host = {}
pool = container
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 = ''
@@ -71,6 +116,7 @@ angular.module('xoWebApp')
$scope.template = ''
$scope.VDIs = []
$scope.VIFs = []
$scope.isDiskTemplate = false
$scope.addVIF = do ->
id = 0
@@ -115,7 +161,14 @@ angular.module('xoWebApp')
delete $scope.installation_method
VDIs = $scope.VDIs = angular.copy template.template_info.disks
VDIs = $scope.VDIs = cloneDeep template.template_info.disks
# if the template has no config disk
# nor it's Other install media (specific case)
# then do NOT display disk and network panel
if VDIs.length is 0 and template.name_label isnt 'Other install media'
$scope.isDiskTemplate = true
$scope.VIFs.length = 0
else $scope.isDiskTemplate = false
for VDI in VDIs
VDI.id = VDI_id++
VDI.size = bytesToSizeFilter VDI.size
@@ -124,6 +177,7 @@ angular.module('xoWebApp')
$scope.createVM = ->
{
CPUs
pv_args
installation_cdrom
installation_method
installation_network
@@ -134,9 +188,8 @@ angular.module('xoWebApp')
VDIs
VIFs
} = $scope
# Does not edit the displayed data directly.
VDIs = angular.copy VDIs
VDIs = cloneDeep VDIs
for VDI, index in VDIs
# Removes the dummy identifier used for AngularJS.
delete VDI.id
@@ -149,7 +202,7 @@ angular.module('xoWebApp')
# TODO: handles invalid values.
# 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
@@ -159,23 +212,27 @@ angular.module('xoWebApp')
VIF.MAC = VIF.MAC.trim()
delete VIF.MAC unless VIF.MAC
if installation_method is 'cdrom'
installation = {
method: 'cdrom'
repository: installation_cdrom
}
else
else if installation_network
matches = /^(http|ftp|nfs)/i.exec installation_network
# FIXME: handles invalid methods.
throw new Error 'invalid network URL' unless matches
installation = {
method: matches[1].toLowerCase()
repository: installation_network
}
else
installation = undefined
data = {
installation
pv_args
name_label
template: template.UUID
template: template.id
VDIs
VIFs
}
@@ -183,6 +240,11 @@ angular.module('xoWebApp')
# TODO:
# - disable the form during creation
# - indicate the progress of the operation
notify.info {
title: 'VM creation'
message: 'VM creation started'
}
xoApi.call('vm.create', data).then (id) ->
# If nothing to sets, just stops.
return id unless CPUs or name_description or memory
@@ -195,6 +257,9 @@ angular.module('xoWebApp')
if name_description
data.name_description = name_description
if pv_args
data.pv_args = pv_args
if memory
memory = sizeToBytesFilter memory
# FIXME: handles invalid entries.
@@ -210,3 +275,6 @@ angular.module('xoWebApp')
}
console.log error
# A module exports its name.
.name

View File

@@ -0,0 +1,224 @@
.grid
.panel.panel-default
p.page-title
i.xo-icon-vm
| Create VM on&nbsp;
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}}
//- Add server panel
form.form-horizontal(ng-submit="createVM()")
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-info-circle
| VM info
.panel-body
.form-group
label.col-sm-3.control-label Template
.col-sm-9
select.form-control(ng-model="template", ng-options="template.name_label for template in templates | orderBy:natural('name_label') track by template.id", required="")
.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")
.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
| 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 RAM
.col-sm-9
input.form-control(type="text", placeholder="{{template.memory.size | bytesToSize}}", ng-model="memory")
.grid(ng-if="isDiskTemplate")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-info-circle
| Template info
.panel-body
p.center This template will create automatically a VM with:
.col-md-6
ul(ng-repeat="VIF in template.VIFs | resolve | orderBy:natural('device') track by VIF.id")
li Interface \#{{VIF.device}} (MTU {{VIF.MTU}}) on {{(VIF.$network | resolve).name_label}}
.col-md-6
ul(ng-repeat = 'VBD in (template.$VBDs | resolve) track by VBD.id')
li Disk {{(VBD.VDI | resolve).name_label}} ({{(VBD.VDI | resolve).size | bytesToSize}}) on {{((VBD.VDI | resolve).$SR | resolve).name_label}}
.grid(ng-if="!isDiskTemplate")
//- Install panel
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-download
| Install settings
.panel-body
.form-group(ng-show="availableMethods.cdrom")
label.col-sm-3.control-label ISO/DVD
.col-sm-9
.input-group
span.input-group-addon
input(
type = 'radio'
name = 'installation_method'
ng-model = '$parent.installation_method'
value = 'cdrom'
required
)
select.form-control.disabled(
ng-disabled="'cdrom' !== installation_method"
ng-model="$parent.installation_cdrom"
required
)
option(value = '') Please select
optgroup(ng-repeat="SR in ISO_SRs | orderBy:natural('name_label') track by SR.id", ng-if="SR.VDIs.length", label="{{SR.name_label}}")
option(ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.id", ng-value="VDI.id")
| {{VDI.name_label}}
.form-group(ng-show="availableMethods.http || availableMethods.ftp || availableMethods.nfs")
label.col-sm-3.control-label Network
.col-sm-9
.input-group
span.input-group-addon
input(
type = 'radio'
name = 'installation_method'
ng-model = '$parent.installation_method'
value = 'network'
required
)
input.form-control(type="text", ng-disabled="'network' !== installation_method", placeholder="e.g: http://ftp.debian.org/debian", ng-model="$parent.installation_network")
.form-group(ng-show="template.PV_args")
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")
//- <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>
//- Interface panel
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-network
| Interfaces
.panel-body
table.table.table-hover
tr
th MAC
th Network
th.col-md-1 &#160;
//- 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")
td
select.form-control(
ng-options = 'network.id as network.name_label for network in networks | orderBy:natural("name_label") track by network.id'
ng-model = 'VIF.network'
required
)
option(value = '') Please select
td
.pull-right
button.btn.btn-default(type="button", ng-click="removeVIF($index)", title="Remove this interface")
i.fa.fa-times
.btn-form
p.center
.btn-form
p.center
button.btn.btn-success(type="button", ng-click="addVIF()")
i.fa.fa-plus
| Add interface
//- end of misc and interface panel
//- Disk panel
.grid(ng-if="!isDiskTemplate")
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-disk
| Disks
.panel-body
table.table.table-hover
tr
th.col-md-2 SR
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;
//- Buttons
tr(ng-repeat="VDI in VDIs track by VDI.id")
td
select.form-control(ng-model="VDI.SR", ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))")
td.text-center
input(type="checkbox", ng-model="VDI.bootable")
td
input.form-control(type="text", ng-model="VDI.size", required="")
td
input.form-control(type="text", placeholder="Name of this virtual disk", ng-model="VDI.name_label")
td
input.form-control(type="text", placeholder="Description of this virtual disk", ng-model="VDI.name_description")
td
.btn-group
button.btn.btn-default(type="button", ng-click="moveVDI($index, -1)", ng-disabled="$first", title="Move this disk up")
i.fa.fa-chevron-up
button.btn.btn-default(type="button", ng-click="moveVDI($index, 1)", ng-disabled="$last", title="Move this disk down")
i.fa.fa-chevron-down
.pull-right
button.btn.btn-default(type="button", ng-click="removeVDI($index)", title="Remove this disk")
i.fa.fa-times
.btn-form
p.center
.btn-form
p.center
button.btn.btn-success(type="button", ng-click="addVDI()")
i.fa.fa-plus
| Add disk
//- Confirmation panel
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flag-checkered
| Summary
.panel-body
.grid
.grid-cell
p.center.big {{name_label}}
| &nbsp;
span.small(ng-if="template.name_label") ({{template.name_label}})
.grid
.grid-cell
//- 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(tooltip="RAM")
| {{(memory) || (template.memory.size | bytesToSize)}}&nbsp;
i.xo-icon-memory
.grid-cell
//- p.stat-name Disks
p.center.big(tooltip="Disks")
| {{(VDIs.length) || (template.$VBDs.length) || 0}}x&nbsp;
i.xo-icon-disk
.grid-cell
//- p.stat-name Interfaces
p.center.big(tooltip="Network interfaces")
| {{(VIFs.length) || (template.VIFs.length) || 0}}x&nbsp;
i.xo-icon-network
p.center
button.btn.btn-lg.btn-primary(type="submit")
i.fa.fa-play
| Create VM

110
app/modules/pool/index.js Normal file
View File

@@ -0,0 +1,110 @@
import angular from 'angular'
import forEach from 'lodash.foreach'
import uiRouter from 'angular-ui-router'
import xoTag from 'tag'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.pool', [
uiRouter,
xoTag
])
.config(function ($stateProvider) {
$stateProvider.state('pools_view', {
url: '/pools/:id',
controller: 'PoolCtrl',
template: view
})
})
.controller('PoolCtrl', function ($scope, $stateParams, xoApi, xo, modal) {
{
const {id} = $stateParams
const hostsByPool = xoApi.getIndex('hostsByPool')
const runningHostsByPool = xoApi.getIndex('runningHostsByPool')
const srsByContainer = xoApi.getIndex('srsByContainer')
Object.defineProperties($scope, {
hosts: {
get: () => hostsByPool[id]
},
runningHosts: {
get: () => runningHostsByPool[id]
},
srs: {
get: () => srsByContainer[id]
}
})
}
$scope.$watch(() => xoApi.get($stateParams.id), function (pool) {
$scope.pool = pool
})
$scope.currentLogPage = 1
$scope.savePool = function ($data) {
let {pool} = $scope
let {name_label, name_description} = $data
$data = {
id: pool.id
}
if (name_label !== pool.name_label) {
$data.name_label = name_label
}
if (name_description !== pool.name_description) {
$data.name_description = name_description
}
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?'
}).then(function () {
// TODO: return all promises.
forEach($scope.pool.messages, function (message) {
xo.log.delete(message.id)
console.log('Remove log', message.id)
})
})
}
$scope.deleteLog = function (id) {
console.log('Remove log', id)
return xo.log.delete(id)
}
// $scope.patchPool = ($files, id) ->
// file = $files[0]
// xo.pool.patch id
// .then ({ $sendTo: url }) ->
// return $upload.http {
// method: 'POST'
// url
// data: file
// }
// .progress throttle(
// (event) ->
// percentage = (100 * event.loaded / event.total)|0
// notify.info
// title: 'Upload patch'
// message: "#{percentage}%"
// 6e3
// )
// .then (result) ->
// throw result.status if result.status isnt 200
// notify.info
// title: 'Upload patch'
// message: 'Success'
})
// A module exports its name.
.name

151
app/modules/pool/view.jade Normal file
View File

@@ -0,0 +1,151 @@
//- TODO: lots of stuff.
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-pool
| {{pool.name_label}}
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| General
span.quick-edit(tooltip="Edit General settings", ng-click="poolSettings.$show()")
i.fa.fa-edit.fa-fw
.panel-body
form(editable-form="", name="poolSettings", onbeforesave="savePool($data)")
dl.dl-horizontal
dt Name
dd
span(editable-text="pool.name_label", e-name="name_label", e-form="poolSettings")
| {{pool.name_label}}
dt Description
dd
span(editable-text="pool.name_description", e-name="name_description", e-form="poolSettings")
| {{pool.name_description}}
dt Master
dd(ng-repeat="master in [pool.master] | resolve")
a(ui-sref="hosts_view({id: master.id})")
| {{master.name_label}}
dt Tags
dd
xo-tag(ng-if = 'pool', object = 'pool')
dt(ng-if="pool.default_SR") Default SR
dd(ng-if="pool.default_SR", ng-init="default_SR = (pool.default_SR | resolve)")
a(ui-sref="SRs_view({id: default_SR.id})") {{default_SR.name_label}}
dt HA
dd
| {{pool.HA_enabled}}
dt UUID
dd {{pool.UUID}}
.btn-form(ng-show="poolSettings.$visible")
p.center
button.btn.btn-default(type="button", ng-disabled="poolSettings.$waiting", ng-click="poolSettings.$cancel()")
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(type="submit", ng-disabled="poolSettings.$waiting")
i.fa.fa-save
| Save
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-stats
| Stats
.row
.col-xs-6
p.stat-name Hosts:
p.center.big-stat {{hosts | count}}
.col-xs-6
p.stat-name Running:
p.center.big-stat {{runningHosts | count}}
//- Action panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flash
| Actions
.panel-body
.grid-cell.text-center
.grid
.grid-cell.btn-group
button.btn(tooltip="Add SR", type="button", style="width: 90%", disabled)
i.xo-icon-sr.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Add VM", type="button", style="width: 90%", xo-sref="VMs_new({container: pool.id})")
i.xo-icon-vm.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Patch the pool", type="button", style="width: 90%", ng-file-select = "patchPool($files, pool.id)")
i.fa.fa-file-code-o.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Add Host", 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")
i.fa.fa-unlink.fa-2x.fa-fw
//- Hosts panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
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.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] | percentage}}")
//- Shared SR panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr
| Shared SR
.panel-body
table.table.table-hover
th Name
th Description
th Type
th Size
th.col-md-4 Physical/Allocated usage
tr(
ng-repeat="SR in srs | map | orderBy:natural('name_label') track by SR.id"
xo-sref="SRs_view({id: SR.id})"
)
td.oneliner {{SR.name_label}}
td.oneliner {{SR.name_description}}
td {{SR.SR_type}}
td {{SR.size | bytesToSize}}
td
.progress-condensed
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | percentage}}", tooltip="Allocated: {{[(SR.usage), SR.size] | percentage}}")
//- Logs panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-comments
| Logs
span.quick-edit(ng-if="pool.messages | isNotEmpty", tooltip="Remove all logs", xo-click="deleteAllLog()")
i.fa.fa-trash-o.fa-fw
.panel-body
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 | 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.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=">>")

View File

@@ -0,0 +1,257 @@
import angular from 'angular'
import filter from 'lodash.filter'
import find from 'lodash.find'
import forEach from 'lodash.foreach'
import map from 'lodash.map'
import prettyCron from 'prettycron'
import size from 'lodash.size'
import trim from 'lodash.trim'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import view from './view'
// ====================================================================
export default angular.module('scheduler.backup', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('scheduler.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.comesForEditing = $stateParams.id
this.scheduleApi = {}
this.formData = {}
const refreshRemotes = () => {
const selectRemoteId = this.formData.remote && this.formData.remote.id
return xo.remote.getAll()
.then(remotes => {
const r = {}
forEach(remotes, remote => r[remote.id] = remote)
this.remotes = r
if (selectRemoteId) {
this.formData.remote = this.remotes[selectRemoteId]
}
})
}
const refreshSchedules = () => {
return xo.schedule.getAll()
.then(schedules => {
const s = {}
forEach(schedules, schedule => {
this.jobs && this.jobs[schedule.job] && this.jobs[schedule.job].key === JOBKEY && (s[schedule.id] = schedule)
})
this.schedules = s
})
}
const refreshJobs = () => {
return xo.job.getAll()
.then(jobs => {
const j = {}
forEach(jobs, job => j[job.id] = job)
this.jobs = j
})
}
const refresh = () => {
return refreshRemotes().then(refreshJobs).then(refreshSchedules)
}
this.getReady = () => {
return refresh()
.then(() => this.ready = true)
}
this.getReady()
const interval = $interval(() => {
refresh()
}, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
const toggleState = (toggle, state) => {
const selectedVms = this.formData.selectedVms.slice()
if (toggle) {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
forEach(vms, vm => {
if (vm.power_state === state) {
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
}
})
this.formData.selectedVms = selectedVms
} else {
const keptVms = []
for (let index in this.formData.selectedVms) {
if (this.formData.selectedVms[index].power_state !== state) {
keptVms.push(this.formData.selectedVms[index])
}
}
this.formData.selectedVms = keptVms
}
}
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
this.edit = schedule => {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
const job = this.jobs[schedule.job]
const selectedVms = []
forEach(job.paramsVector.items[0].values, value => {
const vm = find(vms, vm => vm.id === value.id)
vm && selectedVms.push(vm)
})
const tag = job.paramsVector.items[0].values[0].tag
const depth = job.paramsVector.items[0].values[0].depth
const cronPattern = schedule.cron
const remoteId = job.paramsVector.items[0].values[0].remoteId
const onlyMetadata = job.paramsVector.items[0].values[0].onlyMetadata || false
this.resetData()
this.formData.selectedVms = selectedVms
this.formData.tag = tag
this.formData.depth = depth
this.formData.scheduleId = schedule.id
this.formData.remote = this.remotes[remoteId]
this.formData.onlyMetadata = onlyMetadata
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, remoteId, tag, depth, cron, enabled, onlyMetadata) => {
if (!vms.length) {
notify.warning({
title: 'No Vms selected',
message: 'Choose VMs to back up'
})
return
}
const _save = (id === undefined) ? saveNew(vms, remoteId, tag, depth, cron, enabled, onlyMetadata) : save(id, vms, remoteId, tag, depth, cron, onlyMetadata)
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) => {
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
})
})
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) => {
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
remoteId,
tag,
depth,
onlyMetadata
})
})
const job = {
type: 'call',
key: JOBKEY,
method: 'vm.rollingBackup',
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
xo.schedule.delete(schedule.id)
.then(() => xo.job.delete(jobId))
.finally(() => {
refresh()
})
}
this.sanitizePath = (...paths) => (paths[0] && paths[0].charAt(0) === '/' && '/' || '') + filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/')
this.resetData = () => {
this.formData.allRunning = false
this.formData.allHalted = false
this.formData.selectedVms = []
this.formData.scheduleId = undefined
this.formData.tag = undefined
this.formData.path = undefined
this.formData.depth = undefined
this.formData.enabled = false
this.formData.remote = undefined
this.formData.onlyMetadata = false
this.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,137 @@
.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)')
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.xo-icon-vm
| VMs to backup
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
.container-fluid(ng-if = 'ctrl.formData')
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Backup
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Backup ID {{ ctrl.formData.scheduleId }}
.form-group
label.control-label.col-md-2(for = 'tag') Tag
.col-md-10
input#tag.form-control(form = 'backupform', ng-model = 'ctrl.formData.tag', placeholder = 'Back-up tag', required)
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
label.control-label.col-md-2(for = 'vmlist') VMs
.col-md-8
ui-select(form = 'backupform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose VMs to backup')
i.xo-icon-working(ng-if="isVMWorking($item)")
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
| {{$item.name_label}}
span(ng-if="$item.$container")
| ({{($item.$container | resolve).name_label}})
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
div
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
| {{vm.name_label}}
span(ng-if="vm.$container")
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
.col-md-2
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
span.fa-stack
i.xo-icon-running.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
input.hidden(form = 'backupform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
span.fa-stack
i.xo-icon-halted.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
.form-group
label.control-label.col-md-2(for = 'depth') Depth
.col-md-10
input#depth.form-control(form = 'backupform', ng-model = 'ctrl.formData.depth', placeholder = 'How many backups to rollover', type = 'number', min = '1', required)
.form-group
label.control-label.col-md-2(for = 'remote') Remote
.col-md-10
select#remote.form-control(form = 'backupform', ng-options = 'remote.name group by remote.type for remote in ctrl.remotes', ng-model = 'ctrl.formData.remote' required)
option(value = ''): em -- Choose a file system remote point --
.form-group
.col-md-10.col-md-offset-2
a.btn.btn-info(ui-sref = 'scheduler.remote')
i.fa.fa-pencil
| &nbsp; Manage your remote points
.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(ng-if = '!ctrl.formData.scheduleId')
label.control-label.col-md-2(for = 'enabled')
input#enabled(form = 'backupform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
.help-block.col-md-10 Enable immediatly after creation
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-clock-o
| Schedule
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
.grid-sm
.panel.panel-default
.panel-body
p.center
button.btn.btn-lg.btn-primary(form = 'backupform', type = 'submit', ng-disabled = '!ctrl.ready')
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()', ng-disabled = '!ctrl.ready')
| &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 VMs to backup
th Remote
th Depth
th Scheduling
th Only MetaData
th Enabled now
th
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
td {{ schedule.id }}
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
td
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
li(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
strong: a(ui-sref = 'scheduler.remote') {{ ctrl.remotes[ctrl.jobs[schedule.job].paramsVector.items[0].values[0].remoteId].name }}
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
td {{ ctrl.prettyCron(schedule.cron) }}
td.text-center
i.fa.fa-check(ng-if = 'ctrl.jobs[schedule.job].paramsVector.items[0].values[0].onlyMetadata')
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-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o

View File

@@ -0,0 +1,347 @@
import angular from 'angular'
import assign from 'lodash.assign'
import forEach from 'lodash.foreach'
import indexOf from 'lodash.indexof'
import later from 'later'
import moment from 'moment'
import prettyCron from 'prettycron'
import remove from 'lodash.remove'
import uiRouter from 'angular-ui-router'
later.date.localTime()
import backup from './backup'
import management from './management'
import mount from './remote'
import rollingSnapshot from './rolling-snapshot'
import view from './view'
import scheduler from './scheduler'
export default angular.module('scheduler', [
uiRouter,
backup,
management,
mount,
rollingSnapshot
])
.config(function ($stateProvider) {
$stateProvider.state('scheduler', {
abstract: true,
data: {
requireAdmin: true
},
template: view,
url: '/scheduler'
})
// Redirect to default sub-state.
$stateProvider.state('scheduler.index', {
url: '',
controller: function ($state) {
$state.go('scheduler.management')
}
})
})
.directive('xoScheduler', function () {
return {
restrict: 'E',
template: scheduler,
controller: 'XoScheduler as ctrl',
bindToController: true,
scope: {
data: '=',
api: '='
}
}
})
.controller('XoScheduler', function () {
this.init = () => {
let i, j
const minutes = []
for (i = 0; i < 6; i++) {
minutes[i] = []
for (j = 0; j < 10; j++) {
minutes[i].push(10 * i + j)
}
}
this.minutes = minutes
const hours = []
for (i = 0; i < 3; i++) {
hours[i] = []
for (j = 0; j < 8; j++) {
hours[i].push(8 * i + j)
}
}
this.hours = hours
const days = []
for (i = 0; i < 4; i++) {
days[i] = []
for (j = 1; j < 8; j++) {
days[i].push(7 * i + j)
}
}
days.push([29, 30, 31])
this.days = days
this.months = [
[
{v: 1, l: 'Jan'},
{v: 2, l: 'Feb'},
{v: 3, l: 'Mar'},
{v: 4, l: 'Apr'},
{v: 5, l: 'May'},
{v: 6, l: 'Jun'}
],
[
{v: 7, l: 'Jul'},
{v: 8, l: 'Aug'},
{v: 9, l: 'Sep'},
{v: 10, l: 'Oct'},
{v: 11, l: 'Nov'},
{v: 12, l: 'Dec'}
]
]
this.dayWeeks = [
{v: 0, l: 'Sun'},
{v: 1, l: 'Mon'},
{v: 2, l: 'Tue'},
{v: 3, l: 'Wed'},
{v: 4, l: 'Thu'},
{v: 5, l: 'Fri'},
{v: 6, l: 'Sat'}
]
this.resetData()
}
this.selectMinute = function (minute) {
if (this.isSelectedMinute(minute)) {
remove(this.data.minSelect, v => String(v) === String(minute))
} else {
this.data.minSelect.push(minute)
}
}
this.isSelectedMinute = function (minute) {
return indexOf(this.data.minSelect, minute) > -1 || indexOf(this.data.minSelect, String(minute)) > -1
}
this.selectHour = function (hour) {
if (this.isSelectedHour(hour)) {
remove(this.data.hourSelect, v => String(v) === String(hour))
} else {
this.data.hourSelect.push(hour)
}
}
this.isSelectedHour = function (hour) {
return indexOf(this.data.hourSelect, hour) > -1 || indexOf(this.data.hourSelect, String(hour)) > -1
}
this.selectDay = function (day) {
if (this.isSelectedDay(day)) {
remove(this.data.daySelect, v => String(v) === String(day))
} else {
this.data.daySelect.push(day)
}
}
this.isSelectedDay = function (day) {
return indexOf(this.data.daySelect, day) > -1 || indexOf(this.data.daySelect, String(day)) > -1
}
this.selectMonth = function (month) {
if (this.isSelectedMonth(month)) {
remove(this.data.monthSelect, v => String(v) === String(month))
} else {
this.data.monthSelect.push(month)
}
}
this.isSelectedMonth = function (month) {
return indexOf(this.data.monthSelect, month) > -1 || indexOf(this.data.monthSelect, String(month)) > -1
}
this.selectDayWeek = function (dayWeek) {
if (this.isSelectedDayWeek(dayWeek)) {
remove(this.data.dayWeekSelect, v => String(v) === String(dayWeek))
} else {
this.data.dayWeekSelect.push(dayWeek)
}
}
this.isSelectedDayWeek = function (dayWeek) {
return indexOf(this.data.dayWeekSelect, dayWeek) > -1 || indexOf(this.data.dayWeekSelect, String(dayWeek)) > -1
}
this.noMinutePlan = function (set = false) {
if (!set) {
// The last part (after &&) of this expression is reliable because we maintain the minSelect array with lodash.remove
return this.data.min === 'select' && this.data.minSelect.length === 1 && String(this.data.minSelect[0]) === '0'
} else {
this.data.minSelect = [0]
this.data.min = 'select'
return true
}
}
this.noHourPlan = function (set = false) {
if (!set) {
// The last part (after &&) of this expression is reliable because we maintain the hourSelect array with lodash.remove
return this.data.hour === 'select' && this.data.hourSelect.length === 1 && String(this.data.hourSelect[0]) === '0'
} else {
this.data.hourSelect = [0]
this.data.hour = 'select'
return true
}
}
this.resetData = () => {
this.data.minRange = 5
this.data.hourRange = 2
this.data.minSelect = [0]
this.data.hourSelect = []
this.data.daySelect = []
this.data.monthSelect = []
this.data.dayWeekSelect = []
this.data.min = 'select'
this.data.hour = 'all'
this.data.day = 'all'
this.data.month = 'all'
this.data.dayWeek = 'all'
this.data.cronPattern = '* * * * *'
this.data.summary = []
this.data.previewLimit = 0
this.update()
}
this.update = () => {
const d = this.data
const i = (d.min === 'all' && '*') ||
(d.min === 'range' && ('*/' + d.minRange)) ||
(d.min === 'select' && d.minSelect.join(',')) ||
'*'
const h = (d.hour === 'all' && '*') ||
(d.hour === 'range' && ('*/' + d.hourRange)) ||
(d.hour === 'select' && d.hourSelect.join(',')) ||
'*'
const dm = (d.day === 'all' && '*') ||
(d.day === 'select' && d.daySelect.join(',')) ||
'*'
const m = (d.month === 'all' && '*') ||
(d.month === 'select' && d.monthSelect.join(',')) ||
'*'
const dw = (d.dayWeek === 'all' && '*') ||
(d.dayWeek === 'select' && d.dayWeekSelect.join(',')) ||
'*'
this.data.cronPattern = i + ' ' + h + ' ' + dm + ' ' + m + ' ' + dw
const tabState = {
min: {
all: d.min === 'all',
range: d.min === 'range',
select: d.min === 'select'
},
hour: {
all: d.hour === 'all',
range: d.hour === 'range',
select: d.hour === 'select'
},
day: {
all: d.day === 'all',
range: d.day === 'range',
select: d.day === 'select'
},
month: {
all: d.month === 'all',
select: d.month === 'select'
},
dayWeek: {
all: d.dayWeek === 'all',
select: d.dayWeek === 'select'
}
}
this.tabs = tabState
this.summarize()
}
this.summarize = () => {
const schedule = later.parse.cron(this.data.cronPattern)
const occurences = later.schedule(schedule).next(25)
this.data.summary = []
forEach(occurences, occurence => {
this.data.summary.push(moment(occurence).format('LLLL'))
})
}
const cronToData = (data, cron) => {
const d = Object.create(null)
const cronItems = cron.split(' ')
if (cronItems[0] === '*') {
d.min = 'all'
} else if (cronItems[0].indexOf('/') !== -1) {
d.min = 'range'
const [, range] = cronItems[0].split('/')
d.minRange = range
} else {
d.min = 'select'
d.minSelect = cronItems[0].split(',')
}
if (cronItems[1] === '*') {
d.hour = 'all'
} else if (cronItems[1].indexOf('/') !== -1) {
d.hour = 'range'
const [, range] = cronItems[1].split('/')
d.hourRange = range
} else {
d.hour = 'select'
d.hourSelect = cronItems[1].split(',')
}
if (cronItems[2] === '*') {
d.day = 'all'
} else {
d.day = 'select'
d.daySelect = cronItems[2].split(',')
}
if (cronItems[3] === '*') {
d.month = 'all'
} else {
d.month = 'select'
d.monthSelect = cronItems[3].split(',')
}
if (cronItems[4] === '*') {
d.dayWeek = 'all'
} else {
d.dayWeek = 'select'
d.dayWeekSelect = cronItems[4].split(',')
}
assign(data, d)
}
this.prettyCron = prettyCron.toString.bind(prettyCron)
this.api.setCron = (cron => {
cronToData(this.data, cron)
this.update()
}).bind(this)
this.api.resetData = this.resetData.bind(this)
this.init()
})
.name

View File

@@ -0,0 +1,80 @@
import angular from 'angular'
import forEach from 'lodash.foreach'
import prettyCron from 'prettycron'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import view from './view'
// ====================================================================
export default angular.module('scheduler.management', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('scheduler.management', {
url: '/management',
controller: 'ManagementCtrl as ctrl',
template: view
})
})
.controller('ManagementCtrl', function ($scope, $state, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
const mapJobKeyToState = {
'rollingSnapshot': 'rollingsnapshot',
'rollingBackup': 'backup'
}
const refreshSchedules = () => {
xo.schedule.getAll()
.then(schedules => this.schedules = schedules)
xo.scheduler.getScheduleTable()
.then(table => this.scheduleTable = table)
}
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 = () => {
return refreshJobs().then(refreshSchedules)
}
refresh()
const interval = $interval(() => {
refresh()
}, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
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.resolveJobKey = schedule => {
return mapJobKeyToState[this.jobs[schedule.job].key]
}
this.collectionLength = col => Object.keys(col).length
this.working = {}
})
// A module exports its name.
.name

View File

@@ -0,0 +1,34 @@
.panel.panel-default
p.page-title
i.fa.fa-clock-o(style="color: #e25440;")
| Scheduler
.panel.panel-default
.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
tr
td.text-center(ng-if = '!ctrl.schedules'): i.xo-icon-loading
table.table
tr
td.text-center(ng-if = 'ctrl.schedules && !ctrl.collectionLength(ctrl.schedules)', style = 'width: 100%;') No scheduled jobs
table.table.table-hover(ng-if = 'ctrl.schedules && ctrl.collectionLength(ctrl.schedules)')
tr
th ID
th Job
th Scheduling
th State
th
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
td: a(ui-sref = 'scheduler.{{ctrl.resolveJobKey(schedule)}}({id: schedule.id})') {{ schedule.id }}
td {{ ctrl.jobs[schedule.job].key }}
td {{ ctrl.prettyCron(schedule.cron) }}
td
span.text-success(ng-if = 'ctrl.scheduleTable[schedule.id] === true')
| enabled&nbsp;
i.fa.fa-cogs
span.text-muted(ng-if = 'ctrl.scheduleTable[schedule.id] === false') disabled
span.text-warning(ng-if = 'ctrl.scheduleTable[schedule.id] === undefined') ?
td.text-right
fieldset(ng-disabled = 'ctrl.working[schedule.id]')
button.btn.btn-success(ng-if = 'ctrl.scheduleTable[schedule.id] === false', type = 'button', ng-click = 'ctrl.enable(schedule.id)') Enable
button.btn.btn-warning(ng-if = 'ctrl.scheduleTable[schedule.id] === true', type = 'button', ng-click = 'ctrl.disable(schedule.id)') Disable

View File

@@ -0,0 +1,67 @@
import angular from 'angular'
import filter from 'lodash.filter'
import map from 'lodash.map'
import trim from 'lodash.trim'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import size from 'lodash.size'
import view from './view'
// ====================================================================
export default angular.module('scheduler.remote', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('scheduler.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 = remotes)
}
this.getReady = () => {
return refresh()
.then(() => this.ready = true)
}
this.getReady()
const interval = $interval(refresh, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.sanitizePath = (...paths) => filter(map(paths, s => s && filter(map(s.split('/'), trim)).join('/'))).join('/')
this.prepareUrl = (type, host, path) => {
let url = type + ':/'
if (type === 'nfs') {
url += '/' + host + ':'
}
url += '/' + this.sanitizePath(path)
return url
}
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 => { console.log('GO !!!');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,86 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-puzzle-piece(style="color: #e25440;")
| File system remotes for backups&nbsp;
a.btn.btn-default(ui-sref = 'scheduler.backup')
i.fa.fa-level-up
.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 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
span(ng-if = 'remote.enabled')
span.text-success
| Accessible&nbsp;
i.fa.fa-puzzle-piece
//- 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 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
span(ng-if = 'remote.enabled')
span.text-success
| Mounted&nbsp;
i.fa.fa-puzzle-piece
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
form(ng-submit = 'ctrl.add(ctrl.name, ctrl.prepareUrl(ctrl.remoteType, ctrl.host, ctrl.path))')
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
| &nbsp;
.form-group
label.sr-only Name
input.form-control(type = 'text', ng-model = 'ctrl.name', placeholder = 'Name', required)
| &nbsp;
.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
span.input-group-addon /
label.sr-only Path&nbsp;
input.form-control(type = 'text', ng-model = 'ctrl.path', placeholder = 'path', required)
| &nbsp;
.form-group
button.btn.btn-primary.pull-right(type = 'submit', ng-disabled = '!ctrl.ready')
| Save&nbsp;
i.fa.fa-floppy-o

View File

@@ -0,0 +1,223 @@
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('scheduler.rollingSnapshot', [
uiRouter,
uiBootstrap
])
.config(function ($stateProvider) {
$stateProvider.state('scheduler.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.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)
}
const interval = $interval(() => {
refresh()
}, 5e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
const toggleState = (toggle, state) => {
const selectedVms = this.formData.selectedVms.slice()
if (toggle) {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
forEach(vms, vm => {
if (vm.power_state === state) {
(selectedVms.indexOf(vm) === -1) && selectedVms.push(vm)
}
})
this.formData.selectedVms = selectedVms
} else {
const keptVms = []
for (let index in this.formData.selectedVms) {
if (this.formData.selectedVms[index].power_state !== state) {
keptVms.push(this.formData.selectedVms[index])
}
}
this.formData.selectedVms = keptVms
}
}
this.toggleAllRunning = toggle => toggleState(toggle, 'Running')
this.toggleAllHalted = toggle => toggleState(toggle, 'Halted')
this.edit = schedule => {
const vms = filterFilter(selectHighLevelFilter(this.objects), {type: 'VM'})
const job = this.jobs[schedule.job]
const selectedVms = []
forEach(job.paramsVector.items[0].values, value => {
const vm = find(vms, vm => vm.id === value.id)
vm && selectedVms.push(vm)
})
const tag = job.paramsVector.items[0].values[0].tag
const depth = job.paramsVector.items[0].values[0].depth
const cronPattern = schedule.cron
this.resetData()
// const formData = this.formData
this.formData.selectedVms = selectedVms
this.formData.tag = tag
this.formData.depth = depth
this.formData.scheduleId = schedule.id
this.scheduleApi.setCron(cronPattern)
}
this.save = (id, vms, tag, depth, cron, enabled) => {
if (!vms.length) {
notify.warning({
title: 'No Vms selected',
message: 'Choose VMs to snapshot'
})
return
}
const _save = (id === undefined) ? saveNew(vms, tag, depth, cron, enabled) : save(id, vms, tag, depth, cron)
return _save
.then(() => {
notify.info({
title: 'Rolling snapshot',
message: 'Job schedule successfuly saved'
})
this.resetData()
})
.finally(() => {
refresh()
})
}
const save = (id, vms, tag, depth, cron) => {
const schedule = this.schedules[id]
const job = this.jobs[schedule.job]
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
tag,
depth
})
})
job.paramsVector.items[0].values = values
return xo.job.set(job)
.then(response => {
if (response) {
return xo.schedule.set(schedule.id, undefined, cron, undefined)
} else {
notify.error({
title: 'Update schedule',
message: 'Job updating failed'
})
throw new Error('Job updating failed')
}
})
}
const saveNew = (vms, tag, depth, cron, enabled) => {
const values = []
forEach(vms, vm => {
values.push({
id: vm.id,
tag,
depth
})
})
const job = {
type: 'call',
key: JOBKEY,
method: 'vm.rollingSnapshot',
paramsVector: {
type: 'crossProduct',
items: [
{
type: 'set',
values
}
]
}
}
return xo.job.create(job)
.then(jobId => {
return xo.schedule.create(jobId, cron, enabled)
})
}
this.delete = schedule => {
let jobId = schedule.job
xo.schedule.delete(schedule.id)
.then(() => xo.job.delete(jobId))
.finally(() => {
refresh()
})
}
this.resetData = () => {
this.formData.allRunning = false
this.formData.allHalted = false
this.formData.selectedVms = []
this.formData.scheduleId = undefined
this.formData.tag = undefined
this.formData.depth = undefined
this.formData.enabled = false
this.scheduleApi && this.scheduleApi.resetData && this.scheduleApi.resetData()
}
this.collectionLength = col => Object.keys(col).length
this.prettyCron = prettyCron.toString.bind(prettyCron)
if (!this.comesForEditing) {
refresh()
} else {
refresh()
.then(() => {
this.edit(this.schedules[this.comesForEditing])
delete this.comesForEditing
})
}
this.resetData()
this.objects = xoApi.all
})
// A module exports its name.
.name

View File

@@ -0,0 +1,117 @@
.panel.panel-default
p.page-title
i.xo-icon-snapshot(style="color: #e25440;")
| Rolling snapshots
form#snapform(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled)')
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.xo-icon-vm(style='color: #e25440;')
| VMs to snapshot
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
.container-fluid(ng-if = 'ctrl.formData')
.alert.alert-info(ng-if = '!ctrl.formData.scheduleId') Creating New Rolling Snapshot
.alert.alert-warning(ng-if = 'ctrl.formData.scheduleId') Modifying Rolling Snapshot ID {{ ctrl.formData.scheduleId }}
.form-group
label.control-label.col-md-2(for = 'tag') Tag
.col-md-10
input#tag.form-control(form = 'snapform', ng-model = 'ctrl.formData.tag', placeholder = 'Rolling snapshot tag', required)
.form-group(ng-class = '{"has-warning": !ctrl.formData.selectedVms.length}')
label.control-label.col-md-2(for = 'vmlist') VMs
.col-md-8
ui-select(form = 'snapform', ng-model = 'ctrl.formData.selectedVms', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose VMs to snapshot')
i.xo-icon-working(ng-if="isVMWorking($item)")
i(class="xo-icon-{{$item.power_state | lowercase}}",ng-if="!isVMWorking($item)")
| {{$item.name_label}}
span(ng-if="$item.$container")
| ({{($item.$container | resolve).name_label}})
ui-select-choices(repeat = 'vm in ctrl.objects | selectHighLevel | filter:{type: "VM"} | filter:$select.search | orderBy:["$container", "name_label"] track by vm.id')
div
i.xo-icon-working(ng-if="isVMWorking(vm)", tooltip="{{vm.power_state}} and {{(vm.current_operations | map)[0]}}")
i(class="xo-icon-{{vm.power_state | lowercase}}",ng-if="!isVMWorking(vm)", tooltip="{{vm.power_state}}")
| {{vm.name_label}}
span(ng-if="vm.$container")
| ({{(vm.$container | resolve).name_label || ((vm.$container | resolve).master | resolve).name_label}})
.col-md-2
label(tooltip = 'select/deselect all running VMs', style = 'cursor: pointer')
input.hidden(form = 'snapform', type = 'checkbox', ng-model = 'ctrl.formData.allRunning', ng-change = 'ctrl.toggleAllRunning(ctrl.formData.allRunning)')
span.fa-stack
i.xo-icon-running.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allRunning')
label(tooltip = 'select/deselect all halted VMs', style = 'cursor: pointer')
input.hidden(form = 'snapform', type = 'checkbox', ng-model = 'ctrl.formData.allHalted', ng-change = 'ctrl.toggleAllHalted(ctrl.formData.allHalted)')
span.fa-stack
i.xo-icon-halted.fa-stack-1x
i.fa.fa-circle-o.fa-stack-2x(ng-if = 'ctrl.formData.allHalted')
.form-group
label.control-label.col-md-2(for = 'depth') Depth
.col-md-10
input#depth.form-control(form = 'snapform', ng-model = 'ctrl.formData.depth', placeholder = 'How many snapshots to rollover', type = 'number', min = '1', required)
.form-group(ng-if = '!ctrl.formData.scheduleId')
label.control-label.col-md-2(for = 'enabled')
input#enabled(form = 'snapform', ng-model = 'ctrl.formData.enabled', type = 'checkbox')
.help-block.col-md-8 Enable immediatly after creation
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-clock-o
| Schedule
.panel-body.form-horizontal
.text-center(ng-if = '!ctrl.formData'): i.xo-icon-loading
xo-scheduler(data = 'ctrl.formData', api = 'ctrl.scheduleApi')
.grid-sm
.panel.panel-default
.panel-body
p.center
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 VMs to snapshot
th Depth
th Scheduling
th Enabled now
th
tr(ng-repeat = 'schedule in ctrl.schedules | orderBy:"id":true track by schedule.id')
td {{ schedule.id }}
td {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].tag }}
td
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
li(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 {{ ctrl.jobs[schedule.job].paramsVector.items[0].values[0].depth }}
td {{ 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-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,16 @@
.menu-grid
.side-menu
ul.nav
li
a(ui-sref = '.management', ui-sref-active = 'active')
i.fa.fa-fw.fa-clock-o.fa-menu
span.menu-entry Scheduler
li
a(ui-sref = '.rollingsnapshot')
i.xo-icon-snapshot.fa-fw.fa-menu
span.menu-entry Rolling snapshots
li
a(ui-sref = '.backup')
i.fa.fa-fw.fa-download.fa-menu
span.menu-entry Backup
.side-content(ui-view = '')

View File

@@ -0,0 +1,131 @@
import angular from 'angular'
import uiBootstrap from 'angular-ui-bootstrap'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import Bluebird from 'bluebird'
import filter from 'lodash.filter'
import forEach from 'lodash.foreach'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
const HIGH_LEVEL_OBJECTS = {
pool: true,
host: true,
VM: true,
SR: true,
network: true
}
export default angular.module('settings.acls', [
uiBootstrap,
uiRouter,
uiSelect,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.acls', {
controller: 'SettingsAcls as ctrl',
url: '/acls',
resolve: {
users (xo) {
return xo.user.getAll()
},
groups (xo) {
return xo.group.getAll()
},
roles (xo) {
return xo.role.getAll()
}
},
template: view
})
})
.controller('SettingsAcls', function ($scope, users, groups, roles, xoApi, xo, selectHighLevelFilter, filterFilter) {
const refreshAcls = () => {
xo.acl.get().then(acls => {
forEach(acls, acl => acl.newRole = acl.action)
this.acls = acls
})
}
refreshAcls()
this.types = Object.keys(HIGH_LEVEL_OBJECTS)
this.selectedTypes = {}
this.users = users
this.roles = roles
this.groups = groups
{
let usersById = this.usersById = Object.create(null)
for (let user of users) {
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.entities = this.users.concat(this.groups)
this.objects = xoApi.all
this.getUser = (id) => {
for (let user of this.users) {
if (user.id === id) {
return user
}
}
}
this.addAcl = () => {
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', () => {
let isHighLevel = (object) => HIGH_LEVEL_OBJECTS[object.type]
return (objects) => filter(objects, isHighLevel)
})
.name

View File

@@ -0,0 +1,84 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-key(style="color: #e25440;")
| ACLs
.grid-lg
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-plus-circle
| Create
.panel-body
form(ng-submit = 'ctrl.addAcl()')
.form-group
ui-select(ng-model = 'ctrl.subject')
ui-select-match(placeholder = 'Choose a user or group')
div
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
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.selectedObjects', multiple, close-on-select = 'false', required)
ui-select-match(placeholder = 'Choose an object')
i(class = 'xo-icon-{{$item.type | lowercase}}')
| {{$item.name_label}}
ui-select-choices(repeat = 'object in ctrl.objects | selectHighLevel | filter:$select.search | orderBy:["type", "name_label"]')
div
i(class = 'xo-icon-{{object.type | lowercase}}')
| {{object.name_label}}
span(ng-if="(object.type === 'SR' || object.type === 'VM') && object.$container")
| ({{(object.$container | resolve).name_label}})
span(ng-if="object.type === 'network'")
| ({{(object.$poolId | resolve).name_label}})
.text-center
span(ng-repeat = 'type in ctrl.types')
label(tooltip = 'select/deselect all {{type}}s', style = 'cursor: pointer')
input.hidden(type = 'checkbox', ng-model = 'ctrl.selectedTypes[type]', ng-change = 'ctrl.toggleType(ctrl.selectedTypes[type], type)')
span.fa-stack
i(class = 'xo-icon-{{type | lowercase}}').fa-stack-1x
i.fa.fa-square-o.fa-stack-2x.text-info(ng-if = 'ctrl.selectedTypes[type]')
.form-group
ui-select(ng-model = 'ctrl.role')
ui-select-match(placeholder = 'Choose a role')
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
| Manage
.panel-body
table.table.table-hover
tr
th User
th Object
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
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

@@ -0,0 +1,44 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import acls from './acls'
import group from './group'
import groups from './groups'
import servers from './servers'
import update from './update'
import user from './user'
import users from './users'
import view from './view'
export default angular.module('settings', [
uiRouter,
acls,
group,
groups,
servers,
update,
user,
users
])
.config(function ($stateProvider) {
$stateProvider.state('settings', {
abstract: true,
data: {
requireAdmin: true
},
template: view,
url: '/settings'
})
// Redirect to default sub-state.
$stateProvider.state('settings.index', {
url: '',
controller: function ($state) {
$state.go('settings.servers')
}
})
})
.name

View File

@@ -0,0 +1,123 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import view from './view'
export default angular.module('settings.servers', [
uiRouter,
uiSelect,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.servers', {
controller: 'SettingsServers as ctrl',
url: '/servers',
resolve: {
servers (xo) {
return xo.server.getAll()
}
},
template: view
})
})
.controller('SettingsServers', function ($scope, $interval, servers, xoApi, xo, notify) {
this.servers = servers
const selected = this.selectedServers = {}
const newServers = this.newServers = []
const refreshServers = () => {
xo.server.getAll().then(servers => {
this.servers = servers
})
}
const interval = $interval(refreshServers, 10e3)
$scope.$on('$destroy', () => {
$interval.cancel(interval)
})
this.connectServer = (id) => {
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)
}
this.addServer = () => {
newServers.push({
// Fake (unique) id needed by Angular.JS
id: Math.random(),
status: 'connecting'
})
}
this.addServer()
this.saveServers = () => {
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
if (selected[id]) {
delete selected[id]
xo.server.remove(id)
} else {
if (!server.password) {
delete server.password
}
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
if (!host) {
continue
}
xo.server.add({
host,
username,
password,
autoConnect: false
}).then(function (id) {
server.id = id
xo.server.connect(id).catch(error => {
notify.error({
title: 'Server connection error',
message: error.message
})
})
})
delete server.password
updateServers.push(server)
}
this.servers = updateServers
this.newServers.length = 0
this.addServer()
}
})
.name

View File

@@ -0,0 +1,77 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-cloud(style="color: #e25440;")
| Servers
.grid-sm
.panel.panel-default
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-1.text.center Actions
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")
td
.input-group
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")
td
input.form-control(type="text", ng-model="server.username")
td
input.form-control(type="password", ng-model="server.password", placeholder="Fill to change the password")
td.text-center
button.btn.btn-default(
ng-if="server.status === 'disconnected'",
type="button",
ng-click="ctrl.connectServer(server.id)",
tooltip="Reconnect this server"
)
i.fa.fa-link
button.btn.btn-danger(
ng-if="server.status === 'connected'",
type="button",
ng-click="ctrl.disconnectServer(server.id)"
tooltip="Disconnect this server"
)
i.fa.fa-unlink
td.text-center
input(type="checkbox", ng-model="ctrl.selectedServers[server.id]")
tr(ng-repeat="server in ctrl.newServers")
td
input.form-control(
type = "text"
ng-model = "server.host"
placeholder = "address[:port]"
)
td
input.form-control(
type = "text"
ng-model = "server.username"
ng-required = "server.host"
placeholder = "user"
)
td
input.form-control(
type="password"
ng-model="server.password"
ng-required = "server.host"
placeholder="password"
)
td &#160;
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.addServer()")
i.fa.fa-plus

View File

@@ -0,0 +1,94 @@
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', () => {
if (this.updater.state === 'registerNeeded' && this.updater.registerState !== 'unregistered' && this.updater.registerState !== 'error') {
this.updater.isRegistered()
}
}))
.catch(err => console.error(err))
this.updater.getConfiguration()
.then(configuration => this.configuration = _assign({}, configuration))
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message
}))
this.registerXoa = (email, password) => {
this.regPwd = ''
this.updater.register(email, password)
.then(() => this.updater.update())
.catch(AuthenticationFailed, () => {})
.catch(() => {})
}
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) => {
const config = {}
config.proxyHost = host && host.trim() || null
config.proxyPort = port && port.trim() || null
return this.updater.configure(config)
.then(configuration => this.configuration = _assign({}, configuration))
.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,111 @@
.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.updater.registerState === "unregistered"', ng-submit = 'ctrl.registerXoa(ctrl.regEmail, ctrl.regPwd)')
p.form-static-control XOA is not registered.
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"')
| Your Xen Orchestra appliance is registered to
span.text-success {{ ctrl.updater.token.registrationEmail }}
| .
br
span.big-stat
i.fa.fa-check-circle.text-success
.grid-sm(ng-if = 'ctrl.updater.state && ctrl.configuration')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| Settings
.panel-body
form.form-inline(ng-submit = 'ctrl.configure(ctrl.configuration.proxyHost, ctrl.configuration.proxyPort)')
fieldset
h4
i.fa.fa-globe
| Proxy settings
p If you need a proxy to access the Internet
.form-group
label.control-label Host:&nbsp;
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyHost', placeholder = 'myproxy.example.org')
| &nbsp;
| &nbsp;
.form-group
label.control-label Port:&nbsp;
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyPort', placeholder = '3128')
| &nbsp;
| &nbsp;
| &nbsp;
.form-group
button.btn.btn-primary(type = 'submit')
i.fa.fa-floppy-o
| Save

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

@@ -0,0 +1,126 @@
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'
export default angular.module('settings.users', [
uiRouter,
uiSelect,
uiEvent,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.users', {
controller: 'SettingsUsers as ctrl',
url: '/users',
resolve: {
users (xo) {
return xo.user.getAll()
}
},
template: view
})
})
.controller('SettingsUsers', function ($scope, $interval, users, xoApi, xo) {
this.users = users
this.permissions = [
{
label: 'User',
value: 'none'
},
{
label: 'Admin',
value: 'admin'
}
]
const selected = this.selectedUsers = {}
this.newUsers = []
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
})
})
}
}
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)
}
}
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)
}
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
}
})
.name

View File

@@ -0,0 +1,56 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-user(style="color: #e25440;")
| Users
.grid-sm
.panel.panel-default
.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
input.form-control(type="password", ng-model="user.password", placeholder="Fill to change the password", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
td.text-center
input(type="checkbox", ng-model="ctrl.selectedUsers[user.id]", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
tr(ng-repeat="user in ctrl.newUsers")
td
input.form-control(
type = "text"
ng-model = "user.email"
placeholder = "email"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td
select.form-control(
ng-options = "p.value as p.label for p in ctrl.permissions"
ng-model = "user.permission"
ng-required = "user.email"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td
input.form-control(
type = "password"
ng-model = "user.password"
ng-required = "user.email"
placeholder = "password"
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
)
td &#160;
p.text-center
button.btn.btn-primary(type="submit")
i.fa.fa-save
| Save
| &nbsp;
button.btn.btn-success(type="button", ng-click="ctrl.addUser()")
i.fa.fa-plus

View File

@@ -0,0 +1,24 @@
.menu-grid
.side-menu
ul.nav
li
a(ui-sref = '.servers', ui-sref-active = 'active')
i.fa.fa-fw.fa-cloud.fa-menu
span.menu-entry Servers
li
a(ui-sref = '.users')
i.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
span.menu-entry ACLs
li
a(ui-sref = '.update')
i.fa.fa-fw.fa-refresh.fa-menu
span.menu-entry Update
.side-content(ui-view = '')

214
app/modules/sr/index.js Normal file
View File

@@ -0,0 +1,214 @@
import angular from 'angular'
import escapeRegExp from 'lodash.escaperegexp'
import filter from 'lodash.filter'
import forEach from 'lodash.foreach'
import isEmpty from 'lodash.isempty'
import trim from 'lodash.trim'
import uiRouter from 'angular-ui-router'
import Bluebird from 'bluebird'
import xoTag from 'tag'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.sr', [
uiRouter,
xoTag
])
.config(function ($stateProvider) {
$stateProvider.state('SRs_view', {
url: '/srs/:id',
controller: 'SrCtrl',
template: view
})
})
.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) {
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
$scope.currentLogPage = 1
$scope.currentVDIPage = 1
let {get} = xoApi
$scope.$watch(() => xoApi.get($stateParams.id), function (SR) {
$scope.SR = SR
})
$scope.saveSR = function ($data) {
let {SR} = $scope
let {name_label, name_description} = $data
$data = {
id: SR.id
}
if (name_label !== SR.name_label) {
$data.name_label = name_label
}
if (name_description !== SR.name_description) {
$data.name_description = name_description
}
return xoApi.call('sr.set', $data)
}
$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.'
}).then(function () {
return xo.vdi.delete(id)
})
}
$scope.disconnectVBD = function (id) {
console.log('Disconnect VBD', id)
return modal.confirm({
title: 'VDI disconnection',
message: 'Are you sure you want to disconnect this VDI?'
}).then(function () {
return xoApi.call('vbd.disconnect', {id: id})
})
}
$scope.connectPBD = function (id) {
console.log('Connect PBD', id)
return xoApi.call('pbd.connect', {id: id})
}
$scope.disconnectPBD = function (id) {
console.log('Disconnect PBD', id)
return xoApi.call('pbd.disconnect', {id: id})
}
$scope.reconnectAllHosts = function () {
// TODO: return a Bluebird.all(promises).
for (let id of $scope.SR.$PBDs) {
let pbd = xoApi.get(id)
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?'
}).then(function () {
for (let id of $scope.SR.$PBDs) {
let pbd = xoApi.get(id)
xoApi.call('pbd.disconnect', {id: pbd.id})
console.log(pbd.id)
}
})
}
$scope.rescanSr = function (id) {
console.log('Rescan SR', id)
return xoApi.call('sr.scan', {id: id})
}
$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.'
}).then(function () {
return Bluebird.map($scope.SR.$PBDs, pbdId => {
let pbd = xoApi.get(pbdId)
return xoApi.call('pbd.disconnect', { id: pbd.id })
})
}).then(function () {
return xoApi.call('sr.destroy', {id: id})
}).then(function () {
$state.go('index')
notify.info({
title: 'SR remove',
message: 'SR is removed'
})
})
}
$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.'
}).then(function () {
return Bluebird.map($scope.SR.$PBDs, pbdId => {
let pbd = xoApi.get(pbdId)
return xoApi.call('pbd.disconnect', { id: pbd.id })
})
}).then(function () {
return xoApi.call('sr.forget', {id: id})
}).then(function () {
$state.go('index')
notify.info({
title: 'SR forget',
message: 'SR is forgotten'
})
})
}
$scope.saveDisks = function (data) {
// Group data by disk.
let disks = {}
forEach(data, function (value, key) {
let i = key.indexOf('/')
let id = key.slice(0, i)
let prop = key.slice(i + 1)
;(disks[id] || (disks[id] = {}))[prop] = value
})
let promises = []
forEach(disks, function (attributes, id) {
// Keep only changed attributes.
let disk = get(id)
forEach(attributes, function (value, name) {
if (value === disk[name]) {
delete attributes[name]
}
})
if (!isEmpty(attributes)) {
// Inject id.
attributes.id = id
// Ask the server to update the object.
promises.push(xoApi.call('vdi.set', attributes))
}
})
return $q.all(promises)
}
})
// A module exports its name.
.name

215
app/modules/sr/view.jade Normal file
View File

@@ -0,0 +1,215 @@
.grid
.panel.panel-default
p.page-title
i.xo-icon-sr
| {{SR.name_label}}
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| General
span.quick-edit(tooltip="Edit General settings", ng-click="srSettings.$show()")
i.fa.fa-edit.fa-fw
.panel-body
form(editable-form="", name="srSettings", onbeforesave="saveSR($data)")
dl.dl-horizontal
dt Name
dd
span(editable-text="SR.name_label", e-name="name_label", e-form="srSettings")
| {{SR.name_label}}
dt Description
dd
span(editable-text="SR.name_description", e-name="name_description", e-form="srSettings")
| {{SR.name_description}}
dt Content type:
dd {{SR.SR_type}}
dt 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.id})") {{container.name_label}}
| )
dd(ng-if="'host' === container.type") No
dt Size
dd {{SR.size | bytesToSize}}
dt UUID
dd {{SR.UUID}}
.btn-form(ng-show="srSettings.$visible")
p.center
button.btn.btn-default(type="button", ng-disabled="srSettings.$waiting", ng-click="srSettings.$cancel()")
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(type="submit", ng-disabled="srSettings.$waiting")
i.fa.fa-save
| Save
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-stats
| Stats
.panel-body
.row
.col-sm-6.col-lg-4
p.stat-name Physical Alloc:
canvas.stat-simple(id="doughnut", class="chart chart-doughnut", data="[(SR.physical_usage), (SR.size - SR.physical_usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
.col-sm-6.col-lg-4
p.stat-name Virtual Alloc:
canvas.stat-simple(id="doughnut", class="chart chart-doughnut", data="[(SR.usage), (SR.size - SR.usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
.col-sm-4.visible-lg
p.stat-name VDIs:
p.center.big-stat {{SR.VDIs.length}}
.row.hidden-lg
.col-sm-12
br
p.stat-name {{SR.VDIs.length}} VDIs
//- Action panel
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flash
| Actions
.panel-body.text-center
.grid
.grid-cell.btn-group
button.btn(tooltip="Rescan all the VDI", type="button", style="width: 90%", ng-click="rescanSr(SR.id)")
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()")
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()")
i.fa.fa-power-off.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Forget SR", type="button", style="width: 90%", xo-click="forgetSR(SR.id)")
i.fa.fa-2x.fa-fw.fa-ban
.grid-cell.btn-group
button.btn(tooltip="Remove SR", type="button", style="width: 90%", xo-click="removeSR(SR.id)")
i.fa.fa-2x.fa-trash-o
//- TODO: Space panel
.grid
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| VDI Map
.panel-body
.progress
.progress-bar.progress-bar-vm(ng-if="((VDI.size/SR.size)*100) > 0.5", ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.id", role="progressbar", aria-valuemin="0", aria-valuenow="{{VDI.size}}", aria-valuemax="{{SR.size}}", style="width: {{[VDI.size, SR.size] | percentage}}", tooltip="{{VDI.name_label}} ({{[VDI.size, SR.size] | percentage}})")
//- 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}}
ul.list-inline.text-center
li Total: {{SR.size | bytesToSize}}
li Currently used: {{SR.usage | bytesToSize}}
li Available: {{SR.size-SR.usage | bytesToSize}}
//- TODO: VDIs.
.grid
form(name = "disksForm" editable-form = '' onbeforesave = 'saveDisks($data)').panel.panel-default
.panel-heading.panel-title
i.xo-icon-disk
| Virtual disks
span.quick-edit(tooltip="Edit disks", ng-click="disksForm.$show()")
i.fa.fa-edit.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 Tags
th Size
th Virtual Machine:
tr(ng-repeat="VDI in SR.VDIs | resolve | vdiFilter:vdiSearch | orderBy:natural('name_label') | slice:(10*(currentVDIPage-1)):(10*currentVDIPage)")
td.oneliner
span(
editable-text="VDI.name_label"
e-name = '{{VDI.id}}/name_label'
)
| {{VDI.name_label}} &nbsp;
span.label.label-info(ng-if="VDI.$snapshot_of") snapshot
td.oneliner
span(
editable-text="VDI.name_description"
e-name = '{{VDI.id}}/name_description'
)
| {{VDI.name_description}}
td
xo-tag(object = 'VDI')
td
//- FIXME: should be editable, but the server needs first
//- to accept a human readable string.
| {{VDI.size | bytesToSize}}
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")
//- 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 = '(SR.VDIs | resolve | vdiFilter:vdiSearch).length > 10 || currentVDIPage > 1')
pagination(boundary-links="true", total-items="(SR.VDIs | resolve | 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(
type="reset"
ng-disabled="disksForm.$waiting"
ng-click="disksForm.$cancel()"
)
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(
type="submit"
ng-disabled="disksForm.$waiting"
)
i.fa.fa-save
| Save
//- /VDIs.
//- Hosts.
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-link
| Connected hosts
span.quick-edit(tooltip="Reconnect all hosts", ng-click="reconnectAllHosts()")
i.fa.fa-plus-square.fa-fw
.panel-body
table.table.table-hover
th Name
th Status
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.id)")
i.fa.fa-unlink.fa-lg(tooltip="Disconnect to this host")
td(ng-if="!PBD.attached")
span.label.label-default Disconnected
span.pull-right.btn-group.quick-buttons
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
| Logs
.panel-body
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 | 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,363 @@
angular = require 'angular'
forEach = require 'lodash.foreach'
throttle = require 'lodash.throttle'
#=====================================================================
sourceHost = null
module.exports = angular.module 'xoWebApp.tree', [
require 'angular-file-upload'
require 'angular-ui-router'
require 'xo-api'
require 'xo-services'
require '../delete-vms'
]
.config ($stateProvider) ->
$stateProvider.state 'tree',
controller: 'TreeCtrl'
data: {
requireAdmin: true
},
template: require './view'
url: '/tree'
.controller 'TreeCtrl', (
$scope
$upload
dateFilter
deleteVmsModal
modal
notify
xo
xoApi
) ->
$scope.stats = xoApi.stats
$scope.hosts = xoApi.getView('hosts')
$scope.hostsByPool = xoApi.getIndex('hostsByPool')
$scope.pools = xoApi.getView('pools')
VMs = $scope.VMs = xoApi.getView('VM')
$scope.runningVms = xoApi.getView('runningVms')
$scope.runningVmsByPool = xoApi.getIndex('runningVmsByPool')
$scope.vmsByPool = xoApi.getIndex('vmsByPool')
$scope.vmsByContainer = xoApi.getIndex('vmsByContainer')
$scope.vmControllersByContainer = xoApi.getIndex('vmControllersByContainer')
$scope.srsByContainer = xoApi.getIndex('srsByContainer')
$scope.pool_disconnect = xo.pool.disconnect
$scope.new_sr = xo.pool.new_sr
$scope.pool_addHost = (id) ->
xo.host.attach id
$scope.enableHost = (id) ->
xo.host.enable id
notify.info {
title: 'Host action'
message: 'Host is enabled'
}
$scope.disableHost = (id) ->
modal.confirm({
title: 'Disable host'
message: 'Are you sure you want to disable this host? In disabled state, no new VMs can be started and currently active VMs on the host continue to execute.'
}).then ->
xo.host.disable id
.then ->
notify.info {
title: 'Host action'
message: 'Host is disabled'
}
$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'
}).then ->
xo.host.detach 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.restartToolStack = (id) ->
modal.confirm({
title: 'Restart XAPI'
message: 'Are you sure you want to restart the XAPI toolstack?'
}).then ->
xo.host.restartToolStack id
$scope.shutdownHost = (id) ->
modal.confirm({
title: 'Shutdown host'
message: 'Are you sure you want to shutdown this host?'
}).then ->
xo.host.stop id
$scope.startHost = (id) ->
xo.host.start id
bulkConfirms = {
'stopVM': {
title: 'VM shutdown',
message: 'Are you sure you want to shutdown all selected VMs ?'
},
'rebootVM': {
title: 'VM reboot',
message: 'Are you sure you want to reboot all selected VMs ?'
},
'suspendVM': {
title: 'VM suspend',
message: 'Are you sure you want to suspend all selected VMs ?'
},
'force_rebootVM': {
title: 'VM force reboot',
message: 'Are you sure you want to force reboot for all selected VMs ?'
},
'force_stopVM': {
title: 'VM force shutdown',
message: 'Are you sure you want to force shutdown for all selected VMs ?'
},
'migrateVM': {
title: 'VM migrate',
message: 'Are you sure you want to migrate all selected VMs ?'
}
}
unitConfirms = {
'stopVM': {
title: 'VM shutdown',
message: 'Are you sure you want to shutdown this VM ?'
},
'rebootVM': {
title: 'VM reboot',
message: 'Are you sure you want to reboot this VM ?'
},
'suspendVM': {
title: 'VM suspend',
message: 'Are you sure you want to suspend this VM ?'
},
'force_rebootVM': {
title: 'VM force reboot',
message: 'Are you sure you want to force reboot for this VM ?'
},
'force_stopVM': {
title: 'VM force shutdown',
message: 'Are you sure you want to force shutdown for this VM ?'
},
'migrateVM': {
title: 'VM force shutdown',
message: 'Are you sure you want to migrate this VM ?'
}
}
$scope.startVM = xo.vm.start
$scope.stopVM = xo.vm.stop
$scope.force_stopVM = (id) -> xo.vm.stop id, true
$scope.rebootVM = xo.vm.restart
$scope.force_rebootVM = (id) -> xo.vm.restart id, true
$scope.suspendVM = (id) -> xo.vm.suspend id, true
$scope.resumeVM = (id) -> xo.vm.resume id, true
$scope.migrateVM = (id, hostId) -> xo.vm.migrate id, hostId
$scope.snapshotVM = (id) ->
vm = xoApi.get(id)
date = dateFilter Date.now(), 'yyyy-MM-ddTHH:mmZ'
snapshot_name = "#{vm.name_label}_#{date}"
xo.vm.createSnapshot id, snapshot_name
# check if there is any operation pending on a VM
$scope.isVMWorking = (VM) ->
return true for _ of VM.current_operations
false
$scope.deleteVMs = ->
{selected_VMs} = $scope
deleteVmsModal (id for id, selected of selected_VMs when selected)
# VMs checkboxes.
do ->
# This map marks which VMs are selected.
selected_VMs = $scope.selected_VMs = Object.create null
# Number of selected VMs.
$scope.n_selected_VMs = 0
# This is the master checkbox.
# Three states: true/false/null
$scope.master_selection = false
# Wheter all VMs are selected.
$scope.all = false
# Whether no VMs are selected.
$scope.none = true
# Updates `all`, `none` and `master_selection` when necessary.
$scope.$watch 'n_selected_VMs', (n) ->
$scope.all = (VMs.size is n)
$scope.none = (n is 0)
# When the master checkbox is clicked from indeterminate
# state, it should go to unchecked like Gmail.
$scope.master_selection = (n isnt 0)
make_matcher = (sieve) ->
(item) ->
for key, val of sieve
return false unless item[key] is val
true
$scope.selectVMs = (sieve) ->
if (sieve is true) or (sieve is false)
forEach(VMs.all, (VM) ->
selected_VMs[VM.id] = sieve
return
)
$scope.n_selected_VMs = if sieve then VMs.size else 0
return
matcher = make_matcher sieve
n = 0
forEach(VMs.all, (VM) ->
if (selected_VMs[VM.id] = matcher(VM))
++n
return
)
$scope.n_selected_VMs = n
$scope.updateVMSelection = (id) ->
if selected_VMs[id]
++$scope.n_selected_VMs
else
--$scope.n_selected_VMs
$scope.bulkAction = (action, args...) ->
fn = $scope[action]
unless angular.isFunction fn
throw new Error "invalid action #{action}"
runBulk = () ->
for id, selected of selected_VMs
fn id, args... if selected
# Unselects all VMs.
$scope.selectVMs false
if action of bulkConfirms
modal.confirm(bulkConfirms[action])
.then runBulk
else
runBulk()
$scope.confirmAction = (action, args...) ->
fn = $scope[action]
unless angular.isFunction fn
throw new Error "invalid action #{action}"
doAction = () ->
fn args...
if action of unitConfirms
modal.confirm(unitConfirms[action])
.then doAction
else
doAction()
$scope.importVm = ($files, id) ->
file = $files[0]
notify.info {
title: 'VM import started'
message: "Starting the VM import"
}
xo.vm.import id
.then ({ $sendTo: url }) ->
return $upload.http {
method: 'POST'
url
data: file
}
.then (result) ->
throw result.status if result.status isnt 200
notify.info
title: 'VM import'
message: 'Success'
$scope.patchPool = ($files, id) ->
file = $files[0]
xo.pool.patch id
.then ({ $sendTo: url }) ->
return $upload.http {
method: 'POST'
url
data: file
}
.progress throttle(
(event) ->
percentage = (100 * event.loaded / event.total)|0
notify.info
title: 'Upload patch'
message: "#{percentage}%"
6e3
)
.then (result) ->
throw result.status if result.status isnt 200
notify.info
title: 'Upload patch'
message: 'Success'
.directive 'draggable', () ->
{
link: (scope, element, attr) ->
element.on 'dragstart', (event) ->
event.originalEvent.dataTransfer.setData('vm', event.currentTarget.getAttribute('vm'))
# event.originalEvent.dataTransfer.setData('host', event.currentTarget.getAttribute('host'))
sourceHost = event.currentTarget.getAttribute('host')
element.addClass('xo-dragged')
$('[droppable]:not([host="' + sourceHost + '"])').addClass('xo-drop-legit')
element.on 'dragend', (event) ->
element.removeClass('xo-dragged')
$('[droppable]').removeClass('xo-drop-target xo-drop-legit')
sourceHost = null
restrict: 'A'
}
.directive 'droppable', (xo, notify, modal) ->
{
link: (scope, element, attr) ->
element.on 'dragover', (event) ->
event.preventDefault()
targetHost = event.currentTarget.getAttribute('host')
if sourceHost isnt targetHost
element.addClass('xo-drop-target').removeClass('xo-drop-legit')
element.on 'dragleave', (event) ->
targetHost = event.currentTarget.getAttribute('host')
if sourceHost isnt targetHost
element.removeClass('xo-drop-target')
element.addClass('xo-drop-legit')
element.on 'drop', (event) ->
event.preventDefault()
vm = event.originalEvent.dataTransfer.getData('vm')
# sourceHost = event.originalEvent.dataTransfer.getData('host')
targetHost = event.currentTarget.getAttribute('host')
if sourceHost isnt targetHost
modal.confirm
title: 'VM migrate'
message: 'Are you sure you want to migrate this VM?'
.then ->
xo.vm.migrate vm, targetHost
restrict: 'A'
}
# A module exports its name.
.name

410
app/modules/tree/view.jade Normal file
View File

@@ -0,0 +1,410 @@
//- @todo Remove code duplication for the VMs listing by using a macro.
.sub-bar
.grid
.grid-cell.overview
//- Stats
i(tooltip="{{pools.size}} pools").hidden-xs
i.small {{pools.size}}x
| &nbsp;
i.xo-icon-pool
| &nbsp;
| &nbsp;
i(tooltip="{{hosts.size}} hosts").hidden-xs
i.small {{hosts.size}}x
| &nbsp;
i.xo-icon-host
| &nbsp;
| &nbsp;
i(tooltip="{{runningVms.size}} of {{VMs.size}} VMs running")
i.small {{runningVms.size}}x
| &nbsp;
i.xo-icon-vm
| &nbsp;
| &nbsp;
i(tooltip="{{stats.$vCPUs}} vCPUs used of {{stats.$CPUs}} CPUs")
i.small {{stats.$vCPUs}}x
| &nbsp;
i.xo-icon-cpu
| &nbsp;
| &nbsp;
i(tooltip="{{stats.$memory.usage | bytesToSize}} RAM allocated of {{stats.$memory.size | bytesToSize}}")
i.small {{stats.$memory.usage | bytesToSize}}
| &nbsp;
i.xo-icon-memory
.grid-cell
.btn-group.before-action-bar.dropdown(dropdown)
a.btn.navbar-btn.btn-default.dropdown-toggle.inversed(dropdown-toggle)
input.inverse(
type="checkbox",
ng-model="master_selection",
ng-change="selectVMs(master_selection)",
ui-indeterminate="!(all || none)", stop-event="click"
)
| &nbsp;
i.fa.fa-caret-down
ul.dropdown-menu.inverse(role="menu")
li(ng-repeat="power_state in ['Halted', 'Running']")
a(ng-click="selectVMs({power_state: power_state})")
i.fa-fw(class="xo-icon-{{power_state | lowercase}}")
| {{power_state}}
li.divider
li(
ng-if="hosts.size"
ng-repeat="host in hosts.all | map | orderBy:natural('name_label') track by host.id"
)
a(ng-click="selectVMs({$container: host.id})")
i.xo-icon-host.fa-fw
| On {{host.name_label}}
.action-bar(ng-if="!none")
| &nbsp;
.btn-group
button.btn.navbar-btn.btn-default.inversed(tooltip="Stop VM", type="button", ng-click="bulkAction('stopVM')")
i.fa.fa-stop
button.btn.navbar-btn.btn-default.inversed(tooltip="Start VM", type="button", ng-click="bulkAction('startVM')")
i.fa.fa-play
button.btn.navbar-btn.btn-default.inversed(tooltip="Reboot VM", type="button", ng-click="bulkAction('rebootVM')")
i.fa.fa-refresh
| &nbsp;
.btn-group.dropdown(dropdown)
button.btn.navbar-btn.btn-default.dropdown-toggle.inversed(
dropdown-toggle
tooltip="Migrate VM"
type="button"
)
i.fa.fa-share
| &nbsp;
i.fa.fa-caret-down
ul.dropdown-menu.inverse(role="menu")
li(ng-repeat="host in hosts.all | map | orderBy:natural('name_label') track by host.id")
a(ng-click="bulkAction('migrateVM', host.id)")
i.xo-icon-host.fa-fw
| To {{host.name_label}}
| &nbsp;
.btn-group.dropdown(dropdown)
button.btn.navbar-btn.btn-default.dropdown-toggle.inversed(
dropdown-toggle
type="button"
)
| More
| &nbsp;
i.fa.fa-caret-down
ul.dropdown-menu.inverse(role="menu")
li
a(ng-click="bulkAction('suspendVM')")
i.fa.fa-pause.fa-fw
| Suspend
li
a(ng-click="bulkAction('resumeVM')")
i.fa.fa-play.fa-fw
| Resume
li
a(ng-click="bulkAction('force_rebootVM')")
i.fa.fa-bolt.fa-fw
| Force reboot
li
a(ng-click="bulkAction('force_stopVM')")
i.fa.fa-power-off.fa-fw
| Force shutdown
li.divider
li
a(ng-click="bulkAction('snapshotVM')")
i.xo-icon-snapshot.fa-fw
| Take a snapshot
li
a(ng-click="deleteVMs()")
i.fa.fa-trash-o.fa-fw
| Delete
//- FIXME: Ugly trick to force the pools to be under the sub bar.
//- Add +7px to the 50px for having some space before the first pool.
div(style="margin-top: 57px; visibility: hidden; height: 0") .
//- If we haven't any data
.panel.panel-default.text-center(ng-if="!pools.size")
h1 Welcome on Xen Orchestra!
h3 It seems you aren't connected to any Xen server:
br
a.btn.btn-success.big(ui-sref="settings.index")
i.fa.fa-plus-circle
| Add server
br
br
br
p You can add a new host anytime by clicking on the menu icon "
i.fa.fa-th
| " and choose "
i.fa.fa-cog
| Settings"
p Enjoy Xen Orchestra!
//- If we have data
.container-fluid(ng-if="pools.size")
//- Contains a pool and all its children (hosts).
.grid.pool-block(ng-repeat="pool in pools.all | map | orderBy:[natural('name_label'), 'id'] track by pool.id")
//- Pseudo pool if it is not a named pool.
//- .grid-cell.pool-cell(ng-if="!pool.name_label")
//- p.center(style="margin-top: 2em;") No pool connected
//- Contains information about the pool if it is a named pool.
.grid-cell.pool-cell.hidden-xs
//- Header (name + dropdown menu).
.dropdown.dropdown-pool(dropdown)
a.pool-name(ui-sref="pools_view({id: pool.id})")
span(ng-if="pool.name_label")
| {{pool.name_label}}
span.text-muted(ng-if="!pool.name_label")
| {{(pool.master | resolve).name_label}}
a.dropdown-toggle(ng-if="pool.name_label", dropdown-toggle)
| &nbsp;
i.fa.fa-caret-down.big-caret
ul.dropdown-menu.left(role="menu")
//- TODO: remove until handled this properly
//- li
//- a(xo-sref="SRs_new({container: pool.id})")
//- i.xo-icon-sr.fa-fw
//- | Add SR
li
a(xo-sref="VMs_new({container: pool.id})")
i.xo-icon-vm.fa-fw
| Create VM
//- TODO: solve the "a" problem for ng-file-select
li(ng-file-select="patchPool($files, pool.id)")
a
i.fa.fa-file-code-o.fa-fw
| Patch
li.divider
li
a.disabled(xo-click="pool_disconnect(pool.id)")
i.fa.fa-unlink.fa-fw
| Disconnect
//- /Header.
//- Stats & SRs list.
div
//- Stats.
ul.list-unstyled.stats
li
i(tooltip="{{hostsByPool[pool.id] | count}} hosts connected")
i.small {{hostsByPool[pool.id] | count}}x
| &nbsp;
i.xo-icon-host
| &nbsp;
| &nbsp;
i(tooltip="{{runningVmsByPool[pool.id] | count}} of {{vmsByPool[pool.id] | count}} VMs running")
i.small {{runningVmsByPool[pool.id] | count}}x
| &nbsp;
i.xo-icon-vm
li(ng-if="pool.master")
| Master:
| &nbsp;
a(ui-sref="hosts_view({id: (pool.master | resolve).id})") {{(pool.master | resolve).name_label}}
//- /Stats.
//- SRs.
div(ng-if="!(srsByContainer[pool.id] | isEmpty)")
p.center.small-caps SRs:
table.table.table-hover.table-condensed
tr(ng-repeat="SR in srsByContainer[pool.id] | map | orderBy:natural('name_label') track by SR.id", xo-sref="SRs_view({id: SR.id})")
td.col-md-6.sr-name.no-border(ng-class="{'default-sr': SR.id === pool.default_SR}", title="{{SR.name_label}}")
i.xo-icon-sr
| {{SR.name_label}}
td.col-md-6.right.no-border
.progress.progress-small(tooltip="Disk: {{[SR.usage, SR.size] | percentage}} allocated")
.progress-bar(role="progressbar", aria-valuenow="{{100*SR.usage/SR.size}}", aria-valuemin="0", aria-valuemax="100", style="width: {{[SR.usage, SR.size] | percentage}}")
//- Contains all the hosts of this pool.
.grid-cell.grid--gutters.hosts-vms-cells
//- Contains a host and all its children (VMs).
.grid(ng-repeat="host in hostsByPool[pool.id] | map | orderBy:natural('name_label') track by host.id")
//- Contains information about the host.
.grid-cell.host-cell
//- Header (name + dropdown menu).
.dropdown.dropdown-pool(dropdown)
a.host-name(ui-sref="hosts_view({id: host.id})")
| {{host.name_label}}
a.dropdown-toggle(dropdown-toggle)
| &nbsp;
i.fa.fa-caret-down
ul.dropdown-menu.left(role="menu")
li
a(xo-sref="SRs_new({container: host.id})")
i.xo-icon-sr.fa-fw
| Add SR
li
a(xo-sref="VMs_new({container: host.id})")
i.xo-icon-vm.fa-fw
| Create VM
//- TODO: solve the "a" problem for ng-file-select
li(ng-file-select="importVm($files, host.id)")
a
i.fa.fa-upload.fa-fw
| Import VM
li.divider
li
a(ng-repeat="controller in [vmControllersByContainer[host.id]] track by controller.id", xo-sref="consoles_view({id: controller.id})")
i.xo-icon-console.fa-fw
| Console
li(ng-if="!host.enabled")
a(xo-click="enableHost(host.id)")
i.fa.fa-check-circle.fa-fw
| Enable
li(ng-if="host.enabled")
a(xo-click="disableHost(host.id)")
i.fa.fa-times-circle.fa-fw
| Disable
li
a(xo-click="rebootHost(host.id)")
i.fa.fa-refresh.fa-fw
| Reboot
li(ng-if="host.power_state === 'Halted'")
a(xo-click="startHost(host.id)")
i.fa.fa-power-off.fa-fw
| Start
li(ng-if="host.power_state === 'Running'")
a(xo-click="shutdownHost(host.id)")
i.fa.fa-power-off.fa-fw
| Shutdown
li
a(xo-click="restartToolStack(host.id)")
i.fa.fa-retweet.fa-fw
| Restart toolstack
li(ng-if="pool.name_label")
a(xo-click="pool_removeHost(host.id)")
i.fa.fa-cloud-upload.fa-fw
| Remove from pool
li(ng-if="!pool.name_label")
a(xo-click="pool_addHost(host.id)")
i.fa.fa-cloud-download.fa-fw
| Add to pool
//- /Header.
//- Stats.
ul.list-unstyled.stats
//- Warning icon if host is halted or disabled
li.text-danger(ng-if="host.power_state === 'Halted'")
i.fa.fa-warning
| Halted
li.text-warning(ng-if="!host.enabled && host.power_state === 'Running'")
i.fa.fa-warning
| Disabled
//- Memory
li(ng-if="host.power_state === 'Running' && host.enabled")
i.xo-icon-memory.i-progress
.progress.progress-small(tooltip="RAM: {{[host.memory.usage, host.memory.size] | percentage}} allocated")
.progress-bar(role="progressbar", aria-valuenow="{{100*host.memory.usage/host.memory.size}}", aria-valuemin="0", aria-valuemax="100", style="width: {{[host.memory.usage, host.memory.size] | percentage}}")
//- Host address
li.text-muted.substats
i.xo-icon-network
| {{host.address}}
//- Contains all the VMs of this host.
.grid-cell.vm-cell(droppable = 'true', host = '{{ host.id }}')
//- If no VMs, fill the space with a message.
.vms-notice(ng-if="vmsByContainer[host.id] | isEmpty")
//- | Host halted.
p(ng-if="host.power_state === 'Halted'")
| Host halted.
div(ng-if="host.power_state === 'Running'")
p(ng-if="!host.enabled")
| Host disabled.
p(ng-if="host.enabled")
| No VMs on this host.
//- /Message if no VMs.
//- TODO: comment
.table-responsive(ng-if="!(vmsByContainer[host.id] | isEmpty)")
table.table.table-hover.table-condensed
//- Contains a VM.
tr(ng-repeat="VM in vmsByContainer[host.id] | map | orderBy:natural('name_label') track by VM.id", xo-sref="VMs_view({id: VM.id})", draggable = 'true', vm = '{{ VM.id }}', host = '{{ host.id }}')
//- Handle used for drag & drop.
td.grab
//- Checkbox used for selection.
td.select-vm
input(type="checkbox", ng-model="selected_VMs[VM.id]", ng-change="updateVMSelection(VM.id)")
//- Power state
td.vm-power-state
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.
td.vm-name.col-xs-8.col-sm-2.col-md-2
p.vm {{VM.name_label}}
//- Quick actions.
td.vm-quick-buttons.col-md-2.hidden-xs
.quick-buttons
a(tooltip="Shutdown VM", xo-click="confirmAction('stopVM', VM.id)")
i.fa.fa-stop
a(tooltip="Start VM", xo-click="startVM(VM.id)")
i.fa.fa-play
a(tooltip="Reboot VM", xo-click="confirmAction('rebootVM', VM.id)")
i.fa.fa-refresh
a(tooltip="VM Console", xo-sref="consoles_view({id: VM.id})")
i.xo-icon-console
//- Description.
td.vm-description.col-md-4.hidden-xs
i(class="xo-icon-{{VM.os_version.distro | osFamily}}",ng-if="VM.os_version.distro", tooltip="{{VM.os_version.name}}")
| &nbsp;
i.fa.fa-fw(ng-if="!VM.os_version.distro")
| {{VM.name_description}}
//- Metrics.
//- Memory
td.vm-memory-stat.col-md-2.hidden-xs
.cpu
| {{VM.memory.size | bytesToSize}}
i.xo-icon-docker.fa-fw(ng-if="VM.docker", tooltip="Docker enabled")
i.fa.fa-fw(ng-if="VM.PV_drivers && !VM.docker && VM.PV_drivers_up_to_date")
i.xo-icon-warning.fa-fw(ng-if="VM.PV_drivers && !VM.PV_drivers_up_to_date", tooltip="Xen tools outdated")
i.xo-icon-info.fa-fw(ng-if="!VM.PV_drivers", tooltip="Xen tools not installed")
//- /Metrics.
//- Address.
td.text-muted.text-right.col-md-2.hidden-xs
| {{VM.addresses["0/ip"]}}
//- Contains a pseudo-host which contains all VMs not in any hosts.
.grid(ng-if="!(vmsByPool[pool.id] | isEmpty)")
//- This is where the information about a host would be displayed.
.grid-cell.host-cell
//- Contains all the VMs of this pool.
.grid-cell.vm-cell
//- TODO: comment
.table-responsive
table.table.table-hover.table-condensed
//- Contains a VM.
tr(ng-repeat="VM in vmsByContainer[pool.id] | map | orderBy:natural('name_label') track by VM.id", xo-sref="VMs_view({id: VM.id})")
//- Handle used for drag & drop.
td.grab
//- Checkbox used for selection.
td.select-vm
input(type="checkbox", ng-model="selected_VMs[VM.id]", ng-change="updateVMSelection(VM.id)")
//- Power state
td.vm-power-state
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.
td.vm-name.col-xs-8.col-sm-2.col-md-2
p.vm {{VM.name_label}}
//- Quick actions.
td.vm-quick-buttons.col-md-2.hidden-xs
.quick-buttons
a(tooltip="Shutdown VM", xo-click="stopVM(VM.id)")
i.fa.fa-stop
a(ng-if="VM.power_state == 'Suspended'", tooltip="Resume VM", xo-click="resumeVM(VM.id)")
i.fa.fa-play
a(ng-if="VM.power_state != 'Suspended'", tooltip="Start VM", xo-click="startVM(VM.id)")
i.fa.fa-play
a(tooltip="Reboot VM", xo-click="rebootVM(VM.id)")
i.fa.fa-refresh
a(tooltip="VM Console")
i.xo-icon-console
//- Description.
td.vm-description.col-md-4.hidden-xs
i(class="xo-icon-{{VM.os_version.distro | osFamily}}",ng-if="VM.os_version.distro", tooltip="{{VM.os_version.name}}")
| &nbsp;
i.fa.fa-fw(ng-if="!VM.os_version.distro")
| {{VM.name_description}}
//- Metrics.
//- Memory
td.vm-memory-stat.col-md-2.hidden-xs
.cpu
| {{VM.memory.size | bytesToSize}}
i.xo-icon-docker.fa-fw(ng-if="VM.docker", tooltip="Docker enabled")
i.fa.fa-fw(ng-if="VM.PV_drivers && !VM.docker && VM.PV_drivers_up_to_date")
i.xo-icon-info.fa-fw(ng-if="!VM.PV_drivers", tooltip="Xen tools not installed")
i.xo-icon-warning.fa-fw(ng-if="VM.PV_drivers && !VM.PV_drivers_up_to_date", tooltip="Xen tools outdated")
//- /Metrics.
//- Address.
td.text-muted.text-right.col-md-2.hidden-xs
| {{VM.addresses["0/ip"]}}
//- /Pseudo host containing VMs not on any hosts.
//- /Hosts of this pool.
//- /Pool with its children.

View File

@@ -0,0 +1,365 @@
import * as format from '@julien-f/json-rpc/format'
import angular from 'angular'
import 'angular-bootstrap'
import Bluebird from 'bluebird'
import makeError from 'make-error'
import parse from '@julien-f/json-rpc/parse'
import WebSocket from 'ws'
import {EventEmitter} from 'events'
import modal from './modal'
const calls = {}
function jsonRpcCall (socket, method, params = {}) {
const req = format.request(method, params)
const reqId = req.id
socket.send(JSON.stringify(req))
let waiter = {}
const promise = new Bluebird((resolve, reject) => {
waiter.resolve = resolve
waiter.reject = reject
})
calls[reqId] = waiter
return promise
}
function jsonRpcNotify (socket, method, params = {}) {
return Bluebird.resolve(socket.send(JSON.stringify(format.notification(method, params))))
}
function getCurrentUrl () {
if (typeof window === 'undefined') {
throw new Error('cannot get current URL')
}
return String(window.location)
}
function adaptUrl (url, port = null) {
const matches = /^http(s?):\/\/([^\/:]*(?::[^\/]*)?)(?:[^:]*)?$/.exec(url)
if (!matches || !matches[2]) {
throw new Error('current URL not recognized')
}
return 'ws' + matches[1] + '://' + matches[2] + '/api/updater'
}
function blockXoaAccess (xoaState) {
return xoaState.state === 'untrustedTrial'
}
export const NotRegistered = makeError('NotRegistered')
export const AuthenticationFailed = makeError('AuthenticationFailed')
export default angular.module('updater', [
'ui.bootstrap'
])
.factory('updater', function ($interval, $timeout, $window, $modal) {
class Updater extends EventEmitter {
constructor () {
super()
this._log = []
this._lastRun = 0
this._lowState = null
this.state = null
this.registerState = 'uknown'
this.registerError = ''
this._connection = null
this.isConnected = false
this.updating = false
this.upgrading = false
this.token = null
}
update () {
this.emit('updating')
this.updating = true
return this._update(false)
}
upgrade () {
this.emit('upgrading')
this.upgrading = true
return this._update(true)
}
_promptForReload () {
const modalInstance = $modal.open({
template: modal,
backdrop: false
})
return modalInstance.result
.then(() => {
$window.location.reload()
})
.catch(() => true)
}
_open () {
if (this._connection) {
return this._connection
} else {
this._connection = new Bluebird((resolve, reject) => {
const socket = new WebSocket(adaptUrl(getCurrentUrl()))
const middle = new EventEmitter()
this.isConnected = true
const timeout = $timeout(() => {
middle.emit('reconnect_failed')
}, 4000)
socket.onmessage = ({data}) => {
const message = parse(data)
if (message.type === 'response' && message.id !== undefined) {
if (calls[message.id]) {
if (message.result) {
calls[message.id].resolve(message.result)
} else {
calls[message.id].reject(message.error)
}
delete calls[message.id]
}
} else {
middle.emit(message.method, message.params)
}
}
socket.onclose = () => {
middle.emit('disconnect')
}
middle.on('connected', ({message}) => {
$timeout.cancel(timeout)
this.log('success', message)
this.state = 'connected'
resolve(socket)
if (!this.updating) {
this.update()
}
this.emit('connected', message)
})
middle.on('print', ({content}) => {
Array.isArray(content) || (content = [content])
content.forEach(elem => this.log('info', elem))
this.emit('print', content)
})
middle.on('end', end => {
this._lowState = end
switch (this._lowState.state) {
case 'xoa-up-to-date':
case 'xoa-upgraded':
case 'updater-upgraded':
this.state = 'upToDate'
break
case 'xoa-upgrade-needed':
case 'updater-upgrade-needed':
this.state = 'upgradeNeeded'
break
case 'register-needed':
this.state = 'registerNeeded'
break
case 'error':
this.state = 'error'
break
default:
this.state = null
}
this.log(end.level, end.message)
this._lastRun = Date.now()
this.upgrading = this.updating = false
this.emit('end', end)
if (this._lowState.state === 'updater-upgraded') {
this.update()
} else if (this._lowState.state === 'xoa-upgraded') {
this._promptForReload()
}
this.xoaState()
})
middle.on('warning', warning => {
this.log('warning', warning.message)
this.emit('warning', warning)
})
middle.on('server-error', error => {
this.log('error', error.message)
this._lowState = error
this.state = 'error'
this.upgrading = this.updating = false
this.emit('error', error)
})
middle.on('disconnect', () => {
this._lowState = null
this.state = null
this.upgrading = this.updating = false
this.log('warning', 'Lost connection with xoa-updater')
this.emit('disconnect')
middle.emit('reconnect_failed') // No reconnecting attempts implemented so far
})
middle.on('reconnect_failed', () => {
this.isConnected = false
middle.removeAllListeners()
socket.close()
this._connection = null
const message = 'xoa-updater could not be reached'
this._xoaStateError({message})
reject(new Error(message))
this.log('error', message)
this.emit('reconnect_failed')
})
})
return this._connection
}
}
isRegistered () {
return this._open()
.then(socket => {
return jsonRpcCall(socket, 'isRegistered')
.then(token => {
if (token.registrationToken === undefined) {
throw new NotRegistered('Your Xen Orchestra Appliance is not registered')
} else {
this.registerState = 'registered'
this.registerError = ''
this.token = token
return token
}
})
})
.catch(NotRegistered, () => {
this.registerState = 'unregistered'
this.registerError = ''
})
.catch(error => {
this.registerError = error.message
this.registerState = 'error'
})
}
register (email, password) {
return this._open()
.then(socket => {
return jsonRpcCall(socket, 'register', {email, password})
.then(token => {
this.registerState = 'registered'
this.token = token
return token
})
})
.catch(error => {
if (error.code && error.code === 1) {
this.registerError = 'Authentication failed'
throw new AuthenticationFailed('Authentication failed')
} else {
this.registerError = error.message
this.registerState = 'error'
throw error
}
})
}
xoaState () {
return this._open()
.then(socket => {
return jsonRpcCall(socket, 'xoaState')
.then(state => {
this._xoaState = state
this._xoaStateTS = Date.now()
return state
})
})
.catch(error => this._xoaStateError(error))
}
_xoaStateError (error) {
this._xoaState = {
state: 'ERROR',
message: error.message
}
this._xoaStateTS = Date.now()
return this._xoaState
}
_update (upgrade = false) {
return this._open()
.tap(() => this.log('info', 'Start ' + (upgrade ? 'upgrading' : 'updating' + '...')))
.then(socket => jsonRpcNotify(socket, 'update', {upgrade}))
}
start () {
if (!this._xoaState) {
this.xoaState()
}
if (!this._interval) {
this._interval = $interval(() => this.run(), 60 * 60 * 1000)
return this.run()
} else {
return Bluebird.resolve()
}
}
stop () {
if (this._interval) {
$interval.cancel(this._interval)
delete this._interval
}
}
run () {
if (Date.now() - this._lastRun < 24 * 60 * 60 * 1000) {
return Bluebird.resolve()
} else {
return this.update()
}
}
isStarted () {
return this._interval !== null
}
log (level, message) {
const date = new Date()
this._log.unshift({
date: date.toLocaleString(),
level,
message
})
while (this._log.length > 10) {
this._log.pop()
}
}
getConfiguration () {
return this._open()
.then(socket => {
return jsonRpcCall(socket, 'getConfiguration')
.then(configuration => this._configuration = configuration)
})
}
configure (config) {
return this._open()
.then(socket => {
return jsonRpcCall(socket, 'configure', config)
.then(configuration => this._configuration = configuration)
})
}
}
return new Updater()
})
.run(function (updater, $rootScope, $state, xoApi) {
updater.start()
.catch(() => {})
$rootScope.$on('$stateChangeStart', function (event, state) {
if (Date.now() - updater._xoaStateTS > (60 * 60 * 1000)) {
updater.xoaState()
}
let {user} = xoApi
let loggedIn = !!user
if (!loggedIn || !updater._xoaState || state.name === 'settings.update') {
return
} else if (blockXoaAccess(updater._xoaState)) {
event.preventDefault()
$state.go('settings.update')
}
})
})
.name

View File

@@ -0,0 +1,19 @@
.modal-header
button.close(
type = 'button',
ng-click = '$dismiss()'
)
span(aria-hidden = 'true') &times;
h4.modal-title
i.fa.fa-bell.text-info
| &nbsp;Upgrade successful
.modal-body
p Your XOA has successfully upgraded, and your browser must reload the application.
button.btn.btn-primary(type = 'button', ng-click = '$close()')
i.fa.fa-repeat
| &nbsp;Reload now
| &ensp;
button.btn.btn-default(type = 'button', ng-click = '$dismiss()')
i.fa.fa-hand-paper-o
| &nbsp;No, I'll reload myself later (F5)

773
app/modules/vm/index.coffee Normal file
View File

@@ -0,0 +1,773 @@
angular = require 'angular'
filter = require 'lodash.filter'
forEach = require 'lodash.foreach'
isEmpty = require 'lodash.isempty'
sortBy = require 'lodash.sortby'
#=====================================================================
module.exports = angular.module 'xoWebApp.vm', [
require 'angular-ui-router',
require 'angular-ui-bootstrap'
require 'iso-device'
require 'tag'
]
.config ($stateProvider) ->
$stateProvider.state 'VMs_view',
url: '/vms/:id'
controller: 'VmCtrl'
template: require './view'
.controller 'VmCtrl', (
$scope, $state, $stateParams, $location, $q
xoApi, xo
sizeToBytesFilter, bytesToSizeFilter, xoHideUnauthorizedFilter
modal
$window
$timeout
dateFilter
notify
) ->
$window.bytesToSize = bytesToSizeFilter # FIXME dirty workaround to custom a Chart.js tooltip template
{get} = xoApi
pool = null
host = null
vm = null
do (
networksByPool = xoApi.getIndex('networksByPool')
srsByContainer = xoApi.getIndex('srsByContainer')
poolSrs = null
hostSrs = null
) ->
Object.defineProperties($scope, {
networks: {
get: () => pool && networksByPool[pool.id]
}
})
updateSrs = () =>
srs = []
poolSrs and forEach(poolSrs, (sr) => srs.push(sr))
hostSrs and forEach(hostSrs, (sr) => srs.push(sr))
srs = xoHideUnauthorizedFilter(srs)
$scope.writable_SRs = filter(srs, (sr) => sr.content_type isnt 'iso')
$scope.SRs = srs
vm and prepareDiskData()
$scope.$watchCollection(
() => pool and srsByContainer[pool.id],
(srs) =>
poolSrs = srs
updateSrs()
)
$scope.$watchCollection(
() => host and srsByContainer[host.id],
(srs) =>
hostSrs = srs
updateSrs()
)
$scope.$watchCollection(
() => vm and vm.$VBDs,
(vbds) =>
return unless vbds?
prepareDiskData()
)
$scope.currentLogPage = 1
$scope.currentSnapPage = 1
$scope.currentPCIPage = 1
$scope.currentGPUPage = 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.VM.id)
.then () => this._reset()
.catch (err) =>
if !this.running || this.attempt >= 2 || $scope.VM.power_state isnt 'Running' || $scope.isVMWorking($scope.VM)
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.hosts = xoApi.getView('hosts')
$scope.$watch(
-> get $stateParams.id, 'VM'
(VM) ->
$scope.VM = vm = VM
return unless VM?
# For the edition of this VM.
$scope.memorySize = bytesToSizeFilter VM.memory.size
$scope.bootParams = parseBootParams($scope.VM.boot.order)
$scope.prepareVDIs()
container = get VM.$container
if container.type is 'host'
host = container
pool = (get container.$poolId) ? {}
else
host = {}
pool = container
if VM.power_state is 'Running' && !($scope.isVMWorking($scope.VM))
refreshStatControl.start()
else
refreshStatControl.stop()
)
$scope.prepareVDIs = () ->
return unless $scope.VM
# build VDI list of this VM
VDIs = []
for VBD in $scope.VM.$VBDs
oVbd = get VBD
continue unless oVbd
oVdi = get oVbd.VDI
continue unless oVdi
VDIs.push oVdi if oVdi and not oVbd.is_cd_drive
$scope.VDIs = sortBy(VDIs, (value) -> (get resolveVBD(value))?.position);
descriptor = (obj) ->
if !obj
return ''
return obj.name_label + (if obj.name_description.length then ' - ' + obj.name_description else '')
prepareDiskData = () ->
# For populating adding position choice
unfreePositions = [];
maxPos = 0;
# build VDI list of this VM
for VBD in $scope.VM.$VBDs
oVbd = get VBD
oVdi = get oVbd?.VDI
if oVdi?
unfreePositions.push parseInt oVbd.position
maxPos = if (oVbd.position > maxPos) then parseInt oVbd.position else maxPos
$scope.maxPos = maxPos
VDIOpts = []
authSRs = xoHideUnauthorizedFilter($scope.SRs)
for SR in authSRs
if 'iso' isnt SR.SR_type
for rVdi in SR.VDIs
oVdi = get rVdi
if oVdi
VDIOpts.push({
sr: descriptor(SR),
label: descriptor(oVdi),
vdi: oVdi
})
$scope.VDIOpts = VDIOpts
parseBootParams = (params) ->
texts = {
c: 'Hard-Drive',
d: 'DVD-Drive',
n: 'Network'
}
bootParams = []
i = 0
if params
while (i < params.length)
char = params.charAt(i++)
bootParams.push({
e: char,
t: texts[char],
v: true
})
delete texts[char]
for key, text of texts
bootParams.push({
e: key,
t: text,
v: false
})
return bootParams
$scope.bootMove = (index, move) ->
tmp = $scope.bootParams[index + move]
$scope.bootParams[index + move] = $scope.bootParams[index]
$scope.bootParams[index] = tmp
$scope.saveBootParams = (id, bootParams) ->
if $scope.savingBootOrder
return
$scope.savingBootOrder = true
paramString = ''
forEach(bootParams, (boot) -> boot.v && paramString += boot.e)
return xoApi.call 'vm.bootOrder', {vm: id, order: paramString}
.finally () ->
$scope.savingBootOrder = false
$scope.bootReordering = false
$scope.refreshStats = (id) ->
return xo.vm.refreshStats id
.then (result) ->
result.cpuSeries = []
forEach result.cpus, (v,k) ->
result.cpuSeries.push 'CPU ' + k
return
result.vifSeries = []
forEach result.vifs, (v,k) ->
result.vifSeries.push '#' + Math.floor(k/2) + ' ' + if k % 2 then 'out' else 'in'
return
result.xvdSeries = []
forEach result.xvds, (v,k) ->
# 97 is ascii code of 'a'
result.xvdSeries.push 'xvd' + String.fromCharCode(Math.floor(k/2) + 97, ) + ' ' + if k % 2 then 'write' else 'read'
return
forEach result.date, (v,k) ->
result.date[k] = new Date(v*1000).toLocaleTimeString()
$scope.stats = result
$scope.startVM = (id) ->
xo.vm.start id
notify.info {
title: 'VM starting...'
message: 'Start VM'
}
$scope.stopVM = (id) ->
modal.confirm
title: 'VM shutdown'
message: 'Are you sure you want to shutdown this VM ?'
.then ->
xo.vm.stop id
notify.info {
title: 'VM shutdown...'
message: 'Gracefully shutdown the VM'
}
$scope.force_stopVM = (id) ->
modal.confirm
title: 'VM force shutdown'
message: 'Are you sure you want to force shutdown for this VM ?'
.then ->
xo.vm.stop id, true
notify.info {
title: 'VM force shutdown...'
message: 'Force shutdown the VM'
}
$scope.rebootVM = (id) ->
modal.confirm
title: 'VM reboot'
message: 'Are you sure you want to reboot this VM ?'
.then ->
xo.vm.restart id
notify.info {
title: 'VM reboot...'
message: 'Gracefully reboot the VM'
}
$scope.force_rebootVM = (id) ->
modal.confirm
title: 'VM reboot'
message: 'Are you sure you want to force reboot for this VM ?'
.then ->
xo.vm.restart id, true
notify.info {
title: 'VM reboot...'
message: 'Force reboot the VM'
}
$scope.suspendVM = (id) ->
modal.confirm
title: 'VM suspend'
message: 'Are you sure you want to suspend this VM ?'
.then ->
xo.vm.suspend id
notify.info {
title: 'VM suspend...'
message: 'Suspend the VM'
}
$scope.resumeVM = (id) ->
xo.vm.resume id, true
notify.info {
title: 'VM resume...'
message: 'Resume the VM'
}
$scope.migrateVM = (id, hostId) ->
modal.confirm
title: 'VM migrate'
message: 'Are you sure you want to migrate this VM?'
.then ->
xo.vm.migrate id, hostId
$scope.destroyVM = (id) ->
modal.confirm
title: 'VM deletion'
message: 'Are you sure you want to delete this VM? (including its disks)'
.then ->
# FIXME: provides a way to not delete its disks.
xo.vm.delete id, true
.then ->
$state.go 'index'
notify.info {
title: 'VM deletion'
message: 'VM is removed'
}
$scope.saveSnapshot = (id, $data) ->
snapshot = get (id)
result = {
id: snapshot.id
name_label: $data
}
if $data isnt snapshot.name_label
result.name_label = $data
xoApi.call 'vm.set', result
$scope.saveVM = ($data) ->
{VM} = $scope
{CPUs, memory, name_label, name_description, high_availability, auto_poweron, PV_args} = $data
$data = {
id: VM.id
}
if memory isnt $scope.memorySize and (memory = sizeToBytesFilter memory)
$data.memory = memory
$scope.memorySize = bytesToSizeFilter memory
if CPUs isnt VM.CPUs.number
$data.CPUs = +CPUs
if name_label isnt VM.name_label
$data.name_label = name_label
if name_description isnt VM.name_description
$data.name_description = name_description
if high_availability isnt VM.high_availability
$data.high_availability = high_availability
if auto_poweron isnt VM.auto_poweron
$data.auto_poweron = auto_poweron
if PV_args isnt VM.PV_args
$data.PV_args = PV_args
xoApi.call 'vm.set', $data
#-----------------------------------------------------------------
# Disks
#-----------------------------------------------------------------
$scope.moveDisk = (index, direction) ->
{VDIs} = $scope
newIndex = index + direction
[VDIs[index], VDIs[newIndex]] = [VDIs[newIndex], VDIs[index]]
return
migrateDisk = (id, sr_id) ->
return modal.confirm({
title: 'Disk migration'
message: 'Are you sure you want to migrate (move) this disk to another SR?'
}).then ->
notify.info {
title: 'Disk migration'
message: 'Disk migration started'
}
xo.vdi.migrate id, sr_id
return
$scope.saveDisks = (data) ->
# Group data by disk.
disks = {}
forEach data, (value, key) ->
i = key.indexOf '/'
(disks[key.slice 0, i] ?= {})[key.slice i + 1] = value
return
promises = []
# Handle SR change.
forEach disks, (attributes, id) ->
disk = get id
if attributes.$SR isnt disk.$SR
promises.push (migrateDisk id, attributes.$SR)
return
forEach disks, (attributes, id) ->
# Keep only changed attributes.
disk = get id
forEach attributes, (value, name) ->
delete attributes[name] if value is disk[name]
return
unless isEmpty attributes
# Inject id.
attributes.id = id
# Ask the server to update the object.
promises.push xoApi.call 'vdi.set', attributes
return
# Handle Position changes
vbds = xoApi.get($scope.VM.$VBDs)
notFreePositions = Object.create(null)
forEach vbds, (vbd) ->
if vbd.is_cd_drive
notFreePositions[vbd.position] = null
position = 0
forEach $scope.VDIs, (vdi) ->
oVbd = get(resolveVBD(vdi))
unless oVbd
return
while position of notFreePositions
++position
if +oVbd.position isnt position
promises.push(
xoApi.call('vbd.set', {
id: oVbd.id,
position: String(position)
})
)
++position
return $q.all promises
.catch (err) ->
console.log(err);
notify.error {
title: 'saveDisks'
message: err
}
$scope.deleteDisk = (id) ->
modal.confirm({
title: 'Disk deletion'
message: 'Are you sure you want to delete this disk? This operation is irreversible'
}).then ->
xoApi.call 'vdi.delete', {id: id}
return
return
#-----------------------------------------------------------------
# returns the id of the VBD that links the VDI to the VM
$scope.resolveVBD = resolveVBD = (vdi) ->
if not vdi?
return
for vbd in vdi.$VBDs
rVbd = vbd if (get vbd).VM is $scope.VM.id
return rVbd || null
$scope.disconnectVBD = (vdi) ->
id = resolveVBD(vdi)
if id?
modal.confirm({
title: 'VBD disconnection'
message: 'Are you sure you want to detach this VM disk ?'
}).then ->
console.log "Disconnect VBD #{id}"
xo.vbd.disconnect id
$scope.connectVBD = (vdi) ->
id = resolveVBD(vdi)
if id?
console.log "Connect VBD #{id}"
xo.vbd.connect id
$scope.deleteVBD = (vdi) ->
id = resolveVBD(vdi)
if id?
modal.confirm({
title: 'VBD deletion'
message: 'Are you sure you want to delete this VM disk attachment (the disk will NOT be destroyed)?'
}).then ->
console.log "Delete VBD #{id}"
xo.vbd.delete id
$scope.connectVIF = (id) ->
console.log "Connect VIF #{id}"
xo.vif.connect id
$scope.disconnectVIF = (id) ->
modal.confirm
title: 'Disconnect VIF'
message: 'Are you sure you want to disconnect this interface ?'
.then ->
console.log "Disconnect VIF #{id}"
xo.vif.disconnect id
$scope.deleteVIF = (id) ->
console.log "Delete VIF #{id}"
modal.confirm({
title: 'VIF deletion'
message: 'Are you sure you want to delete this Virtual Interface (VIF)?'
}).then ->
xo.vif.delete id
$scope.cloneVM = (id, vm_name, full_copy) ->
clone_name = "#{vm_name}_clone"
console.log "Copy VM #{id} #{clone_name} with full copy at #{full_copy}"
notify.info {
title: 'Clone creation'
message: 'Clone creation started'
}
xo.vm.clone id, clone_name, full_copy
$scope.snapshotVM = (id, vm_name) ->
date = dateFilter Date.now(), 'yyyy-MM-ddTHH:mmZ'
snapshot_name = "#{vm_name}_#{date}"
console.log "Snapshot #{snapshot_name} from VM #{id}"
notify.info {
title: 'Snapshot creation'
message: 'Snapshot creation started'
}
xo.vm.createSnapshot id, snapshot_name
$scope.exportVM = (id) ->
console.log "Export VM #{id}"
notify.info {
title: 'VM export'
message: 'VM export started'
}
xo.vm.export id
.then ({$getFrom: url}) ->
window.open url
$scope.exportOnlyMetadataVM = (id) ->
console.log "Export Metadata only for VM #{id}"
notify.info {
title: 'VM export'
message: 'VM export started'
}
xo.vm.export id, true, true
.then ({$getFrom: url}) ->
window.open url
$scope.convertVM = (id) ->
console.log "Convert VM #{id}"
modal.confirm({
title: 'VM to template'
message: 'Are you sure you want to convert this VM into a template?'
}).then ->
xo.vm.convert id
$scope.deleteSnapshot = (id) ->
console.log "Delete snapshot #{id}"
modal.confirm({
title: 'Snapshot deletion'
message: 'Are you sure you want to delete this snapshot? (including its disks)'
}).then ->
# FIXME: provides a way to not delete its disks.
xo.vm.delete id, true
$scope.connectPci = (id, pciId) ->
console.log "Connect PCI device "+pciId+" on VM "+id
xo.vm.connectPci id, pciId
$scope.disconnectPci = (id) ->
xo.vm.disconnectPci id
$scope.deleteAllLog = ->
modal.confirm({
title: 'Log deletion'
message: 'Are you sure you want to delete all the logs?'
}).then ->
forEach($scope.VM.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.revertSnapshot = (id) ->
console.log "Revert snapshot to #{id}"
modal.confirm({
title: 'Revert to snapshot'
message: 'Are you sure you want to revert your VM to this snapshot? The VM will be halted and this operation is irreversible'
}).then ->
notify.info {
title: 'Reverting to snapshot'
message: 'VM revert started'
}
xo.vm.revert id
$scope.isVMWorking = (VM) ->
return false unless VM
return true for _ of VM.current_operations
false
$scope.startContainer = (VM,container) ->
console.log "Start from VM "+VM+" to container "+container
xo.docker.start VM, container
$scope.stopContainer = (VM,container) ->
console.log "Stop from VM "+VM+" to container "+container
xo.docker.stop VM, container
$scope.restartContainer = (VM,container) ->
console.log "Restart from VM "+VM+" to container "+container
xo.docker.restart VM, container
$scope.pauseContainer = (VM,container) ->
console.log "Pause from VM "+VM+" to container "+container
xo.docker.pause VM, container
$scope.resumeContainer = (VM,container) ->
console.log "Unpause from VM "+VM+" to container "+container
xo.docker.unpause VM, container
$scope.addVdi = (vdi, readonly, bootable) ->
$scope.addWaiting = true # disables form fields
position = $scope.maxPos + 1
mode = if (readonly || !isFreeForWriting(vdi)) then 'RO' else 'RW'
return xo.vm.attachDisk $scope.VM.id, vdi.id, bootable, mode, String(position)
.then -> $scope.adding = false # Closes form block
.catch (err) ->
console.log(err);
notify.error {
title: 'vm.attachDisk'
message: err
}
.finally ->
$scope.addWaiting = false
$scope.isConnected = isConnected = (vdi) -> (get resolveVBD(vdi))?.attached
$scope.isFreeForWriting = isFreeForWriting = (vdi) ->
free = true
for vbd in vdi.$VBDs
oVbd = get vbd
free = free && (!oVbd?.attached || oVbd?.read_only)
return free
$scope.createVdi = (name, size, sr, bootable, readonly) ->
$scope.createVdiWaiting = true # disables form fields
position = $scope.maxPos + 1
return xo.disk.create name, String(size), sr
.then (diskUuid) ->
mode = if readonly then 'RO' else 'RW'
return xo.vm.attachDisk $scope.VM.id, diskUuid, bootable, mode, String(position)
.then -> $scope.creatingVdi = false # Closes form block
.catch (err) ->
console.log(err);
notify.error {
title: 'Attach Disk'
message: err
}
.catch (err) ->
console.log(err);
notify.error {
title: 'Create Disk'
message: err
}
.finally ->
$scope.createVdiWaiting = false
$scope.updateMTU = (network) ->
$scope.newInterfaceMTU = network.MTU
$scope.createInterface = (network, mtu, automac, mac) ->
$scope.createVifWaiting = true # disables form fields
position = 0
forEach $scope.VM.VIFs, (vf) ->
int = get vf
position = if int?.device > position then (get vf)?.device else position
position++
mtu = String(mtu || network.mtu)
mac = if automac then undefined else mac
return xo.vm.createInterface $scope.VM.id, network.id, String(position), mtu, mac
.then (id) ->
$scope.creatingVif = false
.catch (err) ->
console.log(err);
notify.error {
title: 'Create Interface'
message: err
}
.finally ->
$scope.createVifWaiting = false
$scope.statView = {
cpuOnly: false,
ramOnly: false,
netOnly: false,
diskOnly: false
}
$scope.canAdmin = (id = undefined) ->
if id == undefined
id = $scope.VM && $scope.VM.id
return id && xoApi.canInteract(id, 'admin') || false
$scope.canOperate = (id = undefined) ->
if id == undefined
id = $scope.VM && $scope.VM.id
return id && xoApi.canInteract(id, 'operator') || false
$scope.canView = (id = undefined) ->
if id == undefined
id = $scope.VM && $scope.VM.id
return id && xoApi.canInteract(id, 'viewer') || false
# A module exports its name.
.name

809
app/modules/vm/view.jade Normal file
View File

@@ -0,0 +1,809 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-vm(ng-if="isVMWorking(VM)", class="xo-color-pending", tooltip="{{VM.power_state}} and {{(VM.current_operations | map)[0]}}")
i.xo-icon-vm(class="xo-color-{{VM.power_state | lowercase}}",ng-if="!isVMWorking(VM)", tooltip="{{VM.power_state}}")
| {{VM.name_label}}
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| General
span.quick-edit(tooltip="Edit General settings", ng-click="vmSettings.$show()", ng-if = '!vmSettings.$visible && canAdmin()')
i.fa.fa-edit.fa-fw
span.quick-edit(ng-if="vmSettings.$visible", tooltip="Cancel Edition", ng-click="vmSettings.$cancel()")
i.fa.fa-undo.fa-fw
.panel-body
form(editable-form="", name="vmSettings", onbeforesave="saveVM($data)")
dl.dl-horizontal
dt Name
dd
span(editable-text="VM.name_label", e-name="name_label", e-form="vmSettings")
| {{VM.name_label}}
dt Description
dd
span(editable-text="VM.name_description", e-name="name_description", e-form="vmSettings")
| {{VM.name_description}}
dt(ng-if="VM.power_state === 'Running' || VM.power_state === 'Paused'") Running on
dt(ng-if="VM.power_state == 'Halted' || VM.power_state === 'Suspended'") Resident on
dd(ng-repeat="container in [VM.$container] | resolve")
span(ng-if = 'container.type === "host" && canView(container.id)')
a(xo-sref="hosts_view({id: container.id})")
| {{container.name_label}}
small
span(ng-if="(container.$poolId | resolve).name_label")
| (
a(ui-sref="pools_view({id: (container.$poolId | resolve).id})") {{(container.$poolId | resolve).name_label}}
| )
a(
ng-if = 'container.type === "pool" && canView(container.id)'
xo-sref="pools_view({id: container.id})"
)
| {{container.name_label}}
dt Tags
dd
xo-tag(ng-if = 'VM', object = 'VM')
dt Addresses
dd(ng-if="!VM.addresses") -
dd(ng-repeat="IP in VM.addresses") {{IP}}
dt(ng-if="!(VM.$poolId | resolve).HA_enabled") Auto Power
dd(ng-if="!(VM.$poolId | resolve).HA_enabled")
span(
editable-select="VM.auto_poweron"
e-ng-options="ap.v as ap.t for ap in [{v: true, t:'Yes'}, {v: false, t:'No'}]"
e-name="auto_poweron"
e-form="vmSettings"
)
| {{VM.auto_poweron ? 'Yes' : 'No'}}
dt(ng-if="(VM.$poolId | resolve).HA_enabled") HA
dd(ng-if="(VM.$poolId | resolve).HA_enabled")
span(
editable-checkbox="VM.high_availability"
e-name="high_availability"
e-form="vmSettings"
)
| {{VM.high_availability}}
dt vCPUs
dd
span(
editable-text="VM.CPUs.number"
e-name="CPUs"
e-form="vmSettings"
)
| {{VM.CPUs.number}}
dt RAM
dd
span(
editable-text="memorySize"
e-name="memory"
e-form="vmSettings"
)
| {{memorySize}}
dt UUID
dd {{VM.UUID}}
dt(ng-if="VM.PV_args") PV Args
dd(ng-if="VM.PV_args")
span(editable-text="VM.PV_args", e-name="PV_args", e-form="vmSettings")
| {{VM.PV_args}}
dt(ng-if="refreshStatControl.running && stats") Xen tools
dd(ng-if="refreshStatControl.running && stats")
span.text-success(ng-if="VM.PV_drivers && VM.PV_drivers_up_to_date") Installed
span(ng-if="!VM.PV_drivers") NOT installed
span.text-warning(ng-if="VM.PV_drivers && !VM.PV_drivers_up_to_date") Outdated
dt(ng-if="refreshStatControl.running && stats && VM.os_version") OS
dd(ng-if="refreshStatControl.running && stats && VM.os_version")
| {{VM.os_version.name}} ({{VM.os_version.distro}})
dt(ng-if="refreshStatControl.running && stats && VM.os_version") Kernel
dd(ng-if="refreshStatControl.running && stats && VM.os_version")
| {{VM.os_version.uname}}
.btn-form(ng-show="vmSettings.$visible")
p.center
button.btn.btn-default(
type="button"
ng-disabled="vmSettings.$waiting"
ng-click="vmSettings.$cancel()"
)
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(
type="submit"
ng-disabled="vmSettings.$waiting"
)
i.fa.fa-save
| Save
.panel.panel-default.panel-height.center
.panel-heading.panel-title
i.xo-icon-stats(style="color: #e25440;", xo-click="refreshStats(VM.id)")
| Stats
.panel-body-stats(ng-if="refreshStatControl.running && stats")
div(ng-if="statView.cpuOnly", ng-click="statView.cpuOnly = false")
p.stat-name
i.xp-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.vifs"
labels="stats.date"
series="stats.vifSeries"
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.diskOnly", ng-click="statView.diskOnly = false")
p.stat-name
i.xo-icon-disk
| &nbsp; Disk I/O
canvas.chart.chart-line.chart-stat-full(
id="bigDisk"
data="stats.xvds"
labels="stats.date"
series="stats.xvdSeries"
colours="['#00dd00', '#77dd77', '#007700', '#33dd33', '#003300']"
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.diskOnly && !statView.cpuOnly && !statView.ramOnly")
.row
.col-sm-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-sm-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-sm-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.vifs"
labels="stats.date"
series="stats.vifSeries"
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.col-sm-6(ng-click="statView.diskOnly=true")
p.stat-name
i.xo-icon-disk
| &nbsp; Disk I/O
canvas.chart.chart-line.chart-stat-preview(
id="smallDisk"
data="stats.xvds"
labels="stats.date"
series="stats.xvdSeries"
colours="['#00dd00', '#77dd77', '#007700', '#33dd33', '#003300']"
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")
.grid
.grid-cell
p.stat-name vCPUs
p.center.big {{VM.CPUs.number}}
.grid-cell
p.stat-name RAM
p.center.big {{VM.memory.size | bytesToSize}}
.grid-cell
p.stat-name Disks
p.center.big {{VM.$VBDs.length || 0}}
br
p.center(ng-if="refreshStatControl.running")
i.xo-icon-loading
| &nbsp; Fetching stats...
.grid
.grid-cell(ng-if="VM.os_version.distro")
p.stat-name OS:
p.center.big
i(class="xo-icon-{{VM.os_version.distro | osFamily}}",tooltip="{{VM.os_version.name}}", style="color: black;")
.grid-cell
p.stat-name Xen tools:
p.center
span(ng-if="VM.PV_drivers", style="color:green;") Installed
span(ng-if="!VM.PV_drivers") NOT installed
span(ng-if="VM.PV_drivers && !VM.PV_drivers_up_to_date") Outdated
//- Action panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flash
| Actions
.panel-body.text-center
.grid-sm.grid--gutters
.grid.grid-cell
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
button.btn(tooltip="Stop VM", type="button", style="width: 90%", xo-click="stopVM(VM.id)", ng-if = 'canOperate()')
i.fa.fa-stop.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="VM.power_state == ('Running')")
button.btn(tooltip="Suspend VM", type="button", style="width: 90%", xo-click="suspendVM(VM.id)", ng-if = 'canOperate()')
i.fa.fa-pause.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
button.btn(tooltip="Start VM", type="button", style="width: 90%", xo-click="startVM(VM.id)", ng-if = 'canOperate()')
i.fa.fa-play.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
button.btn(tooltip="Reboot VM", type="button", style="width: 90%", xo-click="rebootVM(VM.id)", ng-if = 'canOperate()')
i.fa.fa-refresh.fa-2x.fa-fw
.grid-cell.btn-group.dropdown(
ng-if="VM.power_state == ('Running' || 'Paused')"
dropdown
)
button.btn.dropdown-toggle(
ng-if = 'canAdmin()'
dropdown-toggle
tooltip="Migrate VM"
type="button"
style="width: 90%"
)
i.fa.fa-share.fa-2x.fa-fw
span.caret
ul.dropdown-menu.left(role="menu", ng-if = 'canAdmin()')
li(ng-repeat="host in hosts.all | orderBy:natural('name_label') track by host.id")
a(xo-click="migrateVM(VM.id, host.id)")
i.xo-icon-host.fa-fw
| To {{host.name_label}}
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
button.btn(tooltip="Force Reboot", type="button", style="width: 90%", xo-click="force_rebootVM(VM.id)", ng-if = 'canOperate()')
i.fa.fa-flash.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
button.btn(tooltip="Delete VM", type="button", style="width: 90%", xo-click="destroyVM(VM.id)", ng-if = 'canAdmin()')
i.fa.fa-trash-o.fa-2x.fa-fw
.grid-cell.btn-group.dropdown(
ng-if="VM.power_state == ('Halted')"
dropdown
)
button.btn.dropdown-toggle(
ng-if = 'canAdmin()'
dropdown-toggle
tooltip="Create a clone"
style="width: 90%"
type="button"
)
i.fa.fa-files-o.fa-2x.fa-fw
span.caret
ul.dropdown-menu.left(role="menu")
li(ng-if = 'canAdmin()')
a(xo-click="cloneVM(VM.id,VM.name_label,false)")
i.fa.fa-code-fork.fa-fw
| Fast clone
li(ng-if = 'canAdmin()')
a(xo-click="cloneVM(VM.id,VM.name_label,true)")
i.xo-icon-disk.fa-fw
| Full disk copy
.grid-cell.btn-group(ng-if="VM.power_state == ('Halted')")
button.btn(tooltip="Convert to template", type="button", style="width: 90%", xo-click="convertVM(VM.id)", ng-if = 'canAdmin()')
i.fa.fa-thumb-tack.fa-2x.fa-fw
.grid.grid-cell
.grid-cell.btn-group(ng-if="VM.power_state == ('Running' || 'Paused')")
button.btn(tooltip="Force Shutdown", type="button", style="width: 90%", xo-click="force_stopVM(VM.id)", ng-if = 'canOperate()')
i.fa.fa-power-off.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="VM.power_state == ('Suspended')")
button.btn(tooltip="Resume VM", type="button", style="width: 90%", xo-click="resumeVM(VM.id)", ng-if = 'canOperate()')
i.fa.fa-play.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Create a snapshot", style="width: 90%", type="button", xo-click="snapshotVM(VM.id,VM.name_label)", ng-if = 'canAdmin()')
i.xo-icon-snapshot.fa-2x.fa-fw
.grid-cell.btn-group.dropdown(dropdown, ng-if="canOperate()")
button.btn.dropdown-toggle(
dropdown-toggle
tooltip="Export the VM"
style="width: 90%"
type="button"
)
i.fa.fa-download.fa-2x.fa-fw
span.caret
ul.dropdown-menu.left(role="menu")
li(ng-if = 'canOperate()')
a(xo-click="exportVM(VM.id)")
i.fa.fa-download.fa-fw
| Full export
li(ng-if = 'canOperate()')
a(xo-click="exportOnlyMetadataVM(VM.id)")
i.fa.fa-database.fa-fw
| Only Metadata
//- .grid-cell.btn-group
//- button.btn(tooltip="Export the VM", style="width: 90%", type="button", xo-click="exportOnlyMetadataVM(VM.id)")
//- i.fa.fa-download.fa-2x.fa-fw
.grid-cell.btn-group(style="margin-bottom: 0.5em")
button.btn(tooltip="VM Console", type="button", style="width: 90%", xo-sref="consoles_view({id: VM.id})")
i.xo-icon-console.fa-2x.fa-fw
//- Docker Panel (if Docker VM)
.grid-sm(ng-if="VM.docker")
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-docker
| Docker containers
.panel-body
p.text-center(ng-if="!VM.docker.process.item") No Docker container on this VM.
table.table.table-hover(ng-if="VM.docker.process.item")
tr
th.col-sm-2 Name
th.col-sm-6 Command
th.col-sm-2 Created
th.col-sm-2 Status
tr(ng-repeat = "container in VM.docker.process.item")
td.oneliner {{container.entry.names}}
td.oneliner {{container.entry.command}}
td.oneliner {{container.entry.created*1e3 | date:"medium"}}
td(ng-if="container.entry.status === 'Up'")
span.label.label-success Running
span.pull-right.btn-group.quick-buttons
a(
tooltip="Stop this container"
xo-click="stopContainer(VM.id,container.entry.container)"
)
i.fa.fa-stop
a(
tooltip="Pause this container"
xo-click="pauseContainer(VM.id,container.entry.container)"
)
i.fa.fa-pause
a(
tooltip="Restart this container"
xo-click="restartContainer(VM.id,container.entry.container)"
)
i.fa.fa-refresh
td(ng-if="container.entry.status === 'Up (Paused)'")
span.label.label-info Paused
span.pull-right.btn-group.quick-buttons
a(
tooltip="Resume this container"
xo-click="resumeContainer(VM.id,container.entry.container)"
)
i.fa.fa-play
td(ng-if="container.entry.status !== 'Up' && container.entry.status !== 'Up (Paused)'")
span.label.label-danger Halted
span.pull-right.btn-group.quick-buttons
a(
tooltip="Start this container"
xo-click="startContainer(VM.id,container.entry.container)"
)
i.fa.fa-play
//- Disk panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-disk
| Disk
span.quick-edit(
ng-if="!disksForm.$visible && canOperate()"
tooltip="Edit disks"
ng-click="disksForm.$show()"
)
i.fa.fa-edit.fa-fw
span.quick-edit(
ng-if="disksForm.$visible"
tooltip="Cancel Edition"
ng-click="disksForm.$cancel()"
)
i.fa.fa-undo.fa-fw
.panel-body
form(name = "disksForm", editable-form = '', onbeforesave = 'saveDisks($data)')
table.table.table-hover
tr
th Name
th Description
th Tags
th Size
th SR
th Status
th(ng-show="disksForm.$visible")
//- FIXME: ng-init seems to disrupt the implicit $watch.
tr(ng-repeat = 'VDI in VDIs track by VDI.id')
td.oneliner
span(
editable-text="VDI.name_label"
e-name = '{{VDI.id}}/name_label'
)
| {{VDI.name_label}}
td.oneliner
span(
editable-text="VDI.name_description"
e-name = '{{VDI.id}}/name_description'
)
| {{VDI.name_description}}
td
xo-tag(object = 'VDI')
td
//- FIXME: should be editable, but the server needs first
//- to accept a human readable string.
| {{VDI.size | bytesToSize}}
td.oneliner
span(
ng-if = 'canView((VDI.$SR | resolve).id)'
editable-select="(VDI.$SR | resolve).id"
e-ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in writable_SRs"
e-name = '{{VDI.id}}/$SR'
)
//- Are SR editable? will trigger moving VDI to the new SR
a(xo-sref="SRs_view({id: (VDI.$SR | resolve).id})")
| {{(VDI.$SR | resolve).name_label}}
td(ng-if="isConnected(VDI)")
span.label.label-success Connected
span.pull-right.btn-group.quick-buttons(ng-if="canAdmin()")
a(
tooltip="Disconnect this disk"
xo-click="disconnectVBD(VDI)"
)
i.fa.fa-unlink.fa-lg
td(ng-if="!isConnected(VDI)")
span.label.label-default Disconnected
span.pull-right.btn-group.quick-buttons(ng-if="canAdmin()")
a(
tooltip="Plug this disk"
ng-if="VM.power_state == ('Running' || 'Paused')"
xo-click="connectVBD(VDI)"
)
i.fa.fa-plug.fa-lg
a(
tooltip="Forget this disk"
xo-click="deleteVBD(VDI)"
)
i.fa.fa-ban.fa-lg
a(
tooltip="Remove this disk"
xo-click="deleteDisk(VDI.id)"
)
i.fa.fa-trash-o.fa-lg
td(ng-show="disksForm.$visible")
.btn-group
button.btn.btn-default(
type="button"
ng-click="moveDisk($index, -1)"
ng-disabled="$first"
title="Move this disk up"
)
i.fa.fa-chevron-up
button.btn.btn-default(
type="button"
ng-click="moveDisk($index, 1)"
ng-disabled="$last"
title="Move this disk down"
)
i.fa.fa-chevron-down
//- TODO: Ability to create new VDIs.
.btn-form(ng-show="disksForm.$visible")
p.center
button.btn.btn-default(
type="reset"
ng-disabled="disksForm.$waiting"
ng-click="disksForm.$cancel()"
)
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(
type="submit"
ng-disabled="disksForm.$waiting"
)
i.fa.fa-save
| Save
.container-fluid
.row
.col-sm-5
iso-device(ng-if = '(VM && SRs) && canOperate()', vm = 'VM', srs = 'SRs')
.col-sm-7.text-right
div
button.btn(type="button", ng-class = '{"btn-success": adding, "btn-primary": !adding}', ng-disabled="disksForm.$waiting", ng-click="adding = !adding;creatingVdi = false;bootReordering = false", ng-hide = '!canAdmin()')
i.fa.fa-plus(ng-if = '!adding')
i.fa.fa-minus(ng-if = 'adding')
| Attach Disk
| &nbsp;
button.btn(type="button", ng-class = '{"btn-success": creatingVdi, "btn-primary": !creatingVdi}', ng-disabled="disksForm.$waiting", ng-click="creatingVdi = !creatingVdi;adding = false;bootReordering = false", ng-hide = '!canAdmin()')
i.fa.fa-plus(ng-if = '!creatingVdi')
i.fa.fa-minus(ng-if = 'creatingVdi')
| New Disk
| &nbsp;
button.btn(type="button", ng-class = '{"btn-success": bootReordering, "btn-primary": !bootReordering}', ng-disabled="disksForm.$waiting", ng-click="bootReordering = !bootReordering;adding = false;creatingVdi = false", ng-hide = '!canAdmin()')
i.fa.fa-plus(ng-if = '!bootReordering')
i.fa.fa-minus(ng-if = 'bootReordering')
| Boot order
br
.text-right
form.form-inline#addDiskForm(ng-if = 'adding', name = 'addForm', ng-submit = 'addVdi(vdiToAdd.vdi, vdiReadOnly, vdiBootable)')
fieldset(ng-attr-disabled = '{{ addWaiting ? true : undefined }}')
.form-group
select#vdiToAdd.form-control(ng-model = 'vdiToAdd', ng-options = 'vdi.label group by vdi.sr for vdi in VDIOpts', required)
option(value = '', disabled) -- Choose disk --
| &nbsp;
.form-group(ng-if = 'vdiToAdd')
//- .form-group
label(for = 'vdiPosition') Position&nbsp;
select#vdiPosition.form-control(ng-model = '$parent.vdiPos', ng-options = 'vPos for vPos in vdiFreePos', required)
option(value = '', disabled) --
| &nbsp;
label
input(type='checkbox', ng-model = '$parent.$parent.vdiBootable')
| &nbsp;Bootable&nbsp;
label
input(ng-if = '!isFreeForWriting(vdiToAdd.vdi)', type='checkbox', ng-model = '$parent.$parent.vdiReadOnly', ng-checked = 'true', ng-disabled = 'true')
input(ng-if = 'isFreeForWriting(vdiToAdd.vdi)', type='checkbox', ng-model = '$parent.$parent.vdiReadOnly')
| &nbsp;Read-only&nbsp;
.form-group(ng-if = 'vdiToAdd')
button.btn.btn-primary(type = 'submit', ng-disabled="disksForm.$waiting", ng-if = 'canAdmin()')
| Add
span(ng-if = 'addWaiting')
| &nbsp;
i.xo-icon-loading-sm
br
form#createDiskForm(ng-if = 'creatingVdi', name = 'createForm', ng-submit = 'createVdi(newDiskName, newDiskSize, newDiskSR, newDiskBootable, newDiskReadonly)')
fieldset(ng-attr-disabled = '{{ createVdiWaiting ? true : undefined }}')
.form-inline
.form-group
//- label(for = 'newDiskName') Name&nbsp;
input#newDiskName.form-control(type = 'text', ng-model = 'newDiskName', placeholder = 'Disk Name', required)
| &nbsp;
.form-group
//- label(for = 'newDiskSize') Size&nbsp;
input#newDiskSize.form-control(type = 'text', ng-model = 'newDiskSize', required, placeholder = 'Size e.g 128MB, 8GB, 2TB...')
| &nbsp;
.form-group
//- label(for = 'newDiskSR') SR&nbsp;
select.form-control(ng-model = 'newDiskSR', required, ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in writable_SRs")
option(value = '', disabled) Choose your SR
| &nbsp;
br
.form-group
label
input(type='checkbox', ng-model = 'newDiskBootable')
| &nbsp;Bootable&nbsp;
label
input(type='checkbox', ng-model = 'newDiskReadonly')
| &nbsp;Read-only&nbsp;
//- .form-group
label(for = 'diskPosition') Position&nbsp;
select#diskPosition.form-control(ng-model = 'newDiskPosition', ng-options = 'vPos for vPos in vdiFreePos', required)
option(value = '', disabled) --
| &nbsp;
.form-group
button.btn.btn-primary(type = 'submit', ng-disabled="disksForm.$waiting", ng-if = 'canAdmin()')
i.fa.fa-plus-square
| &nbsp;Create
span(ng-if = 'createVdiWaiting')
| &nbsp;
i.xo-icon-loading-sm
br
form#bootOrderForm(ng-if = 'bootReordering', ng-submit = 'saveBootParams(VM.id, bootParams)')
fieldset(ng-disabled = 'savingBootOrder')
.form-group(ng-repeat = 'elem in bootParams')
label
span(ng-class = '{"text-muted": !elem.v}') {{ elem.t }}&nbsp;
input(type = 'checkbox', ng-model = 'elem.v')
| &nbsp;
button.btn.btn-default(type = 'button', ng-click = 'bootMove($index, -1)', ng-disabled = '$first'): i.fa.fa-chevron-up
| &nbsp;
button.btn.btn-default(type = 'button', ng-click = 'bootMove($index, 1)', ng-disabled = '$last'): i.fa.fa-chevron-down
.form-group
button.btn.btn-primary(type = 'submit', ng-disabled = 'disksForm.$waiting', ng-if = 'canAdmin()')
i.fa.fa-database
| &nbsp;Save
//- TODO: add interface in this panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-network
| Interface
.panel-body
table.table.table-hover
th Device
th MAC
th MTU
th Network
th Link status
tr(ng-repeat="VIF in VM.VIFs | resolve | orderBy:natural('device') track by VIF.id")
td VIF \#{{VIF.device}}
td
| {{VIF.MAC}}
td
| {{VIF.MTU}}
td.oneliner
span(ng-if = 'canView((VIF.$network | resolve).id)') {{(VIF.$network | resolve).name_label}}
td(ng-if="VIF.attached")
span.label.label-success Connected
span.pull-right.btn-group.quick-buttons
a(tooltip="Disconnect this interface", ng-if="VM.power_state == ('Running' || 'Paused') && canAdmin()", xo-click="disconnectVIF(VIF.id)")
i.fa.fa-unlink.fa-lg
td(ng-if="!VIF.attached")
span.label.label-default Disconnected
span.pull-right.btn-group.quick-buttons
a(tooltip="Connect this interface", xo-click="connectVIF(VIF.id)", ng-if = 'canAdmin()')
i.fa.fa-link.fa-lg
a(tooltip="Remove this interface", xo-click="deleteVIF(VIF.id)", ng-if = 'canAdmin()')
i.fa.fa-trash-o.fa-lg
.text-right
button.btn(type="button", ng-class = '{"btn-success": creatingVif, "btn-primary": !creatingVif}', ng-click="creatingVif = !creatingVif", ng-hide = '!canAdmin()')
i.fa.fa-plus(ng-if = '!creatingVif')
i.fa.fa-minus(ng-if = 'creatingVif')
| Create Interface
br
form.form-inline.text-right#createInterfaceForm(
ng-if = 'creatingVif'
name = 'createInterfaceForm'
ng-submit = 'createInterface(newInterfaceNetwork, newInterfaceMTU, autoMac, newInterfaceMAC)'
)
fieldset(ng-disabled = 'createVifWaiting')
.form-group
label(for = 'newVifNetwork') Network&nbsp;
select.form-control(
ng-options='network.name_label for network in networks | xoHideUnauthorized'
ng-model = 'newInterfaceNetwork'
ng-change = 'updateMTU(newInterfaceNetwork)'
required
)
option(value = '', disabled) --
| &nbsp;
.form-group
fieldset(ng-disabled = 'autoMac')
label.control-label(for = 'newInterfaceMAC') MAC address&nbsp;
input#newInterfaceMAC.form-control(ng-class = '{hidden: autoMac}', type = 'text', ng-model = 'newInterfaceMAC', ng-required = '!autoMac')
| &nbsp;
.checkbox
label
input(type='checkbox', ng-model = 'autoMac')
| Auto-generate &nbsp;
| &nbsp;
.form-group
label(for = 'newInterfaceMTU') MTU&nbsp;
input#newInterfaceMTU.form-control(type = 'text', ng-model = 'newInterfaceMTU', required)
| &nbsp;
.form-group
button.btn.btn-primary(type = 'submit', ng-if = 'canAdmin()')
i.fa.fa-plus-square
| &nbsp;Create
span(ng-if = 'createVifWaiting')
| &nbsp;
i.xo-icon-loading-sm
br
//- Snap panel
.grid-sm
form(editable-form="", name="vmSnap", oncancel="cancel()").panel.panel-default
.panel-heading.panel-title
i.xo-icon-snapshot
| Snapshots
span.quick-edit(tooltip="Edit snapshots", ng-click="vmSnap.$show()", ng-if="!vmSnap.$visible && canAdmin()")
i.fa.fa-edit.fa-fw
span.quick-edit(ng-if="vmSnap.$visible", tooltip="Cancel Edition", ng-click="vmSnap.$cancel()")
i.fa.fa-undo.fa-fw
.panel-body
p.center(ng-if="!VM.snapshots.length") No snapshots
table.table.table-hover(ng-if="VM.snapshots.length")
th Date
th Name
tr(ng-repeat="snapshot in VM.snapshots | resolve | orderBy:'-snapshot_time' | slice:(5*(currentSnapPage-1)):(5*currentSnapPage) track by snapshot.id")
td.oneliner {{snapshot.snapshot_time*1e3 | date:"medium"}}
td.oneliner
span(editable-text="snapshot.name_label", e-name="name_label", e-form="vmSnap", onbeforesave="saveSnapshot(snapshot.id, $data)")
| {{snapshot.name_label}}
span.pull-right.btn-group.quick-buttons
a(tooltip="Export this snapshot", type="button", xo-click="exportVM(snapshot.id)", ng-if = 'canAdmin()')
i.fa.fa-upload.fa-lg
a(tooltip="Revert VM to this snapshot", xo-click="revertSnapshot(snapshot.id)", ng-if = 'canAdmin()')
i.fa.fa-undo.fa-lg
a(tooltip="Remove this snapshot", xo-click="deleteSnapshot(snapshot.id)", ng-if = 'canAdmin()')
i.fa.fa-trash-o.fa-lg
.center(ng-if = '(VM.snapshots | count) > 5 || currentSnapPage > 1')
pagination(boundary-links="true", total-items="VM.snapshots | count", ng-model="$parent.currentSnapPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.btn-form(ng-show="vmSnap.$visible")
p.center
button.btn.btn-default(type="button", ng-disabled="vmSnap.$waiting", ng-click="vmSnap.$cancel()")
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(type="submit", ng-disabled="vmSnap.$waiting", ng-click="saveChanges()")
i.fa.fa-save
| Save
//- Logs panel
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-comments
| Logs
span.quick-edit(ng-if="(VM.messages | isNotEmpty) && canAdmin()", tooltip="Remove all logs", xo-click="deleteAllLog()")
i.fa.fa-trash-o.fa-fw
.panel-body
p.center(ng-if="VM.messages | isEmpty") No recent logs
table.table.table-hover(ng-if="VM.messages | isNotEmpty")
th Date
th Name
tr(ng-repeat="message in VM.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
td.oneliner {{message.time*1e3 | date:"medium"}}
td.oneliner
| {{message.name}}
span.pull-right.btn-group.quick-buttons(ng-if="canAdmin()")
a(xo-click="deleteLog(message.id)")
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
.center(ng-if = '(VM.messages | count) > 5 || currentLogPage > 1')
pagination(boundary-links="true", total-items="VM.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(ng-if="canAdmin()")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-plug
| PCI Devices
.panel-body
p.center(ng-if="!(VM.$container | resolve).$PCIs") No PCI devices available
table.table.table-hover(ng-if="(VM.$container | resolve).$PCIs")
th PCI Info
th Device Name
th Status
tr(ng-repeat="pci in ((VM.$container | resolve).$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}}
td(ng-if="pci.pci_id === VM.other.pci")
span.label.label-success Attached
span.pull-right.btn-group.quick-buttons
a(tooltip="Disconnect this PCI device", xo-click="disconnectPci(VM.id)", ng-if = 'canAdmin()')
i.fa.fa-unlink.fa-lg
td(ng-if="pci.pci_id !== VM.other.pci")
span.label.label-default Not attached
span.pull-right.btn-group.quick-buttons
a(tooltip="Connect this PCI device", xo-click="connectPci(VM.id, pci.pci_id)", ng-if = 'canAdmin()')
i.fa.fa-link.fa-lg
.center(ng-if = '((VM.$container | resolve).$PCIs | resolve).length > 5 || currentPCIPage > 1')
pagination(boundary-links="true", total-items="((VM.$container | resolve).$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
| vGPUs
.panel-body
p.center(ng-if="!VM.$VGPus") No vGPUs available
table.table.table-hover(ng-if="VM.$VGPus")
th Device
th Status
tr(ng-repeat="vgpu in VM.$VGPUs | resolve | orderBy:'device' | slice:(5*(currentGPUPage-1)):(5*currentGPUPage) track by vgpu.id")
td.oneliner {{vgpu.device}}
td(ng-if="vgu.currentlyAttached")
span.label.label-success Attached
td(ng-if="!vgu.currentlyAttached")
span.label.label-default Not attached
.center(ng-if = '(VM.$VGPUs | resolve).length > 5 || currentGPUPage > 1')
pagination(boundary-links="true", total-items="(VM.$VGPUs | resolve).length", ng-model="$parent.currentGPUPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")

130
app/node_modules/angular-no-vnc/index.js generated vendored Normal file
View File

@@ -0,0 +1,130 @@
import angular from 'angular'
import {
format as formatUrl,
parse as parseUrl,
resolve as resolveUrl
} from 'url'
import {RFB} from 'novnc-node'
import view from './view'
// ===================================================================
function parseRelativeUrl (url) {
/* global window: false */
return parseUrl(resolveUrl(String(window.location), url))
}
const PROTOCOL_ALIASES = {
'http:': 'ws:',
'https:': 'wss:'
}
function fixProtocol (url) {
let protocol = PROTOCOL_ALIASES[url.protocol]
if (protocol) {
url.protocol = protocol
}
}
// ===================================================================
export default angular.module('no-vnc', [])
.controller('NoVncCtrl', function ($attrs, $element, $scope, $timeout) {
this.height = 480
$attrs.$observe('height', (height) => {
this.height = height
})
this.width = 640
$attrs.$observe('width', (width) => {
this.width = width
})
const retrySteps = {
0: 1000,
1: 3000,
2: 5000,
3: 5000
}
let retryStep = 0
let rfb
let reconnectTry
function clean () {
// If there was a previous connection.
if (rfb) {
rfb._onUpdateState = () => {}
rfb.disconnect()
rfb = undefined
}
}
function reset (url) {
// Remove previous connection.
clean()
// If the URL is empty, stop now.
if (!url) {
return
}
// Parse the URL.
url = parseRelativeUrl(url)
fixProtocol(url)
let isSecure = url.protocol === 'wss:'
rfb = new RFB({
encrypt: isSecure,
target: canvas,
wsProtocols: ['chat']
})
rfb._onUpdateState = (rfb, state) => {
if (state === 'normal') {
retryStep = 0
if (reconnectTry) {
$timeout.cancel(reconnectTry)
reconnectTry = undefined
}
}
if (state !== 'disconnected') {
return
}
if (retryStep in retrySteps) {
reconnectTry = $timeout(() => reset(url), retrySteps[retryStep++])
}
}
// Connect.
rfb.connect(formatUrl(url))
}
this.remoteControl = {
sendCtrlAltDel () {
if (rfb) {
rfb.sendCtrlAltDel()
}
}
}
let canvas = $element.find('canvas')[0]
$attrs.$observe('url', (url) => {
reset(url)
})
$scope.$on('$destroy', clean)
})
.directive('noVnc', function () {
return {
bindToController: true,
controller: 'NoVncCtrl as noVnc',
restrict: 'E',
scope: {
remoteControl: '='
},
template: view
}
})
.name

9
app/node_modules/angular-no-vnc/package.json generated vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"private": true,
"browserify": {
"transform": [
"babelify",
"browserify-plain-jade"
]
}
}

5
app/node_modules/angular-no-vnc/view.jade generated vendored Normal file
View File

@@ -0,0 +1,5 @@
canvas.center-block(
height = "{{noVnc.height}}"
width = "{{noVnc.width}}"
)
| Sorry, your browser does not support the canvas element.

63
app/node_modules/iso-device/index.js generated vendored Normal file
View File

@@ -0,0 +1,63 @@
import angular from 'angular'
import view from './view'
// =====================================================================
export default angular.module('xoWebApp.isoDevice', [])
.directive('isoDevice', () => ({
restrict: 'E',
template: view,
scope: {
srs: '=',
vm: '='
},
controller: 'IsoDevice as isoDevice',
bindToController: true
}))
.controller('IsoDevice', function ($scope, xo, xoApi) {
const {get} = xoApi
const descriptor = obj => obj.name_label + (obj.name_description.length ? (' - ' + obj.name_description) : '')
this.eject = VM => xo.vm.ejectCd(VM.id)
this.insert = (VM, disc_id) => xo.vm.insertCd(VM.id, disc_id, true)
const prepareDiskData = (srs, vbds) => {
const ISOOpts = []
for (let key in srs) {
const SR = srs[key]
if (SR.SR_type === 'iso') {
for (let key in SR.VDIs) {
const rIso = SR.VDIs[key]
const oIso = get(rIso)
ISOOpts.push({
sr: SR.name_label,
label: descriptor(oIso),
iso: oIso
})
}
}
}
let mounted = ''
for (let key in vbds) {
const VBD = vbds[key]
const oVbd = get(VBD)
if (oVbd && oVbd.is_cd_drive) {
const oVdi = get(oVbd.VDI)
oVdi && (mounted = oVdi.id)
}
}
return {
opts: ISOOpts,
mounted
}
}
$scope.$watchCollection(() => this.srs, srs => this.isos = prepareDiskData(srs, this.vm.$VBDs))
$scope.$watch(() => this.vm && this.vm.$VBDs, vbds => this.isos = prepareDiskData(this.srs, vbds))
})
// A module exports its name.
.name

9
app/node_modules/iso-device/package.json generated vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"private": true,
"browserify": {
"transform": [
"babelify",
"browserify-plain-jade"
]
}
}

15
app/node_modules/iso-device/view.jade generated vendored Normal file
View File

@@ -0,0 +1,15 @@
.form-inline
.form-group
.input-group
select.form-control(
ng-model = 'isoDevice.isos.mounted'
ng-change = 'isoDevice.insert(isoDevice.vm, isoDevice.isos.mounted)'
ng-options = 'iso.iso.id as iso.label group by iso.sr for iso in isoDevice.isos.opts'
)
option(value = '', disabled) -- CD Drive (empty) --
.input-group-btn
button.btn.btn-default(
ng-click = 'isoDevice.eject(isoDevice.vm)'
ng-disabled = '!isoDevice.isos.mounted'
)
i.fa.fa-eject

36
app/node_modules/master-select/demo.js generated vendored Normal file
View File

@@ -0,0 +1,36 @@
import angular from 'angular'
import angularUiRouter from 'angular-ui-router'
import masterSelect from './'
import view from './view'
// -------------------------------------------------------------------
class MasterSelectDemoCtrl {
constructor () {
this.master = null
const items = this.items = new Array(10)
for (let i = 0, n = items.length; i < n; ++i) {
items[i] = {
label: `Item ${i}`,
selected: (Math.random() < 0.5)
}
}
}
}
export default angular.module('xoWebApp.foo', [
angularUiRouter,
masterSelect
])
.config($stateProvider => {
$stateProvider.state('master-select-demo', {
url: '/master-select-demo',
controller: 'MasterSelectDemoCtrl as demo',
template: view
})
})
.controller('MasterSelectDemoCtrl', MasterSelectDemoCtrl)
.name

87
app/node_modules/master-select/index.js generated vendored Normal file
View File

@@ -0,0 +1,87 @@
import angular from 'angular'
import forEach from 'lodash.foreach'
// -------------------------------------------------------------------
const defaultIsSelected = (object) => object != null && object.selected
const defaultSelect = (object) => {
if (object != null) {
object.selected = true
}
}
const defaultUnselect = (object) => {
if (object != null) {
object.selected = false
}
}
export default angular.module('master-select', [])
.directive('masterSelect', function () {
return {
link ($scope, $elem, $attrs) {
const isSelected = defaultIsSelected
const select = defaultSelect
const unselect = defaultUnselect
let internalChange = false
$scope.$watch('items', items => {
if (internalChange) {
internalChange = false
return
}
const selected = $scope.selectedItems = Object.create(null)
let nAll = 0
let nSelected = 0
forEach(items, (item, key) => {
++nAll
if (isSelected(item, key, items)) {
selected[key] = item
++nSelected
}
})
const indeterminate = nSelected && nSelected !== nAll
const previousNgModel = $scope.ngModel
$scope.ngModel = indeterminate || Boolean(nSelected)
if (previousNgModel !== $scope.ngModel) {
internalChange = true
}
$elem.prop('indeterminate', indeterminate)
}, true)
$scope.$watch('ngModel', selected => {
if (internalChange) {
internalChange = false
return
}
internalChange = true
$elem.prop('indeterminate', false)
const {items} = $scope
const selectedItems = $scope.selectedItems = Object.create(null)
forEach(items, selected ?
(item, key) => {
select(item, key, items)
selectedItems[key] = item
} :
(item, key) => {
unselect(item, key, items)
}
)
})
},
restrict: 'A',
scope: {
items: '=masterSelect',
ngModel: '=',
selectedItems: '=?'
}
}
})
.name

10
app/node_modules/master-select/package.json generated vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"private": true,
"browserify": {
"transform": [
"babelify",
"browserify-plain-jade"
]
},
"devDependencies": {}
}

31
app/node_modules/master-select/view.jade generated vendored Normal file
View File

@@ -0,0 +1,31 @@
.container-fluid: .row: .grid
.panel.panel-default: .panel-body
p
button(
ng-click = 'demo.master = !demo.master'
) Toggle
p
input(
type = 'checkbox'
master-select = 'demo.items'
selected-items = 'selectedItems'
ng-model = 'demo.master'
)
| Master
ul
li(
ng-repeat = 'item in demo.items'
)
input(
type = 'checkbox'
ng-model = 'item.selected'
)
| {{item.label}}
.panel.panel-default: .panel-body
pre {{demo | json}}
.panel.panel-default: .panel-body
pre {{selectedItems | json}}

52
app/node_modules/tag/index.js generated vendored Normal file
View File

@@ -0,0 +1,52 @@
import angular from 'angular'
import trim from 'lodash.trim'
import pull from 'lodash.pull'
import view from './view'
// =====================================================================
export default angular.module('xoWebApp.tag', [])
.directive('xoTag', () => {
function link (scope, element, attrs) {
element.find('.add-button').on('click', function () {
element.find('input').focus()
})
}
return {
restrict: 'E',
template: view,
scope: {
object: '='
},
controller: 'XoTag as ctrl',
bindToController: true,
link
}
})
.controller('XoTag', function ($scope, xo, xoApi) {
const {get} = xoApi
if (typeof this.object === 'string') {
this.object = get(this.object)
}
this.add = (tag) => {
tag = trim(tag)
if (tag === '') {
return
}
xo.tag.add(tag, this.object.id)
.then(() => this.object.tags.push(tag)) // FIXME Work around until refresh problems solved
}
this.remove = (tag) => {
xo.tag.remove(tag, this.object.id)
.then(() => pull(this.object.tags, tag)) // FIXME Work around until refresh problems solved
}
})
// A module exports its name.
.name

9
app/node_modules/tag/package.json generated vendored Normal file
View File

@@ -0,0 +1,9 @@
{
"private": true,
"browserify": {
"transform": [
"babelify",
"browserify-plain-jade"
]
}
}

15
app/node_modules/tag/view.jade generated vendored Normal file
View File

@@ -0,0 +1,15 @@
.tag-zone
span
span(ng-repeat = 'tag in ctrl.object.tags | orderBy:tag')
span.label.label-primary
span
i.fa.fa-times(ng-click = 'ctrl.remove(tag)')
| &ensp;{{tag}}
| &ensp;
i.fa.add-button(ng-class = '{"fa-plus": !edit, "fa-minus": edit, edit: edit}', ng-click = 'edit = !edit; newTag = ""')
| &ensp;
form.form-inline(ng-attr-id = 'xo-tag-{{ctrl.object.id}}',ng-submit = 'ctrl.add(newTag); newTag = ""; edit = false')
.input-group(ng-hide = '!edit')
input.form-control.input-sm(type = 'text', ng-model = 'newTag', form = 'xo-tag-{{ctrl.object.id}}')
span.input-group-btn
button.btn.btn-primary.btn-sm(type = 'submit', ng-click = '', form = 'xo-tag-{{ctrl.object.id}}') Add tag

441
app/node_modules/xo-api/index.js generated vendored Normal file
View File

@@ -0,0 +1,441 @@
import angular from 'angular'
import angularCookies from 'angular-cookies'
import cloneDeep from 'lodash.clonedeep'
import forEach from 'lodash.foreach'
import indexOf from 'lodash.indexof'
import sum from 'lodash.sum'
import XoIndex from 'xo-collection/index'
import xoLib from 'xo-lib'
import XoUniqueIndex from 'xo-collection/unique-index'
import XoView from 'xo-collection/view'
const {defineProperty} = Object
const {isArray, isString} = angular
// ===================================================================
// Low level XO API for Angular.
export default angular.module('xo-api', [
angularCookies
])
.run(function ($rootScope) {
// Ensure correct integration with Angular.
xoLib.setScheduler(function (fn) {
$rootScope.$evalAsync(fn)
})
})
.service('xoApi', function (
$cookies,
$rootScope,
$timeout,
$window
) {
const xo = new xoLib.Xo()
// A lots of event listeners are added to the collection.
xo.objects.setMaxListeners(0)
// Notifies Angular about changes in the collection.
xo.objects.on('finish', () => {
$rootScope.$applyAsync()
})
xo.signIn({ token: $cookies.get('token') }).catch(() => {
$cookies.remove('token')
// Full page reload.
$window.location.reload(true)
})
// ---------------------------------------------------------------
const getObject = (function (objects) {
const {
all: byIds,
indexes: {
ref: byRefs
}
} = objects
return function getObject (id, type) {
const object = byIds[id] || byRefs[id]
if (
// The object has been found and …
object && (
// … no type specified.
!type ||
// … is of the expected type.
(type === object.type) ||
// … is of one of the allowed types.
isArray(type) && (indexOf(type, object.type) === -1)
)
) {
return object
}
}
})(xo.objects)
// ---------------------------------------------------------------
// TODO: should probably be merged in the main collection in xo-lib.
let currentAcls = Object.create(null)
let detailedAcls = Object.create(null)
;(function updateCurrentAcls () {
xo.call('acl.getCurrent').then(acls => {
currentAcls = Object.create(null)
forEach(acls, acl => {
const object = getObject(acl.object)
if (object) {
currentAcls[object.id] = true
detailedAcls[object.id] = acl
}
})
$timeout(updateCurrentAcls, 1e4)
})
})()
function canAccess (id) {
// Auto unbox.
if (id.id) {
id = id.id
}
const {user} = xo
let object
return (
// Administrators can access everything.
user && (user.permission === 'admin') ||
// Check if the id is in the ACLs table.
(id in currentAcls) ||
// Check if the id is in fact not a true id (maybe a ref or a
// UUID) and if we can resolve it to an id.
(object = getObject(id)) && (object.id in currentAcls)
)
}
function canInteract (id, interaction) {
if (id.id) {
id = id.id
}
const {user} = xo
if (user && (user.permission === 'admin')) {
return true
}
let object
let acl = ((id in detailedAcls) && detailedAcls[id]) || ((object = getObject(id)) && (object.id in detailedAcls) && detailedAcls[object.id])
if (!acl) {
return false
}
const roleScale = {
'admin': 3,
'operator': 2,
'viewer': 1
}
return roleScale[interaction] <= (roleScale[acl.action] || 0)
}
// ---------------------------------------------------------------
const getView = (function () {
const views = Object.create(null)
function getView (viewName) {
let view = views[viewName]
if (!view) {
// The view name can be plural (ex VMs) but the type is
// singular.
const type = viewName[viewName.length - 1] === 's' ?
viewName.slice(0, -1) :
viewName
const predicate = (object) => object.type === type
view = views[viewName] = new XoView(xo.objects, predicate)
}
return view
}
// -------------------------------------------------------------
function registerLazyView (name, predicate, collection) {
defineProperty(views, name, {
configurable: true,
enumerable: true,
get () {
if (!collection) {
collection = xo.objects
} else if (isString(collection)) {
collection = getView(collection)
}
const view = new XoView(collection, predicate)
delete views[name]
views[name] = view
return view
}
})
}
registerLazyView(
'runningHosts',
host => host.power_state === 'Running',
'hosts'
)
const RUNNING_VM_STATUSES = {
Running: true,
Paused: true
}
registerLazyView(
'runningVms',
(vm) => RUNNING_VM_STATUSES[vm.power_state],
'VMs'
)
const RUNNING_TASK_STATUSES = {
cancelling: true,
pending: true
}
registerLazyView(
'runningTasks',
(task) => RUNNING_TASK_STATUSES[task.status] && canAccess(task.$host),
'tasks'
)
// -------------------------------------------------------------
return getView
})()
// ---------------------------------------------------------------
const getIndex = (function (indexes) {
function registerLazyIndex (name, computeHash, collection, isUnique) {
Object.defineProperty(indexes, name, {
configurable: true,
get () {
if (!collection) {
collection = xo.objects
} else if (isString(collection)) {
collection = getView(collection)
}
const index = new (isUnique ? XoUniqueIndex : XoIndex)(computeHash)
index._attachCollection(collection)
const items = index.items
delete indexes[name]
indexes[name] = items
return items
}
})
}
registerLazyIndex(
'hostsByPool',
'$poolId',
'hosts'
)
registerLazyIndex(
'networksByPool',
'$poolId',
'networks'
)
registerLazyIndex(
'poolPatchesByPool',
'$poolId',
'pool_patch'
)
registerLazyIndex(
'runningHostsByPool',
'$poolId',
'runningHosts'
)
registerLazyIndex(
'runningTasksByHost',
'$host',
'runningTasks'
)
registerLazyIndex(
'runningVmsByPool',
'$poolId',
'runningVms'
)
registerLazyIndex(
'srsByContainer',
'$container',
'SRs'
)
registerLazyIndex(
'vmsByContainer',
'$container',
'VMs'
)
registerLazyIndex(
'vmsByPool',
'$poolId',
'VMs'
)
registerLazyIndex(
'vmControllersByContainer',
'$container',
'VM-controllers',
true
)
registerLazyIndex(
'vmTemplatesByContainer',
'$container',
'VM-templates'
)
return function getIndex (name) {
const index = indexes[name]
if (!index) {
throw new Error('no such index ' + name)
}
return index
}
})(Object.create(null))
// ---------------------------------------------------------------
const stats = {
$CPUs: 0,
$vCPUs: 0,
$memory: {
usage: 0,
size: 0
}
}
getView('hosts').on('finish', function () {
stats.$CPUs = sum(this.all, host => +host.CPUs.cpu_count)
})
getView('runningVms').on('finish', function () {
stats.$vCPUs = sum(this.all, vm => vm.CPUs.number)
})
// TODO: maybe merge with stats.$CPUs.
getView('runningHosts').on('finish', function () {
// TODO: merge into a single loop.
stats.$memory.usage = sum(this.all, host => host.memory.usage)
stats.$memory.size = sum(this.all, host => host.memory.size)
})
// ---------------------------------------------------------------
return {
// -----------------
// Session
// -----------------
logOut () {
$cookies.remove('token')
// Full page reload.
$window.location.reload(true)
},
get status () {
return xo.status
},
get user () {
return xo.user
},
// -----------------
// RPC
// -----------------
call (method, params) {
// The params need to be cloned to prevent them from being
// changed before the method has really been sent.
return xo.call(method, cloneDeep(params))
},
// -----------------
// Objects
// -----------------
// Registers a watcher for objects update.
//
// The returned function can be used to unregister the watcher.
onUpdate (fn) {
// Prevent access to `xo.objects` via `this`.
const listener = () => { fn() }
xo.objects.on('finish', listener)
return () => {
xo.objects.removeListener('finish', listener)
}
},
// Collection of all objects.
all: xo.objects.all,
// Returns an object (or multiple objects) from its id/ref.
//
// They can be filter by type(s).
get (id, types) {
if (isArray(id)) {
const objects = []
forEach(id, id => {
const object = getObject(id, types)
if (object) {
objects.push(object)
}
})
return objects
}
return getObject(id, types)
},
getIndex,
// Returns a view (read-only XoCollection).
getView,
// Checks whether the current user has access to an object
// (identified via its id or ref).
canAccess,
// Provides a check for user interaction with an object
canInteract,
// -----------------
// Various
// -----------------
// Global stats.
stats
}
})
.name

8
app/node_modules/xo-api/package.json generated vendored Normal file
View File

@@ -0,0 +1,8 @@
{
"private": true,
"browserify": {
"transform": [
"babelify"
]
}
}

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