Compare commits
285 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4ccee8df6 | ||
|
|
fbcf803d06 | ||
|
|
5247b7a9af | ||
|
|
dc218cc992 | ||
|
|
c21761d9d4 | ||
|
|
36c0bf06d7 | ||
|
|
ccdab2b083 | ||
|
|
15a8a56807 | ||
|
|
385d42281b | ||
|
|
b0dc933021 | ||
|
|
b73ee1f638 | ||
|
|
51c2a54179 | ||
|
|
2d71a916a2 | ||
|
|
5f9cf47003 | ||
|
|
16b39185dc | ||
|
|
6f0410f26e | ||
|
|
0b86845852 | ||
|
|
d5f914bd2f | ||
|
|
663c65e42e | ||
|
|
b9de86f96c | ||
|
|
bd9c0ffb25 | ||
|
|
9d763773cf | ||
|
|
540f977146 | ||
|
|
d16b09d3fc | ||
|
|
6f8a8d3b90 | ||
|
|
00ef4166c7 | ||
|
|
b88414735e | ||
|
|
af092fae9b | ||
|
|
b889efc913 | ||
|
|
877dd68a6b | ||
|
|
2805a1c7bc | ||
|
|
c5c000ea6f | ||
|
|
673f1072bf | ||
|
|
d0e93b9b9f | ||
|
|
f239088bcb | ||
|
|
32642f105c | ||
|
|
4adaf6d355 | ||
|
|
291e2a5e40 | ||
|
|
05bdb56203 | ||
|
|
cb71df8345 | ||
|
|
c6c5f5188b | ||
|
|
a7b6ca0914 | ||
|
|
30ba062695 | ||
|
|
a595af7b3f | ||
|
|
b2ee3172d8 | ||
|
|
73992ee8e9 | ||
|
|
78885fd00a | ||
|
|
ce55ac6ccb | ||
|
|
8ce0951e5f | ||
|
|
7788fa9d3e | ||
|
|
7f36552c71 | ||
|
|
16f9437b29 | ||
|
|
0beaff718e | ||
|
|
9b6f37b5d0 | ||
|
|
3d6d4aea6a | ||
|
|
2356a21e54 | ||
|
|
a55e7ed34f | ||
|
|
e355e4d35d | ||
|
|
6dcaf80f3f | ||
|
|
a465114d36 | ||
|
|
07fbcb3488 | ||
|
|
534fbe1b6e | ||
|
|
f5c9c1ba0e | ||
|
|
5d5485f569 | ||
|
|
3d3fa5d18a | ||
|
|
312c41f229 | ||
|
|
2df1dc9028 | ||
|
|
222f245e63 | ||
|
|
2aa7702aed | ||
|
|
0b185c35c2 | ||
|
|
48dcec3cc3 | ||
|
|
8567179fa3 | ||
|
|
79d15ecd7e | ||
|
|
837c7e4bc7 | ||
|
|
2ae7e9920d | ||
|
|
8cf955b674 | ||
|
|
33f897d43e | ||
|
|
ddb0946a0d | ||
|
|
0f5beac4a8 | ||
|
|
974e2f71f9 | ||
|
|
3c427d7e28 | ||
|
|
0f10c8f5df | ||
|
|
7840b51f5c | ||
|
|
6578855182 | ||
|
|
58d68497a4 | ||
|
|
bddcf42a54 | ||
|
|
6318f4e7ac | ||
|
|
0c6cced7ee | ||
|
|
925bf47c9e | ||
|
|
8472b991ff | ||
|
|
ed59c32d96 | ||
|
|
b1981d7499 | ||
|
|
8983dfea57 | ||
|
|
5231b9b22b | ||
|
|
55846a2314 | ||
|
|
1c94f5749d | ||
|
|
90bacd9d31 | ||
|
|
0053cbf782 | ||
|
|
5d120a79e8 | ||
|
|
3389569ea0 | ||
|
|
f546606de0 | ||
|
|
fef95b3aae | ||
|
|
5ba2b72439 | ||
|
|
4bb849f7c9 | ||
|
|
21b5e7e701 | ||
|
|
34a1965497 | ||
|
|
1701682636 | ||
|
|
5d826972f1 | ||
|
|
2467b336e5 | ||
|
|
4f78414c7f | ||
|
|
4532714bae | ||
|
|
352c23b0ba | ||
|
|
8e432ee818 | ||
|
|
47bb2d24f5 | ||
|
|
f3fd4c607d | ||
|
|
0610ceafdf | ||
|
|
032fcdce40 | ||
|
|
636bacd637 | ||
|
|
3f3fbd8bbc | ||
|
|
955e88b4fb | ||
|
|
5954b552c9 | ||
|
|
aaad4c5d20 | ||
|
|
a24c8526ea | ||
|
|
a533535520 | ||
|
|
badded3aa4 | ||
|
|
3055e612d4 | ||
|
|
525cb1a2b6 | ||
|
|
4dd70abc3b | ||
|
|
2ea4c214df | ||
|
|
0a0174a79d | ||
|
|
3db031be1b | ||
|
|
6d3a87fe7d | ||
|
|
8cfd2cdd79 | ||
|
|
9e874e076f | ||
|
|
28192bf184 | ||
|
|
a54957b4de | ||
|
|
f4b1a076b7 | ||
|
|
27a3296d6e | ||
|
|
1aaaee128f | ||
|
|
15a16a2c35 | ||
|
|
db23fe5a58 | ||
|
|
620c88b615 | ||
|
|
99f2fb9764 | ||
|
|
d5a3e67dbd | ||
|
|
55ef81f3e7 | ||
|
|
41699fab1e | ||
|
|
32a1195157 | ||
|
|
f53db2ddfa | ||
|
|
e060f9172b | ||
|
|
4adef88e61 | ||
|
|
d734f2cf89 | ||
|
|
3e81d14bd8 | ||
|
|
e88a94d9e0 | ||
|
|
f4f16e4e87 | ||
|
|
6268f3a3d9 | ||
|
|
06e7c8d19a | ||
|
|
32395232ea | ||
|
|
65d6ef91ff | ||
|
|
4aecc875d1 | ||
|
|
0e649a626c | ||
|
|
5fa249b0f3 | ||
|
|
24ca86aad3 | ||
|
|
8a4f413289 | ||
|
|
6dbad4501d | ||
|
|
9ab6490fee | ||
|
|
a413efa550 | ||
|
|
cd337d444c | ||
|
|
45e1ce0a42 | ||
|
|
e5ef1e6efe | ||
|
|
b1ce3be3d2 | ||
|
|
e13ab73a29 | ||
|
|
aede952b12 | ||
|
|
acc1476b29 | ||
|
|
138bf56624 | ||
|
|
c608de4183 | ||
|
|
ccb6c02c31 | ||
|
|
5cc457b28c | ||
|
|
a353b3d40d | ||
|
|
6f7aca8e5b | ||
|
|
92b0d4561e | ||
|
|
ef8b8346dc | ||
|
|
058058a015 | ||
|
|
fddba7315a | ||
|
|
a5e964ea19 | ||
|
|
3d2152e559 | ||
|
|
50f9c68c26 | ||
|
|
b40207b367 | ||
|
|
6c9305d2b1 | ||
|
|
9fda3c911d | ||
|
|
473c3601ef | ||
|
|
fde8a3720d | ||
|
|
13a6d6b458 | ||
|
|
29d9ba0446 | ||
|
|
71e271774e | ||
|
|
c9db49e255 | ||
|
|
22f35f0e86 | ||
|
|
375f3ac3ac | ||
|
|
b60a02bc34 | ||
|
|
5a4d821c98 | ||
|
|
cd0305c71d | ||
|
|
371459ff5e | ||
|
|
5a8a7c6a0f | ||
|
|
69db541300 | ||
|
|
94949866ee | ||
|
|
c22b3e7449 | ||
|
|
096dde922b | ||
|
|
00f26d854f | ||
|
|
6c8ff1717e | ||
|
|
c7288c1d8a | ||
|
|
2e52fe369d | ||
|
|
df3430add5 | ||
|
|
7af848c94b | ||
|
|
f57c462b5f | ||
|
|
6018035908 | ||
|
|
c8b0351786 | ||
|
|
26cc812f82 | ||
|
|
67f98950e6 | ||
|
|
8ba8537b9f | ||
|
|
a7f05a68e0 | ||
|
|
a0db228154 | ||
|
|
eec6fabe58 | ||
|
|
501c038f97 | ||
|
|
e0a0f717fd | ||
|
|
4dd3f7487c | ||
|
|
99d1cddaa5 | ||
|
|
2158e1a47e | ||
|
|
059238759a | ||
|
|
8d3ea7548a | ||
|
|
221b411b63 | ||
|
|
e2c173990f | ||
|
|
a609a8d5d6 | ||
|
|
c5c2afddc2 | ||
|
|
409d87f210 | ||
|
|
78baa4b01e | ||
|
|
b41c66c4af | ||
|
|
d6b7388c2e | ||
|
|
b0dc681d48 | ||
|
|
c20b460f2d | ||
|
|
cd0a46fd7f | ||
|
|
0a9c868678 | ||
|
|
027d1e8cb1 | ||
|
|
a5c9880318 | ||
|
|
e09988e1d7 | ||
|
|
21a1629dfa | ||
|
|
7644c626c8 | ||
|
|
2c91b73016 | ||
|
|
ae54b8eba8 | ||
|
|
47bcaa8659 | ||
|
|
73dc6fb050 | ||
|
|
a4ced510c6 | ||
|
|
40d9fe873c | ||
|
|
d06fe4a7c7 | ||
|
|
52d71e5c4a | ||
|
|
d86c2b64f2 | ||
|
|
84449f4e2e | ||
|
|
835dbac81a | ||
|
|
4d190b91a5 | ||
|
|
c4cddca84a | ||
|
|
bfe9598ce4 | ||
|
|
3421222c2f | ||
|
|
3ca95feb7c | ||
|
|
903bc364ba | ||
|
|
16fd498c29 | ||
|
|
ca45776739 | ||
|
|
c849f06ce8 | ||
|
|
77aa74e9c3 | ||
|
|
8dc0a92c52 | ||
|
|
30b7cfb53d | ||
|
|
0caeb71603 | ||
|
|
a40ad2eab9 | ||
|
|
1fba0cd31f | ||
|
|
e6ce672bc5 | ||
|
|
1418f0b697 | ||
|
|
81aa972108 | ||
|
|
c36ac49cca | ||
|
|
5bf069f307 | ||
|
|
1837d3d291 | ||
|
|
8fe3a788d7 | ||
|
|
c622f9a295 | ||
|
|
ba72a48e02 | ||
|
|
b1bd74c5b7 | ||
|
|
8f3a5780a9 | ||
|
|
177143b57c | ||
|
|
36dcfc1517 | ||
|
|
50d8cb0b5d |
14
.babelrc
Normal file
14
.babelrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"comments": false,
|
||||
"compact": true,
|
||||
"optional": [
|
||||
// Experimental features.
|
||||
// "minification.constantFolding",
|
||||
// "minification.deadCodeElimination",
|
||||
|
||||
"es7.asyncFunctions",
|
||||
"es7.decorators",
|
||||
"es7.functionBind"
|
||||
//"runtime"
|
||||
]
|
||||
}
|
||||
@@ -46,7 +46,7 @@ indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
# Less
|
||||
[*.js]
|
||||
[*.less]
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
|
||||
|
||||
94
.jshintrc
94
.jshintrc
@@ -1,94 +0,0 @@
|
||||
{
|
||||
// Julien Fontanet JSHint configuration
|
||||
// https://gist.github.com/julien-f/8095615
|
||||
//
|
||||
// Changes from defaults:
|
||||
// - all enforcing options (except `++` & `--`) enabled
|
||||
// - single quotes
|
||||
// - indentation set to 2 instead of 4
|
||||
// - almost all relaxing options disabled
|
||||
// - allow expression statements (necessary for chai.expect())
|
||||
// - environments are set to Browserify, mocha & Node.js
|
||||
//
|
||||
// See http://jshint.com/docs/ for more details
|
||||
|
||||
"maxerr" : 50, // {int} Maximum error before stopping
|
||||
|
||||
// Enforcing
|
||||
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
|
||||
"camelcase" : true, // true: Identifiers must be in camelCase
|
||||
"curly" : true, // true: Require {} for every new block or scope
|
||||
"eqeqeq" : true, // true: Require triple equals (===) for comparison
|
||||
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
|
||||
"freeze" : true, // true: Prohibit overwriting prototypes of native objects (Array, Date, ...)
|
||||
"immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
|
||||
"indent" : 2, // {int} Number of spaces to use for indentation
|
||||
"latedef" : true, // true: Require variables/functions to be defined before being used
|
||||
"newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()`
|
||||
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
|
||||
"noempty" : true, // true: Prohibit use of empty blocks
|
||||
"nonbsp" : true, // true: Prohibit use of non breakable spaces
|
||||
"nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment)
|
||||
"plusplus" : false, // true: Prohibit use of `++` & `--`
|
||||
"quotmark" : "single", // Quotation mark consistency:
|
||||
// false : do nothing (default)
|
||||
// true : ensure whatever is used is consistent
|
||||
// "single" : require single quotes
|
||||
// "double" : require double quotes
|
||||
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
|
||||
"unused" : true, // true: Require all defined variables be used
|
||||
"strict" : false, // true: Requires all functions run in ES5 Strict Mode
|
||||
"maxcomplexity" : 7, // {int} Max cyclomatic complexity per function
|
||||
"maxdepth" : 3, // {int} Max depth of nested blocks (within functions)
|
||||
"maxlen" : 80, // {int} Max number of characters per line
|
||||
"maxparams" : 4, // {int} Max number of formal params allowed per function
|
||||
"maxstatements" : 20, // {int} Max number statements per function
|
||||
|
||||
// Relaxing
|
||||
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
|
||||
"boss" : false, // true: Tolerate assignments where comparisons would be expected
|
||||
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
|
||||
"eqnull" : false, // true: Tolerate use of `== null`
|
||||
"esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
|
||||
"expr" : false, // true: Tolerate `ExpressionStatement` as Programs
|
||||
"funcscope" : false, // true: Tolerate defining variables inside control statements
|
||||
"globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
|
||||
"iterator" : false, // true: Tolerate using the `__iterator__` property
|
||||
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
|
||||
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
|
||||
"laxcomma" : false, // true: Tolerate comma-first style coding
|
||||
"loopfunc" : false, // true: Tolerate functions being defined in loops
|
||||
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
|
||||
// (ex: `for each`, multiple try/catch, function expression…)
|
||||
"multistr" : false, // true: Tolerate multi-line strings
|
||||
"notypeof" : false, // true: Tolerate typeof comparison with unknown values.
|
||||
"proto" : false, // true: Tolerate using the `__proto__` property
|
||||
"scripturl" : false, // true: Tolerate script-targeted URLs
|
||||
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
|
||||
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
|
||||
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
|
||||
"validthis" : false, // true: Tolerate using this in a non-constructor function
|
||||
"noyield" : false, // true: Tolerate generators without yields
|
||||
|
||||
// Environments
|
||||
"browser" : false, // Web Browser (window, document, etc)
|
||||
"browserify" : true, // Browserify (node.js code in the browser)
|
||||
"couch" : false, // CouchDB
|
||||
"devel" : false, // Development/debugging (alert, confirm, etc)
|
||||
"dojo" : false, // Dojo Toolkit
|
||||
"jquery" : false, // jQuery
|
||||
"mocha" : false, // mocha
|
||||
"mootools" : false, // MooTools
|
||||
"node" : false, // Node.js
|
||||
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
|
||||
"phantom" : false, // PhantomJS
|
||||
"prototypejs" : false, // Prototype and Scriptaculous
|
||||
"rhino" : false, // Rhino
|
||||
"worker" : false, // Web Workers
|
||||
"wsh" : false, // Windows Scripting Host
|
||||
"yui" : false, // Yahoo User Interface
|
||||
|
||||
// Custom Globals
|
||||
"globals" : {} // additional predefined global variables
|
||||
}
|
||||
9
.npmignore
Normal file
9
.npmignore
Normal file
@@ -0,0 +1,9 @@
|
||||
/examples/
|
||||
example.js
|
||||
*.example.js
|
||||
*.example.js.map
|
||||
|
||||
/test/
|
||||
/tests/
|
||||
*.spec.js
|
||||
*.spec.js.map
|
||||
119
CHANGELOG.md
119
CHANGELOG.md
@@ -1,10 +1,125 @@
|
||||
# ChangeLog
|
||||
|
||||
## **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
|
||||
### 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))
|
||||
@@ -13,7 +128,7 @@
|
||||
- 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
|
||||
### 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))
|
||||
|
||||
661
LICENSE
Normal file
661
LICENSE
Normal 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/>.
|
||||
13
README.md
13
README.md
@@ -1,5 +1,7 @@
|
||||
# Xen Orchestra Web
|
||||
|
||||

|
||||
|
||||
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).
|
||||
@@ -39,6 +41,9 @@ Otherwise, please consider using the [bugtracker of the general repository](http
|
||||
# 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
|
||||
|
||||
@@ -50,11 +55,15 @@ git checkout next-release
|
||||
|
||||
# Fetches the last changes (the merge and version bump) from master to
|
||||
# next-release.
|
||||
git pull --fast-forward master
|
||||
git merge --ff-only master
|
||||
|
||||
# Push the changes on git.
|
||||
git push origin master:master next-release:next-release
|
||||
git push --follow-tags origin master next-release
|
||||
|
||||
# Publish this release to npm.
|
||||
npm publish
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
AGPL3 © [Vates SAS](http://vates.fr)
|
||||
|
||||
143
app/app.js
143
app/app.js
@@ -1,40 +1,40 @@
|
||||
// Must be loaded before angular.
|
||||
import 'angular-file-upload';
|
||||
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 angular from 'angular'
|
||||
import uiBootstrap from'angular-ui-bootstrap'
|
||||
import uiIndeterminate from'angular-ui-indeterminate'
|
||||
import uiRouter from'angular-ui-router'
|
||||
import uiSelect from'angular-ui-select'
|
||||
|
||||
import naturalSort from 'angular-natural-sort';
|
||||
import xeditable from 'angular-xeditable';
|
||||
import naturalSort from 'angular-natural-sort'
|
||||
import xeditable from 'angular-xeditable'
|
||||
|
||||
import xoDirectives from 'xo-directives';
|
||||
import xoFilters from 'xo-filters';
|
||||
import xoServices from 'xo-services';
|
||||
import xoDirectives from 'xo-directives'
|
||||
import xoFilters from 'xo-filters'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import aboutState from './modules/about';
|
||||
import adminState from './modules/admin';
|
||||
import consoleState from './modules/console';
|
||||
import deleteVmsState from './modules/delete-vms';
|
||||
import genericModalState from './modules/generic-modal';
|
||||
import hostState from './modules/host';
|
||||
import listState from './modules/list';
|
||||
import loginState from './modules/login';
|
||||
import navbarState from './modules/navbar';
|
||||
import newSrState from './modules/new-sr';
|
||||
import newVmState from './modules/new-vm';
|
||||
import poolState from './modules/pool';
|
||||
import settingsState from './modules/settings';
|
||||
import srState from './modules/sr';
|
||||
import treeState from './modules/tree';
|
||||
import vmState from './modules/vm';
|
||||
import isoDevice from './modules/iso-device';
|
||||
import aboutState from './modules/about'
|
||||
import consoleState from './modules/console'
|
||||
import deleteVmsState from './modules/delete-vms'
|
||||
import genericModalState from './modules/generic-modal'
|
||||
import hostState from './modules/host'
|
||||
import listState from './modules/list'
|
||||
import loginState from './modules/login'
|
||||
import navbarState from './modules/navbar'
|
||||
import newSrState from './modules/new-sr'
|
||||
import newVmState from './modules/new-vm'
|
||||
import poolState from './modules/pool'
|
||||
import schedulerState from './modules/scheduler'
|
||||
import settingsState from './modules/settings'
|
||||
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';
|
||||
import '../dist/bower_components/angular-chart.js/dist/angular-chart.js'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp', [
|
||||
uiBootstrap,
|
||||
@@ -50,7 +50,6 @@ export default angular.module('xoWebApp', [
|
||||
xoServices,
|
||||
|
||||
aboutState,
|
||||
adminState,
|
||||
consoleState,
|
||||
deleteVmsState,
|
||||
genericModalState,
|
||||
@@ -61,18 +60,19 @@ export default angular.module('xoWebApp', [
|
||||
newSrState,
|
||||
newVmState,
|
||||
poolState,
|
||||
schedulerState,
|
||||
settingsState,
|
||||
srState,
|
||||
treeState,
|
||||
updater,
|
||||
vmState,
|
||||
isoDevice,
|
||||
'chart.js'
|
||||
])
|
||||
|
||||
// Prevent Angular.js from mangling exception stack (interfere with
|
||||
// source maps).
|
||||
.factory('$exceptionHandler', () => function (exception) {
|
||||
throw exception;
|
||||
throw exception
|
||||
})
|
||||
|
||||
.config(function (
|
||||
@@ -88,29 +88,29 @@ export default angular.module('xoWebApp', [
|
||||
// the console.
|
||||
//
|
||||
// See https://docs.angularjs.org/guide/production
|
||||
$compileProvider.debugInfoEnabled(false);
|
||||
$compileProvider.debugInfoEnabled(false)
|
||||
|
||||
// Redirect to default state.
|
||||
$stateProvider.state('index', {
|
||||
url: '/',
|
||||
controller: function ($state, xoApi) {
|
||||
let isAdmin = xoApi.user && (xoApi.user.permission === 'admin');
|
||||
let isAdmin = xoApi.user && (xoApi.user.permission === 'admin')
|
||||
|
||||
$state.go(isAdmin ? 'tree' : 'list');
|
||||
},
|
||||
});
|
||||
$state.go(isAdmin ? 'tree' : 'list')
|
||||
}
|
||||
})
|
||||
|
||||
// Redirects unmatched URLs to `/`.
|
||||
$urlRouterProvider.otherwise('/');
|
||||
$urlRouterProvider.otherwise('/')
|
||||
|
||||
// Changes the default settings for the tooltips.
|
||||
$tooltipProvider.options({
|
||||
appendToBody: true,
|
||||
placement: 'bottom',
|
||||
});
|
||||
placement: 'bottom'
|
||||
})
|
||||
|
||||
uiSelectConfig.theme = 'bootstrap';
|
||||
uiSelectConfig.resetSearchInput = true;
|
||||
uiSelectConfig.theme = 'bootstrap'
|
||||
uiSelectConfig.resetSearchInput = true
|
||||
})
|
||||
.run(function (
|
||||
$anchorScroll,
|
||||
@@ -119,65 +119,64 @@ export default angular.module('xoWebApp', [
|
||||
editableOptions,
|
||||
editableThemes,
|
||||
notify,
|
||||
xoApi,
|
||||
xo
|
||||
updater,
|
||||
xoApi
|
||||
) {
|
||||
$rootScope.$on('$stateChangeStart', function (event, state, stateParams) {
|
||||
let {user} = xoApi;
|
||||
let loggedIn = !!user;
|
||||
let {user} = xoApi
|
||||
let loggedIn = !!user
|
||||
|
||||
if (state.name === 'login') {
|
||||
if (loggedIn) {
|
||||
event.preventDefault();
|
||||
$state.go('index');
|
||||
event.preventDefault()
|
||||
$state.go('index')
|
||||
}
|
||||
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (!loggedIn) {
|
||||
event.preventDefault();
|
||||
event.preventDefault()
|
||||
|
||||
// FIXME: find a better way to pass info to the login controller.
|
||||
$rootScope._login = { state, stateParams };
|
||||
$rootScope._login = { state, stateParams }
|
||||
|
||||
$state.go('login');
|
||||
return;
|
||||
$state.go('login')
|
||||
return
|
||||
}
|
||||
|
||||
if (user.permission === 'admin') {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// The user must have the `admin` permission to access the
|
||||
// settings and admin pages.
|
||||
if (/^admin\..*|settings|tree$/.test(state.name)) {
|
||||
event.preventDefault();
|
||||
// settings pages.
|
||||
if (/^settings\..*|tree$/.test(state.name)) {
|
||||
event.preventDefault()
|
||||
notify.error({
|
||||
title: 'Restricted area',
|
||||
message: 'You do not have the permission to view this page',
|
||||
});
|
||||
message: 'You do not have the permission to view this page'
|
||||
})
|
||||
}
|
||||
|
||||
let {id} = stateParams;
|
||||
if (id && !xo.canAccess(id)) {
|
||||
event.preventDefault();
|
||||
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',
|
||||
});
|
||||
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();
|
||||
});
|
||||
$anchorScroll()
|
||||
})
|
||||
|
||||
editableThemes.bs3.inputClass = 'input-sm';
|
||||
editableThemes.bs3.buttonsClass = 'btn-sm';
|
||||
editableOptions.theme = 'bs3';
|
||||
editableThemes.bs3.inputClass = 'input-sm'
|
||||
editableThemes.bs3.buttonsClass = 'btn-sm'
|
||||
editableOptions.theme = 'bs3'
|
||||
})
|
||||
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -48,7 +48,6 @@ html.no-js(lang="en", dir="ltr")
|
||||
link(rel="stylesheet", href="styles/main.css")
|
||||
body(
|
||||
ng-app = 'xoWebApp'
|
||||
ng-strict-di
|
||||
)
|
||||
|
||||
toaster-container
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import pkg from '../../../package';
|
||||
import pkg from '../../../package'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
module.exports = angular.module('xoWebApp.about', [
|
||||
uiRouter,
|
||||
export default angular.module('xoWebApp.about', [
|
||||
uiRouter
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('about', {
|
||||
url: '/about',
|
||||
controller: 'AboutCtrl',
|
||||
template: require('./view'),
|
||||
});
|
||||
template: require('./view')
|
||||
})
|
||||
})
|
||||
.controller('AboutCtrl', function ($scope) {
|
||||
$scope.pkg = pkg;
|
||||
$scope.pkg = pkg
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -45,6 +45,6 @@
|
||||
p.text-center
|
||||
img(src="images/support.png")
|
||||
p.text-center
|
||||
a.btn.btn-primary(href="https://xen-orchestra.com/services/")
|
||||
a.btn.btn-primary(href="https://vates.fr/services.html")
|
||||
i.fa.fa-envelope
|
||||
| Get services
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import uiSelect from 'angular-ui-select';
|
||||
|
||||
import filter from 'lodash.filter';
|
||||
|
||||
import xoApi from 'xo-api';
|
||||
import xoServices from 'xo-services';
|
||||
|
||||
import view from './view';
|
||||
|
||||
export default angular.module('admin.acls', [
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
|
||||
xoApi,
|
||||
xoServices,
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('admin.acls', {
|
||||
controller: 'AdminAcls as ctrl',
|
||||
url: '/acls',
|
||||
resolve: {
|
||||
acls(xo) {
|
||||
return xo.acl.get();
|
||||
},
|
||||
users(xo) {
|
||||
return xo.user.getAll();
|
||||
},
|
||||
},
|
||||
template: view,
|
||||
});
|
||||
})
|
||||
.controller('AdminAcls', function ($scope, acls, users, xoApi, xo) {
|
||||
this.acls = acls;
|
||||
|
||||
this.users = users;
|
||||
{
|
||||
let usersById = this.usersById = Object.create(null);
|
||||
for (let user of users) {
|
||||
usersById[user.id] = user;
|
||||
}
|
||||
}
|
||||
|
||||
this.objects = xoApi.all;
|
||||
|
||||
let refreshAcls = () => {
|
||||
xo.acl.get().then(acls => {
|
||||
this.acls = acls;
|
||||
});
|
||||
};
|
||||
|
||||
this.getUser = (id) => {
|
||||
for (let user of this.users) {
|
||||
if (user.id === id) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.addAcl = () => {
|
||||
xo.acl.add(this.subject.id, this.object.id).then(refreshAcls);
|
||||
};
|
||||
this.removeAcl = (subject, object) => {
|
||||
xo.acl.remove(subject, object).then(refreshAcls);
|
||||
};
|
||||
})
|
||||
.filter('selectHighLevel', () => {
|
||||
const HIGH_LEVEL_OBJECTS = {
|
||||
pool: true,
|
||||
host: true,
|
||||
VM: true,
|
||||
SR: true,
|
||||
};
|
||||
let isHighLevel = (object) => HIGH_LEVEL_OBJECTS[object.type];
|
||||
|
||||
return (objects) => filter(objects, isHighLevel);
|
||||
})
|
||||
.name
|
||||
;
|
||||
@@ -1,66 +0,0 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-users
|
||||
| Access Control List
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-plus-circle(style="color: #e25440;")
|
||||
| Create ACLs
|
||||
.panel-body.text-center
|
||||
form(
|
||||
ng-submit = 'ctrl.addAcl()'
|
||||
)
|
||||
.form-group
|
||||
ui-select(
|
||||
ng-model = 'ctrl.subject'
|
||||
)
|
||||
ui-select-match(
|
||||
placeholder = 'Choose a user'
|
||||
)
|
||||
div
|
||||
i.fa.fa-user
|
||||
| {{$select.selected.email}}
|
||||
ui-select-choices(
|
||||
repeat = 'user in ctrl.users | filter:{ permission: "!admin" } | filter:$select.search'
|
||||
)
|
||||
div
|
||||
i.fa.fa-user
|
||||
| {{user.email}}
|
||||
.form-group
|
||||
ui-select(
|
||||
ng-model = 'ctrl.object'
|
||||
)
|
||||
ui-select-match(
|
||||
placeholder = 'Choose an object'
|
||||
)
|
||||
div
|
||||
i(class = 'xo-icon-{{$select.selected.type | lowercase}}')
|
||||
| {{$select.selected.name_label}}
|
||||
ui-select-choices(
|
||||
repeat = 'object in ctrl.objects | selectHighLevel | filter:$select.search | orderBy:["type", "name_label"]'
|
||||
)
|
||||
div
|
||||
i(class = 'xo-icon-{{object.type | lowercase}}')
|
||||
| {{object.name_label}}
|
||||
button.btn.btn-success
|
||||
i.fa.fa-plus
|
||||
| Create
|
||||
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-street-view(style="color: #e25440;")
|
||||
| Existing ACLs
|
||||
.panel-body(ng-if = 'ctrl.acls.length')
|
||||
table.table.table-hover
|
||||
tr
|
||||
th User
|
||||
th Object
|
||||
th Remove permission
|
||||
tr(ng-repeat = 'acl in ctrl.acls')
|
||||
td {{ctrl.usersById[acl.subject].email}}
|
||||
td {{(acl.object | resolve).name_label}}
|
||||
td
|
||||
button.btn.btn-sm.btn-danger(ng-click = 'ctrl.removeAcl(acl.subject, acl.object)')
|
||||
i.fa.fa-trash
|
||||
@@ -1,29 +0,0 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
|
||||
import acls from './acls';
|
||||
|
||||
import view from './view';
|
||||
|
||||
export default angular.module('admin', [
|
||||
uiRouter,
|
||||
|
||||
acls,
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('admin', {
|
||||
abstract: true,
|
||||
template: view,
|
||||
url: '/admin',
|
||||
});
|
||||
|
||||
// Redirect to default sub-state.
|
||||
$stateProvider.state('admin.index', {
|
||||
url: '',
|
||||
controller: function ($state) {
|
||||
$state.go('admin.acls');
|
||||
}
|
||||
});
|
||||
})
|
||||
.name
|
||||
;
|
||||
@@ -1,14 +0,0 @@
|
||||
//- .container-fluid: .row
|
||||
|
||||
//- //- Side menu
|
||||
//- .col-md-2.acl-menu: .panel.panel-default: .panel-body: .side-menu
|
||||
//- ul.nav
|
||||
//- li
|
||||
//- a(ui-sref = '.acls', ui-sref-active = 'active')
|
||||
//- i.fa.fa-fw.fa-users
|
||||
//- | ACLs
|
||||
|
||||
//- //- Content
|
||||
//- .col-md-10: div(ui-view = '')
|
||||
|
||||
div(ui-view = '')
|
||||
@@ -1,29 +1,50 @@
|
||||
angular = require 'angular'
|
||||
forEach = require('lodash.foreach')
|
||||
includes = require('lodash.includes')
|
||||
|
||||
contains = require('lodash.contains')
|
||||
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) ->
|
||||
.controller 'ConsoleCtrl', ($scope, $stateParams, xoApi, xo, xoHideUnauthorizedFilter) ->
|
||||
{id} = $stateParams
|
||||
{get} = xoApi
|
||||
push = Array::push.apply.bind Array::push
|
||||
merge = do ->
|
||||
(args...) ->
|
||||
result = []
|
||||
for arg in args
|
||||
push result, arg if arg?
|
||||
result
|
||||
|
||||
pool = null
|
||||
host = null
|
||||
do (
|
||||
srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
poolSrs = null
|
||||
hostSrs = null
|
||||
) ->
|
||||
updateSrs = () =>
|
||||
srs = []
|
||||
poolSrs and forEach(poolSrs, (sr) => srs.push(sr))
|
||||
hostSrs and forEach(hostSrs, (sr) => srs.push(sr))
|
||||
$scope.SRs = xoHideUnauthorizedFilter(srs)
|
||||
$scope.$watchCollection(
|
||||
() => pool and srsByContainer[pool.id],
|
||||
(srs) =>
|
||||
poolSrs = srs
|
||||
updateSrs()
|
||||
)
|
||||
$scope.$watchCollection(
|
||||
() => host and srsByContainer[host.id],
|
||||
(srs) =>
|
||||
hostSrs = srs
|
||||
updateSrs()
|
||||
)
|
||||
|
||||
$scope.$watch(
|
||||
-> xoApi.get id
|
||||
@@ -38,34 +59,15 @@ module.exports = angular.module 'xoWebApp.console', [
|
||||
return unless (
|
||||
VM? and
|
||||
VM.power_state is 'Running' and
|
||||
not contains(VM.current_operations, 'clean_reboot')
|
||||
not includes(VM.current_operations, 'clean_reboot')
|
||||
)
|
||||
|
||||
pool = get VM.poolRef
|
||||
pool = get VM.$poolId
|
||||
return unless pool
|
||||
|
||||
$scope.consoleUrl = "/consoles/#{id}"
|
||||
$scope.consoleUrl = "./api/consoles/#{id}"
|
||||
|
||||
host = get VM.$container # host because the VM is running.
|
||||
return unless host
|
||||
|
||||
# FIXME: We should filter on connected SRs (PBDs)!
|
||||
SRs = get (merge host.SRs, pool.SRs)
|
||||
$scope.VDIs = do ->
|
||||
VDIs = []
|
||||
for SR in SRs
|
||||
push VDIs, SR.VDIs if SR.content_type is 'iso'
|
||||
get VDIs
|
||||
|
||||
cdDrive = do ->
|
||||
return VBD for VBD in (get VM.$VBDs) when VBD.is_cd_drive
|
||||
null
|
||||
|
||||
$scope.mountedIso =
|
||||
if cdDrive and cdDrive.VDI and (VDI = get cdDrive.VDI)
|
||||
VDI.UUID
|
||||
else
|
||||
''
|
||||
)
|
||||
|
||||
$scope.startVM = xo.vm.start
|
||||
|
||||
@@ -8,25 +8,14 @@
|
||||
|
|
||||
a(
|
||||
class = 'xo-color-{{VM.power_state | lowercase}}'
|
||||
ui-sref = 'VMs_view({id: VM.UUID})'
|
||||
ui-sref = 'VMs_view({id: VM.id})'
|
||||
) {{VM.name_label}}
|
||||
|
||||
.list-group
|
||||
|
||||
//- Toolbar
|
||||
.list-group-item: .row.text-center
|
||||
.col-sm-6: .input-group
|
||||
select.form-control(
|
||||
ng-model = 'mountedIso'
|
||||
ng-change = 'insert(mountedIso)'
|
||||
ng-options = 'VDI.UUID as VDI.name_label group by (VDI.$SR | resolve).name_label for VDI in VDIs | orderBy:natural("name_label")'
|
||||
)
|
||||
.input-group-btn
|
||||
button.btn.btn-default(
|
||||
ng-click = 'eject()'
|
||||
ng-disabled = '!mountedIso'
|
||||
)
|
||||
i.fa.fa-eject
|
||||
.col-sm-6: iso-device(ng-if = 'VM && SRs', vm = 'VM', srs = 'SRs')
|
||||
.col-sm-3: button.btn.btn-default(
|
||||
ng-click = 'vncRemote.sendCtrlAltDel()'
|
||||
)
|
||||
@@ -40,21 +29,21 @@
|
||||
ng-if = "VM.power_state == ('Running' || 'Paused')"
|
||||
tooltip = "Stop VM"
|
||||
type = "button"
|
||||
xo-click = "stopVM(VM.UUID)"
|
||||
xo-click = "stopVM(VM.id)"
|
||||
)
|
||||
i.fa.fa-stop.fa-fw
|
||||
button.btn.btn-default.inversed(
|
||||
ng-if = "VM.power_state == ('Halted')"
|
||||
tooltip = "Start VM"
|
||||
type = "button"
|
||||
xo-click = "startVM(VM.UUID)"
|
||||
xo-click = "startVM(VM.id)"
|
||||
)
|
||||
i.fa.fa-play.fa-fw
|
||||
button.btn.btn-default.inversed(
|
||||
ng-if = "VM.power_state == ('Running' || 'Paused')"
|
||||
tooltip = "Reboot VM"
|
||||
type = "button"
|
||||
xo-click = "rebootVM(VM.UUID)"
|
||||
xo-click = "rebootVM(VM.id)"
|
||||
)
|
||||
i.fa.fa-refresh.fa-fw
|
||||
//- Console
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import angular from 'angular';
|
||||
import uiBootstrap from 'angular-ui-bootstrap';
|
||||
// TODO: should be integrated xo.deleteVms()
|
||||
|
||||
import xoServices from 'xo-services';
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
|
||||
import view from './view';
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
//====================================================================
|
||||
import view from './view'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.deleteVms', [
|
||||
uiBootstrap,
|
||||
|
||||
xoServices,
|
||||
xoServices
|
||||
])
|
||||
.controller('DeleteVmsCtrl', function (
|
||||
$scope,
|
||||
@@ -19,23 +22,23 @@ export default angular.module('xoWebApp.deleteVms', [
|
||||
VMsIds
|
||||
) {
|
||||
$scope.$watchCollection(() => xoApi.get(VMsIds), function (VMs) {
|
||||
$scope.VMs = VMs;
|
||||
});
|
||||
$scope.VMs = VMs
|
||||
})
|
||||
|
||||
// Do disks have to be deleted for a given VM.
|
||||
let disks = $scope.disks = {};
|
||||
angular.forEach(VMsIds, id => {
|
||||
disks[id] = true;
|
||||
});
|
||||
let disks = $scope.disks = {}
|
||||
forEach(VMsIds, id => {
|
||||
disks[id] = true
|
||||
})
|
||||
|
||||
$scope.delete = function () {
|
||||
let value = [];
|
||||
angular.forEach(VMsIds, id => {
|
||||
value.push([id, disks[id]]);
|
||||
});
|
||||
let value = []
|
||||
forEach(VMsIds, id => {
|
||||
value.push([id, disks[id]])
|
||||
})
|
||||
|
||||
$modalInstance.close(value);
|
||||
};
|
||||
$modalInstance.close(value)
|
||||
}
|
||||
})
|
||||
.service('deleteVmsModal', function ($modal, xo) {
|
||||
return function (ids) {
|
||||
@@ -46,16 +49,15 @@ export default angular.module('xoWebApp.deleteVms', [
|
||||
VMsIds: () => ids
|
||||
}
|
||||
}).result.then(function (toDelete) {
|
||||
let promises = [];
|
||||
let promises = []
|
||||
|
||||
angular.forEach(toDelete, ([id, deleteDisks]) => {
|
||||
promises.push(xo.vm.delete(id, deleteDisks));
|
||||
});
|
||||
forEach(toDelete, ([id, deleteDisks]) => {
|
||||
promises.push(xo.vm.delete(id, deleteDisks))
|
||||
})
|
||||
|
||||
return promises;
|
||||
});
|
||||
};
|
||||
return promises
|
||||
})
|
||||
}
|
||||
})
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -12,11 +12,11 @@ form(ng-submit="delete()")
|
||||
th.col-sm-6 Description
|
||||
th.col-sm-3 Delete disks?
|
||||
tbody
|
||||
tr(ng-repeat="VM in VMs | orderBy:natural('name_label') track by VM.UUID")
|
||||
tr(ng-repeat="VM in VMs | orderBy:natural('name_label') track by VM.id")
|
||||
td {{VM.name_label}}
|
||||
td {{VM.name_description}}
|
||||
td
|
||||
input(type="checkbox", ng-model="disks[VM.UUID]")
|
||||
input(type="checkbox", ng-model="disks[VM.id]")
|
||||
.modal-footer
|
||||
button.btn.btn-primary(type="submit")
|
||||
| Delete
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import angular from 'angular';
|
||||
import uiBootstrap from 'angular-ui-bootstrap';
|
||||
import angular from 'angular'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.genericModal', [
|
||||
uiBootstrap,
|
||||
uiBootstrap
|
||||
])
|
||||
.controller('GenericModalCtrl', function ($scope, $modalInstance, options) {
|
||||
$scope.title = options.title;
|
||||
$scope.message = options.message;
|
||||
$scope.title = options.title
|
||||
$scope.message = options.message
|
||||
|
||||
$scope.yesButtonLabel = options.yesButtonLabel || 'Ok';
|
||||
$scope.noButtonLabel = options.noButtonLabel;
|
||||
$scope.yesButtonLabel = options.yesButtonLabel || 'Ok'
|
||||
$scope.noButtonLabel = options.noButtonLabel
|
||||
})
|
||||
.service('modal', function ($modal) {
|
||||
return {
|
||||
confirm: function (opts) {
|
||||
var modal = $modal.open({
|
||||
const modal = $modal.open({
|
||||
controller: 'GenericModalCtrl',
|
||||
template: require('./view'),
|
||||
resolve: {
|
||||
@@ -24,17 +24,16 @@ export default angular.module('xoWebApp.genericModal', [
|
||||
return {
|
||||
title: opts.title,
|
||||
message: opts.message,
|
||||
noButtonLabel: 'Cancel',
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
noButtonLabel: 'Cancel'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return modal.result;
|
||||
return modal.result
|
||||
}
|
||||
};
|
||||
}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
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'
|
||||
|
||||
#=====================================================================
|
||||
@@ -13,20 +18,100 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
controller: 'HostCtrl'
|
||||
template: require './view'
|
||||
.controller 'HostCtrl', (
|
||||
$scope, $stateParams
|
||||
$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?
|
||||
|
||||
$scope.pool = xoApi.get host.poolRef
|
||||
pool = $scope.pool = xoApi.get host.$poolId
|
||||
|
||||
SRsToPBDs = $scope.SRsToPBDs = Object.create null
|
||||
for PBD in host.$PBDs
|
||||
@@ -36,9 +121,17 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
continue unless PBD
|
||||
|
||||
SRsToPBDs[PBD.SR] = PBD
|
||||
$scope.listMissingPatches($scope.host.id)
|
||||
|
||||
if host.power_state is 'Running'
|
||||
refreshStatControl.start()
|
||||
else
|
||||
refreshStatControl.stop()
|
||||
)
|
||||
|
||||
$scope.removeMessage = xo.message.delete
|
||||
$scope.$watch('vms', (vms) =>
|
||||
$scope.vCPUs = sum(vms, (vm) => vm.CPUs.number)
|
||||
)
|
||||
|
||||
$scope.cancelTask = (id) ->
|
||||
modal.confirm({
|
||||
@@ -113,14 +206,19 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
{name_label, name_description, enabled} = $data
|
||||
|
||||
$data = {
|
||||
id: host.UUID
|
||||
id: host.id
|
||||
}
|
||||
if name_label isnt host.name_label
|
||||
$data.name_label = name_label
|
||||
if name_description isnt host.name_description
|
||||
$data.name_description = name_description
|
||||
if enabled isnt host.enabled
|
||||
$data.enabled = host.enabled
|
||||
if host.enabled
|
||||
$scope.disableHost($data.id)
|
||||
else
|
||||
$scope.enableHost($data.id)
|
||||
# enabled is not set via the "set" method, so we remove it before send it
|
||||
delete $data.enabled
|
||||
|
||||
xoApi.call 'host.set', $data
|
||||
|
||||
@@ -130,67 +228,131 @@ module.exports = angular.module 'xoWebApp.host', [
|
||||
message: 'Are you sure you want to delete all the logs?'
|
||||
}).then ->
|
||||
for log in $scope.host.messages
|
||||
console.log "Remove log #{log}"
|
||||
xo.log.delete log
|
||||
console.log "Remove log #{log.id}"
|
||||
xo.log.delete log.id
|
||||
|
||||
$scope.deleteLog = (id) ->
|
||||
console.log "Remove log #{id}"
|
||||
xo.log.delete id
|
||||
|
||||
$scope.connectPBD = (UUID) ->
|
||||
console.log "Connect PBD #{UUID}"
|
||||
$scope.connectPBD = (id) ->
|
||||
console.log "Connect PBD #{id}"
|
||||
|
||||
xoApi.call 'pbd.connect', {id: UUID}
|
||||
xoApi.call 'pbd.connect', {id: id}
|
||||
|
||||
$scope.disconnectPBD = (UUID) ->
|
||||
console.log "Disconnect PBD #{UUID}"
|
||||
$scope.disconnectPBD = (id) ->
|
||||
console.log "Disconnect PBD #{id}"
|
||||
|
||||
xoApi.call 'pbd.disconnect', {id: UUID}
|
||||
xoApi.call 'pbd.disconnect', {id: id}
|
||||
|
||||
$scope.removePBD = (UUID) ->
|
||||
console.log "Remove PBD #{UUID}"
|
||||
$scope.removePBD = (id) ->
|
||||
console.log "Remove PBD #{id}"
|
||||
|
||||
xoApi.call 'pbd.delete', {id: UUID}
|
||||
xoApi.call 'pbd.delete', {id: id}
|
||||
|
||||
$scope.connectPIF = (UUID) ->
|
||||
console.log "Connect PIF #{UUID}"
|
||||
$scope.connectPIF = (id) ->
|
||||
console.log "Connect PIF #{id}"
|
||||
|
||||
xoApi.call 'pif.connect', {id: UUID}
|
||||
xoApi.call 'pif.connect', {id: id}
|
||||
|
||||
$scope.disconnectPIF = (UUID) ->
|
||||
console.log "Disconnect PIF #{UUID}"
|
||||
$scope.disconnectPIF = (id) ->
|
||||
console.log "Disconnect PIF #{id}"
|
||||
|
||||
xoApi.call 'pif.disconnect', {id: UUID}
|
||||
xoApi.call 'pif.disconnect', {id: id}
|
||||
|
||||
$scope.removePIF = (UUID) ->
|
||||
console.log "Remove PIF #{UUID}"
|
||||
$scope.removePIF = (id) ->
|
||||
console.log "Remove PIF #{id}"
|
||||
|
||||
xoApi.call 'pif.delete', {id: UUID}
|
||||
xoApi.call 'pif.delete', {id: id}
|
||||
|
||||
$scope.importVm = ($files) ->
|
||||
$scope.importVm = ($files, id) ->
|
||||
file = $files[0]
|
||||
notify.info {
|
||||
title: 'VM import started'
|
||||
message: "Starting the VM import"
|
||||
}
|
||||
|
||||
xo.vm.import host.UUID
|
||||
xo.vm.import 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: 'VM import'
|
||||
message: "#{percentage}%"
|
||||
6e3
|
||||
)
|
||||
.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
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
| {{host.name_label}}
|
||||
small(ng-if="pool.name_label")
|
||||
| (
|
||||
a(ui-sref="pools_view({id: pool.UUID})") {{pool.name_label}}
|
||||
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
|
||||
| )
|
||||
p.center {{host.bios_strings["system-manufacturer"]}} {{host.bios_strings["system-product-name"]}}
|
||||
.grid
|
||||
@@ -28,8 +28,8 @@
|
||||
| {{host.name_description}}
|
||||
dt Enabled
|
||||
dd
|
||||
span(editable-checkbox="host.enabled", e-name="enabled", e-form="hostSettings")
|
||||
| {{host.enabled}}
|
||||
span(editable-select="host.enabled", e-ng-options="ap.v as ap.t for ap in [{v: true, t:'Yes'}, {v: false, t:'No'}]", e-name="enabled", e-form="hostSettings")
|
||||
| {{host.enabled ? 'Yes' : 'No'}}
|
||||
dt Tags
|
||||
dd(ng-if="host.tags.length")
|
||||
span(ng-repeat="tag in host.tags")
|
||||
@@ -45,6 +45,12 @@
|
||||
dd {{host.UUID}}
|
||||
dt iQN
|
||||
dd {{host.iSCSI_name}}
|
||||
dt(ng-if="refreshStatControl.running && stats") vCPUs/CPUs:
|
||||
dd(ng-if="refreshStatControl.running && stats") {{vCPUs}}/{{host.CPUs['cpu_count']}}
|
||||
dt(ng-if="refreshStatControl.running && stats") Running VMs:
|
||||
dd(ng-if="refreshStatControl.running && stats") {{vms | count}}
|
||||
dt(ng-if="refreshStatControl.running && stats") RAM (used/free):
|
||||
dd(ng-if="refreshStatControl.running && stats") {{host.memory.usage | bytesToSize}}/{{host.memory.size | bytesToSize}}
|
||||
.btn-form(ng-show="hostSettings.$visible")
|
||||
p.center
|
||||
button.btn.btn-default(type="button", ng-disabled="hostSettings.$waiting", ng-click="hostSettings.$cancel()")
|
||||
@@ -58,23 +64,133 @@
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-stats(style="color: #e25440;")
|
||||
| Stats
|
||||
.grid
|
||||
.grid-cell
|
||||
p.stat-name CPU usage:
|
||||
canvas(
|
||||
id="bar"
|
||||
class="chart chart-bar"
|
||||
data="[[host.$vCPUs], [host.CPUs['cpu_count']]]"
|
||||
labels="['']"
|
||||
series="['vCPUs','CPUs']"
|
||||
options="{scaleShowGridLines: false, barDatasetSpacing : 10, showScale: false}"
|
||||
.panel-body(ng-if="refreshStatControl.running && stats")
|
||||
div(ng-if="statView.cpuOnly", ng-click="statView.cpuOnly = false")
|
||||
p.stat-name
|
||||
i.fa.fa-tachometer
|
||||
| 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}'
|
||||
)
|
||||
.grid-cell
|
||||
p.stat-name RAM used:
|
||||
canvas(id="doughnut", class="chart chart-doughnut", data="[(host.memory.usage), (host.memory.size - host.memory.usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
|
||||
.grid-cell
|
||||
p.stat-name Running VMs:
|
||||
p.center.big-stat {{host.VMs.length}}
|
||||
div(ng-if="statView.ramOnly", ng-click="statView.ramOnly = false")
|
||||
p.stat-name
|
||||
//- i.fa.fa-bar-chart
|
||||
i.fa.fa-tasks
|
||||
//- i.fa.fa-server
|
||||
| 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.fa.fa-sitemap
|
||||
| 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
|
||||
| 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-sm-6(ng-click="statView.cpuOnly=true")
|
||||
p.stat-name
|
||||
i.fa.fa-tachometer
|
||||
| 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.fa.fa-bar-chart
|
||||
i.fa.fa-tasks
|
||||
//- i.fa.fa-server
|
||||
| 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.fa.fa-sitemap
|
||||
| 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-sm-6(ng-click="statView.loadOnly=true")
|
||||
p.stat-name
|
||||
i.fa.fa-cogs
|
||||
| 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-6.col-lg-4
|
||||
p.stat-name CPU usage:
|
||||
p.center.mid-stat {{vCPUs}}/{{host.CPUs['cpu_count']}}
|
||||
.col-sm-6.col-lg-4
|
||||
p.stat-name RAM used:
|
||||
p.center.mid-stat {{host.memory.usage | bytesToSize}}
|
||||
.col-sm-4.visible-lg
|
||||
p.stat-name Running VMs:
|
||||
p.center.mid-stat {{vms | count}}
|
||||
.row
|
||||
p.center(ng-if="refreshStatControl.running")
|
||||
i.fa.fa-circle-o-notch.fa-spin.fa-2x
|
||||
| Fetching stats...
|
||||
.row.hidden-lg
|
||||
.col-sm-12
|
||||
br
|
||||
p.stat-name {{vms | count}} running VMs
|
||||
//- Action panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
@@ -84,42 +200,47 @@
|
||||
.panel-body.text-center
|
||||
.grid
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add SR", type="button", style="width: 90%", xo-sref="SRs_new({container: host.UUID})")
|
||||
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.UUID})")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
button.btn(tooltip="Enable host", type="button", style="width: 90%", xo-click="enableHost(host.id)")
|
||||
i.fa.fa-check-circle.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Restart toolstack", type="button", style="width: 90%", xo-click="restartToolStack(host.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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)'
|
||||
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%", ng-repeat="controller in [host.controller] | resolve track by controller.UUID", xo-sref="consoles_view({id: controller.UUID})")
|
||||
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
|
||||
@@ -130,9 +251,9 @@
|
||||
| Memory
|
||||
.panel-body.text-center
|
||||
.progress
|
||||
.progress-bar-host(ng-repeat="controller in [host.controller] | resolve track by controller.UUID", role="progressbar", aria-valuemin="0", aria-valuenow="{{controller.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[controller.memory.size, host.memory.size] | %}}", tooltip="{{host.name_label}}: {{[controller.memory.size, host.memory.size] | %}}")
|
||||
.progress-bar-host(role="progressbar", aria-valuemin="0", aria-valuenow="{{controller.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[controller.memory.size, host.memory.size] | percentage}}", tooltip="{{host.name_label}}: {{[controller.memory.size, host.memory.size] | percentage}}")
|
||||
small {{host.name_label}}
|
||||
.progress-bar.progress-bar-vm(ng-repeat="VM in host.VMs | resolve | orderBy:natural('name_label') track by VM.UUID", role="progressbar", aria-valuemin="0", aria-valuenow="{{VM.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[VM.memory.size, host.memory.size] | %}}", xo-sref="VMs_view({id: VM.UUID})", tooltip="{{VM.name_label}}: {{[VM.memory.size, host.memory.size] | %}}")
|
||||
.progress-bar.progress-bar-vm(ng-repeat="VM in vms | map | orderBy:natural('name_label') track by VM.id", role="progressbar", aria-valuemin="0", aria-valuenow="{{VM.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[VM.memory.size, host.memory.size] | percentage}}", xo-sref="VMs_view({id: VM.id})", tooltip="{{VM.name_label}}: {{[VM.memory.size, host.memory.size] | percentage}}")
|
||||
small {{VM.name_label}}
|
||||
ul.list-inline.text-center
|
||||
li Total: {{host.memory.size | bytesToSize}}
|
||||
@@ -155,55 +276,55 @@
|
||||
th Status
|
||||
//- TODO: display PBD status for each SR of this host (connected or not)
|
||||
//- Shared SR
|
||||
tr(xo-sref="SRs_view({id: SR.UUID})", ng-repeat="SR in pool.SRs | resolve | orderBy:natural('name_label') track by SR.UUID")
|
||||
td
|
||||
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in sharedSrs | map | orderBy:natural('name_label') track by SR.id")
|
||||
td.oneliner
|
||||
| {{SR.name_label}}
|
||||
td {{SR.SR_type}}
|
||||
td {{SR.size | bytesToSize}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | %}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | %}}")
|
||||
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | %}}", tooltip="Allocated: {{[(SR.usage), SR.size] | %}}")
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.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.ref].attached")
|
||||
td(ng-if="SRsToPBDs[SR.id].attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.ref].ref)")
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)")
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!SRsToPBDs[SR.ref].attached")
|
||||
td(ng-if="!SRsToPBDs[SR.id].attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.ref].ref)")
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)")
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.ref].ref)")
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)")
|
||||
i.fa.fa-ban.fa-lg
|
||||
//- Local SR
|
||||
//- TODO: migrate to SRs and not PBDs when implemented in xo-server spec
|
||||
tr(xo-sref="SRs_view({id: SR.UUID})", ng-repeat="SR in host.SRs | resolve | orderBy:natural('name_label') track by SR.UUID")
|
||||
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in srs | map | orderBy:natural('name_label') track by SR.id")
|
||||
td
|
||||
| {{SR.name_label}}
|
||||
td {{SR.SR_type}}
|
||||
td {{SR.size | bytesToSize}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | %}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | %}}")
|
||||
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | %}}", tooltip="Allocated: {{[(SR.usage), SR.size] | %}}")
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.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.ref].attached")
|
||||
td(ng-if="SRsToPBDs[SR.id].attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.ref].ref)")
|
||||
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)")
|
||||
i.fa.fa-unlink.fa-lg
|
||||
td(ng-if="!SRsToPBDs[SR.ref].attached")
|
||||
td(ng-if="!SRsToPBDs[SR.id].attached")
|
||||
span.label.label-default Disconnected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.ref].ref)")
|
||||
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)")
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.ref].ref)")
|
||||
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)")
|
||||
i.fa.fa-ban.fa-lg
|
||||
//- Interfaces panel
|
||||
//- Networks/Interfaces panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
@@ -212,59 +333,101 @@
|
||||
.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.UUID")
|
||||
tr(ng-repeat="PIF in host.$PIFs | resolve | orderBy:natural('name_label') track by PIF.id")
|
||||
td
|
||||
| {{PIF.device}}
|
||||
span(ng-if="PIF.vlan > -1") .{{PIF.vlan}}
|
||||
span.label.label-primary(ng-if="PIF.management") XAPI
|
||||
td {{PIF.IP}} ({{PIF.mode}})
|
||||
td {{PIF.MAC}}
|
||||
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.ref)")
|
||||
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.ref)")
|
||||
a(tooltip="Connect this interface", xo-click="connectPIF(PIF.id)")
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Forget this interface", xo-click="removePIF(PIF.ref)")
|
||||
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
|
||||
select.form-control(ng-model = 'newNetworkPIF', ng-change = 'updateMTU(newNetworkPIF)', ng-options='(PIF | resolve).device for PIF in host.$PIFs')
|
||||
option(value = '', disabled) None
|
||||
|
|
||||
.form-group
|
||||
label.control-label(for = 'newNetworkName') Name
|
||||
input#newNetworkName.form-control(type = 'text', ng-model = 'newNetworkName', required)
|
||||
|
|
||||
.form-group
|
||||
label.control-label(for = 'newNetworkDescription') Description
|
||||
input#newNetworkDescription.form-control(type = 'text', ng-model = 'newNetworkDescription', placeholder= 'Network created with Xen Orchestra')
|
||||
|
|
||||
.form-group
|
||||
label.control-label(for = 'newNetworkVlan') VLAN
|
||||
input#newNetworkVlan.form-control(type = 'text', ng-model = 'newNetworkVlan', placeholder = 'Defaut: no VLAN')
|
||||
|
|
||||
.form-group
|
||||
label(for = 'newNetworkMTU') MTU
|
||||
input#newNetworkMTU.form-control(type = 'text', ng-model = 'newNetworkMTU', placeholder = 'Default: 1500')
|
||||
|
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
i.fa.fa-plus-square
|
||||
| Create
|
||||
span(ng-if = 'createNetworkWaiting')
|
||||
|
|
||||
i.fa.fa-spin.fa-circle-o-notch
|
||||
br
|
||||
//- CPU and Logs panels
|
||||
.grid
|
||||
//- Task panel
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title(ng-if="host.tasks.length")
|
||||
.panel-heading.panel-title(ng-if="tasks | isNotEmpty")
|
||||
i.fa.fa-spinner.fa-pulse(style="color: #e25440;")
|
||||
| Pending tasks
|
||||
.panel-heading.panel-title(ng-if="!host.tasks.length")
|
||||
.panel-heading.panel-title(ng-if="tasks | isEmpty")
|
||||
i.fa.fa-spinner(style="color: #e25440;")
|
||||
| Pending tasks
|
||||
.panel-body
|
||||
p.center(ng-if="!host.tasks.length") No recent tasks
|
||||
table.table.table-hover(ng-if="host.tasks.length")
|
||||
p.center(ng-if="tasks | isEmpty") No recent tasks
|
||||
table.table.table-hover(ng-if="tasks | isNotEmpty")
|
||||
th Date
|
||||
th Progress
|
||||
th Name
|
||||
//- TODO: working reverse order, from recent to oldest
|
||||
tr(ng-repeat="task in host.tasks | resolve | orderBy:'created':true track by task.UUID")
|
||||
td {{task.created}}
|
||||
tr(ng-repeat="task in tasks | map | orderBy:'created':true track by task.id")
|
||||
td.oneliner {{task.created * 1e3 | date:'medium'}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar.progress-bar-success.progress-bar-striped.active.progress-bar-black(role="progressbar", aria-valuemin="0", aria-valuenow="{{task.progress*100}}", aria-valuemax="100", style="width: {{task.progress*100}}%", tooltip="Progress: {{task.progress*100 | number:1}}%")
|
||||
| {{task.progress*100 | number:1}}%
|
||||
td
|
||||
td.oneliner
|
||||
| {{task.name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="cancelTask(task.UUID)")
|
||||
a(xo-click="cancelTask(task.id)")
|
||||
i.fa.fa-times.fa-lg(tooltip="Cancel this task")
|
||||
a(xo-click="destroyTask(task.UUID)")
|
||||
a(xo-click="destroyTask(task.id)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Destroy this task")
|
||||
|
||||
|
||||
@@ -273,44 +436,87 @@
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-comments(style="color: #e25440;")
|
||||
| Logs
|
||||
span.quick-edit(ng-if="host.messages.length", tooltip="Remove all logs", ng-click="deleteAllLog()")
|
||||
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.length") No recent logs
|
||||
table.table.table-hover(ng-if="host.messages.length")
|
||||
p.center(ng-if="host.messages | isEmpty") No recent logs
|
||||
table.table.table-hover(ng-if="host.messages | isNotEmpty")
|
||||
th Date
|
||||
th Name
|
||||
tr(ng-repeat="message in host.messages | resolve | orderBy:'-time' track by message.UUID")
|
||||
tr(ng-repeat="message in host.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
| {{message.name}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="deleteLog(message.UUID)")
|
||||
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')
|
||||
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
|
||||
//- Patches panel
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-file-code-o(style="color: #e25440;")
|
||||
| Patches
|
||||
span.quick-edit(ng-click="listMissingPatches(host.id)", tooltip="Check for updates")
|
||||
i.fa.fa-question-circle
|
||||
.panel-body
|
||||
p.center(ng-if="!host.patches.length") No patches
|
||||
table.table.table-hover(ng-if="host.patches.length")
|
||||
th Applied on
|
||||
th Name
|
||||
th Description
|
||||
th Status
|
||||
tr(ng-repeat="patch in host.patches | resolve | orderBy:'-time'")
|
||||
td {{patch.time*1e3 | date:"medium"}}
|
||||
td {{(patch.pool_patch | resolve).name_label}}
|
||||
td {{(patch.pool_patch | resolve).name_description}}
|
||||
//- TODO: allow patch application and removal
|
||||
table.table.table-hover(ng-if="poolPatches || updates")
|
||||
th.col-sm-2 Name
|
||||
th.col-sm-5 Description
|
||||
th.col-sm-3 Applied/Released date
|
||||
th.col-sm-1 Size
|
||||
th.col-sm-1 Status
|
||||
tr(ng-repeat="patch in updates")
|
||||
td.oneliner {{patch.name}}
|
||||
td.oneliner
|
||||
a(href="{{patch.documentationUrl}}", target="_blank") {{patch.description}}
|
||||
td.oneliner {{patch.date | date:"medium"}}
|
||||
td -
|
||||
td
|
||||
span(ng-if="patch.applied")
|
||||
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to install the patch on this host")
|
||||
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-if="!patch.applied")
|
||||
span.label.label-error Not 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.UUID)")
|
||||
//- a(xo-click="deletePatch(patch.id)")
|
||||
//- i.fa.fa-trash-o.fa-lg(tooltip="Remove this patch")
|
||||
.center(ng-if = '(poolPatches | count) > 5')
|
||||
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
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-plug(style="color: #e25440;")
|
||||
| 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(style="color: #e25440;")
|
||||
| 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=">>")
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
angular = require 'angular'
|
||||
|
||||
contains = require('lodash.contains')
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = angular.module 'xoWebApp.isoDevice', []
|
||||
|
||||
.directive 'isoDevice', -> {
|
||||
restrict: 'E'
|
||||
template: require './view'
|
||||
scope: {
|
||||
isos: '='
|
||||
vm: '='
|
||||
}
|
||||
controller: 'IsoDevice as isoDevice'
|
||||
bindToController: true
|
||||
}
|
||||
|
||||
.controller 'IsoDevice', (xo) ->
|
||||
|
||||
this.eject = (VM) ->
|
||||
xo.vm.ejectCd VM.UUID
|
||||
|
||||
this.insert = (VM, disc_id) ->
|
||||
xo.vm.insertCd VM.UUID, disc_id, true
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
63
app/modules/iso-device/index.js
Normal file
63
app/modules/iso-device/index.js
Normal 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
|
||||
@@ -4,7 +4,7 @@
|
||||
select.form-control(
|
||||
ng-model = 'isoDevice.isos.mounted'
|
||||
ng-change = 'isoDevice.insert(isoDevice.vm, isoDevice.isos.mounted)'
|
||||
ng-options = 'iso.iso.UUID as iso.label group by iso.sr for iso in isoDevice.isos.opts'
|
||||
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
|
||||
|
||||
@@ -1,27 +1,33 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import xoApi from 'xo-api';
|
||||
import xoApi from 'xo-api'
|
||||
|
||||
import view from './view';
|
||||
import view from './view'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.list', [
|
||||
uiRouter,
|
||||
xoApi,
|
||||
xoApi
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('list', {
|
||||
url: '/list',
|
||||
controller: 'ListCtrl as list',
|
||||
template: view,
|
||||
});
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('ListCtrl', function (xoApi) {
|
||||
this.byTypes = xoApi.byTypes;
|
||||
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
|
||||
;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//- TODO: print a message when no entries.
|
||||
|
||||
//- If it's a (named) pool.
|
||||
.grid.flat-object(ng-repeat="pool in list.byTypes.pool | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by pool.UUID", ng-if="pool.name_label", xo-sref="pools_view({id: pool.UUID})")
|
||||
.grid.flat-object(ng-repeat="pool in list.pools.all | xoHideUnauthorized | filter: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
|
||||
@@ -18,13 +18,13 @@
|
||||
.grid-cell.flat-cell(ng-init="default_SR = (pool.default_SR | resolve)")
|
||||
div(ng-if="default_SR")
|
||||
| Default SR:
|
||||
a(ui-sref="SRs_view({id: default_SR.UUID})") {{default_SR.name_label}}
|
||||
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.UUID})") {{master.name_label}}
|
||||
a(ui-sref="hosts_view({id: master.id})") {{master.name_label}}
|
||||
div(ng-if="!master")
|
||||
em Unknown master.
|
||||
.grid-cell.flat-cell
|
||||
@@ -33,7 +33,7 @@
|
||||
div(ng-if="!pool.HA_enabled")
|
||||
| HA disabled
|
||||
.grid-cell.flat-cell
|
||||
| {{pool.$running_hosts.length}}/{{pool.hosts.length}} hosts
|
||||
| {{list.runningHostsByPool[pool.id] | count}}/{{list.hostsByPool[pool.id] | count}} hosts
|
||||
//- /Properties.
|
||||
//- Tags.
|
||||
.grid
|
||||
@@ -46,7 +46,7 @@
|
||||
//- /Properties & tags.
|
||||
//- /Pool.
|
||||
//- If it's a host.
|
||||
.grid.flat-object(ng-repeat="host in list.byTypes.host | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by host.UUID", xo-sref="hosts_view({id: host.UUID})")
|
||||
.grid.flat-object(ng-repeat="host in list.hosts.all | xoHideUnauthorized | filter: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}}")
|
||||
@@ -66,10 +66,10 @@
|
||||
//- | {{host.$vCPUs}} vCPUs used on {{host.CPUs["cpu_count"]}} cores
|
||||
.grid-cell.flat-cell
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{100*host.memory.usage/host.memory.size}}", aria-valuemax="100", style="width: {{[host.memory.usage, host.memory.size] | %}}", tooltip="RAM: {{[host.memory.usage, host.memory.size] | %}} allocated")
|
||||
| {{[host.memory.usage, host.memory.size] | %}}
|
||||
.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
|
||||
| {{host.VMs.length}} VMs running
|
||||
| {{list.vmsByContainer[host.id] | count}} VMs running
|
||||
//- /Properties.
|
||||
//- Tags.
|
||||
.grid
|
||||
@@ -82,7 +82,7 @@
|
||||
//- /Properties & tags.
|
||||
//- /Host.
|
||||
//- If it's a VM.
|
||||
.grid.flat-object(ng-repeat="VM in list.byTypes.VM | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by VM.UUID", xo-sref="VMs_view({id: VM.UUID})")
|
||||
.grid.flat-object(ng-repeat="VM in list.VMs.all | xoHideUnauthorized | filter: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}}")
|
||||
@@ -105,13 +105,13 @@
|
||||
.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.UUID})") {{container.name_label}}
|
||||
div(ng-if="'host' === container.type", ng-init="pool = (container.poolRef | resolve)")
|
||||
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.UUID})") {{container.name_label}}
|
||||
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
|
||||
small(ng-if="pool.name_label")
|
||||
| (
|
||||
a(ui-sref="pools_view({id: pool.UUID})") {{pool.name_label}}
|
||||
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
|
||||
| )
|
||||
//- /Properties.
|
||||
//- Tags.
|
||||
@@ -125,7 +125,7 @@
|
||||
//- /Properties & tags.
|
||||
//- /VM.
|
||||
//- If it's a SR.
|
||||
.grid.flat-object(ng-repeat="SR in list.byTypes.SR | xoHideUnauthorized | filter:listFilter | orderBy:natural('name_label') track by SR.UUID", xo-sref="SRs_view({id: SR.UUID})")
|
||||
.grid.flat-object(ng-repeat="SR in list.SRs.all | xoHideUnauthorized | filter: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
|
||||
@@ -140,17 +140,17 @@
|
||||
.grid-cell.flat-cell.flat-cell-description
|
||||
i {{SR.name_description}}
|
||||
.grid-cell.flat-cell
|
||||
| Usage: {{[SR.usage, SR.size] | %}} ({{SR.usage | bytesToSize}}/{{SR.size | bytesToSize}})
|
||||
| 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.UUID})") {{container.name_label}}
|
||||
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.UUID})") {{container.name_label}}
|
||||
a(ui-sref="hosts_view({id: container.id})") {{container.name_label}}
|
||||
//- /Properties.
|
||||
//- Tags.
|
||||
.grid
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import view from './view';
|
||||
import view from './view'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.login', [
|
||||
uiRouter,
|
||||
uiRouter
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('login', {
|
||||
url: '/login',
|
||||
controller: 'LoginCtrl',
|
||||
template: view,
|
||||
});
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('LoginCtrl', function($scope, $state, $rootScope, xoApi, notify) {
|
||||
var toState, toStateParams;
|
||||
{
|
||||
let tmp = $rootScope._login;
|
||||
if (tmp) {
|
||||
toState = tmp.state.name;
|
||||
toStateParams = tmp.stateParams;
|
||||
delete $rootScope._login;
|
||||
} else {
|
||||
toState = 'index';
|
||||
.controller('LoginCtrl', function ($scope, $state, $rootScope, xoApi, notify) {
|
||||
const {toState, toStateParams} = (function (login) {
|
||||
if (login) {
|
||||
delete $rootScope._login
|
||||
|
||||
return {
|
||||
toState: login.state.name,
|
||||
toStateParams: login.stateParams
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { toState: 'index' }
|
||||
})($rootScope._login)
|
||||
|
||||
$scope.$watch(() => xoApi.user, function (user) {
|
||||
// When the user is logged in, go the wanted view, fallbacks on
|
||||
// the index view if necessary.
|
||||
if (user) {
|
||||
$state.go(toState, toStateParams).catch(function () {
|
||||
$state.go('index');
|
||||
});
|
||||
$state.go('index')
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
Object.defineProperties($scope, {
|
||||
user: {
|
||||
get() {
|
||||
return xoApi.user;
|
||||
},
|
||||
},
|
||||
status: {
|
||||
get() {
|
||||
return xoApi.status;
|
||||
get () {
|
||||
return xoApi.user
|
||||
}
|
||||
},
|
||||
});
|
||||
status: {
|
||||
get () {
|
||||
return xoApi.status
|
||||
}
|
||||
}
|
||||
})
|
||||
$scope.logIn = (...args) => {
|
||||
xoApi.logIn(...args).catch(error => {
|
||||
notify.warning({
|
||||
title: 'Authentication failed',
|
||||
message: error.message,
|
||||
});
|
||||
});
|
||||
};
|
||||
message: error.message
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -16,24 +16,28 @@ div.container
|
||||
legend.login: h3 Sign in
|
||||
div.form-group
|
||||
div.col-sm-12
|
||||
input.form-control.input-sm(
|
||||
name = 'email'
|
||||
type = 'text'
|
||||
placeholder = 'Username'
|
||||
ng-model = 'email'
|
||||
required
|
||||
fix-autofill
|
||||
)
|
||||
.input-group
|
||||
span.input-group-addon: i.fa.fa-user.fa-fw
|
||||
input.form-control.input-sm(
|
||||
name = 'email'
|
||||
type = 'text'
|
||||
placeholder = 'Username'
|
||||
ng-model = 'email'
|
||||
required
|
||||
fix-autofill
|
||||
)
|
||||
div.form-group
|
||||
div.col-sm-12
|
||||
input.form-control.input-sm(
|
||||
name = 'password'
|
||||
type = 'password'
|
||||
placeholder = 'Password'
|
||||
ng-model = 'password'
|
||||
required
|
||||
fix-autofill
|
||||
)
|
||||
.input-group
|
||||
span.input-group-addon: i.fa.fa-key.fa-fw
|
||||
input.form-control.input-sm(
|
||||
name = 'password'
|
||||
type = 'password'
|
||||
placeholder = 'Password'
|
||||
ng-model = 'password'
|
||||
required
|
||||
fix-autofill
|
||||
)
|
||||
div.form-group
|
||||
div.col-sm-12
|
||||
button.btn.btn-login.btn-block.btn-success(
|
||||
|
||||
@@ -1,59 +1,52 @@
|
||||
import angular from 'angular';
|
||||
import filter from 'lodash.filter';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import xoServices from 'xo-services';
|
||||
import updater from '../updater'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import view from './view';
|
||||
import view from './view'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.navbar', [
|
||||
uiRouter,
|
||||
|
||||
xoServices,
|
||||
updater,
|
||||
xoServices
|
||||
])
|
||||
.controller('NavbarCtrl', function ($state, xoApi, xo, $scope) {
|
||||
.controller('NavbarCtrl', function ($state, xoApi, xo, $scope, updater) {
|
||||
this.updater = updater
|
||||
// TODO: It would make sense to inject xoApi in the scope.
|
||||
Object.defineProperties(this, {
|
||||
status: {
|
||||
get: () => xoApi.status,
|
||||
get: () => xoApi.status
|
||||
},
|
||||
user: {
|
||||
get: () => xoApi.user,
|
||||
},
|
||||
});
|
||||
this.logIn = xoApi.logIn;
|
||||
get: () => xoApi.user
|
||||
}
|
||||
})
|
||||
this.logIn = xoApi.logIn
|
||||
this.logOut = function () {
|
||||
xoApi.logOut();
|
||||
$state.go('login');
|
||||
};
|
||||
xoApi.logOut()
|
||||
$state.go('login')
|
||||
}
|
||||
|
||||
// When a searched is entered, we must switch to the list view if
|
||||
// necessary.
|
||||
this.ensureListView = function () {
|
||||
$state.go('list');
|
||||
};
|
||||
$state.go('list')
|
||||
}
|
||||
|
||||
const ALIVE_STATUS = {
|
||||
cancelling: true,
|
||||
pending: true,
|
||||
};
|
||||
let {canAccess} = xo;
|
||||
let sieve = (task) => ALIVE_STATUS[task.status] && canAccess(task.$host);
|
||||
$scope.$watchCollection(() => xoApi.byTypes.task, (tasks) => {
|
||||
this.tasks = filter(tasks, sieve);
|
||||
});
|
||||
this.tasks = xoApi.getView('runningTasks')
|
||||
})
|
||||
.directive('navbar', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
controller: 'NavbarCtrl as navbar',
|
||||
template: view,
|
||||
scope: {},
|
||||
};
|
||||
scope: {}
|
||||
}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -32,9 +32,9 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
|
||||
//- /Search form.
|
||||
ul.nav.navbar-nav
|
||||
li
|
||||
a(href="https://xen-orchestra.com/pricing.html?utm=xoa", target="_blank")
|
||||
i.xo-icon-info
|
||||
| Unregistered version
|
||||
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")
|
||||
@@ -44,15 +44,15 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
|
||||
i.fa.fa-refresh.fa-spin
|
||||
| Connecting to XO-Server
|
||||
//- Running tasks
|
||||
li.disabled(ng-if="!navbar.tasks.length", tooltip="No running tasks")
|
||||
li.disabled(ng-if="!navbar.tasks.size", tooltip="No running tasks")
|
||||
a.dropdown-toggle.inverse
|
||||
i.xo-icon-task
|
||||
li.dropdown(dropdown, ng-if="navbar.tasks.length")
|
||||
li.dropdown(dropdown, ng-if="navbar.tasks.size")
|
||||
a.dropdown-toggle.inverse(dropdown-toggle)
|
||||
i.xo-icon-task
|
||||
ul.dropdown-menu.inverse
|
||||
li.task-menu(
|
||||
ng-repeat="task in navbar.tasks | orderBy:natural('name_label') track by task.id"
|
||||
ng-repeat="task in navbar.tasks.all | orderBy:natural('name_label') track by task.id"
|
||||
)
|
||||
a(
|
||||
ui-sref="hosts_view({id: task.$host})"
|
||||
@@ -90,22 +90,16 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
|
||||
//- i.fa.fa-sitemap
|
||||
//- | Graphs view
|
||||
li.divider
|
||||
//- li.disabled
|
||||
//- a
|
||||
//- i.fa.fa-clock-o
|
||||
//- | Scheduler
|
||||
li
|
||||
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 = 'admin.index')
|
||||
i.fa.fa-cog
|
||||
| ACLs
|
||||
li(
|
||||
ui-sref-active = 'active'
|
||||
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
|
||||
)
|
||||
a(ui-sref="settings")
|
||||
a(ui-sref="settings.index")
|
||||
i.fa.fa-cog
|
||||
| Settings
|
||||
li.divider
|
||||
@@ -116,9 +110,18 @@ nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
|
||||
//- /Main menu.
|
||||
|
||||
li
|
||||
a
|
||||
a(ui-sref="settings.update")
|
||||
i.fa.fa-question-circle.text-warning(ng-if = '!navbar.updater.state', tooltip = 'No update information available')
|
||||
i.fa.fa-question-circle.text-info(ng-if = 'navbar.updater.state == "connected"', tooltip = 'Update information may be available')
|
||||
i.fa.fa-check.text-success(ng-if = 'navbar.updater.state == "upToDate"', tooltip = 'Your XOA is up-to-date')
|
||||
i.fa.fa-bell.text-primary(ng-if = 'navbar.updater.state == "upgradeNeeded"', tooltip = 'You need to update your XOA (new version is available)')
|
||||
i.fa.fa-bell-slash.text-warning(ng-if = 'navbar.updater.state == "registerNeeded"', tooltip = 'Your XOA is not registered for updates')
|
||||
i.fa.fa-exclamation-triangle.text-danger(ng-if = 'navbar.updater.state == "error"', tooltip = 'Can\'t fetch update information')
|
||||
|
||||
li
|
||||
a(ui-sref="settings.users", tooltip="{{navbar.user.email}}")
|
||||
i.fa.fa-user
|
||||
| {{navbar.user.email}}
|
||||
span.hidden-sm {{navbar.user.email}}
|
||||
li
|
||||
a(ng-click = 'navbar.logOut()')
|
||||
i.fa.fa-sign-out
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import angular from 'angular';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import Bluebird from 'bluebird';
|
||||
import angular from 'angular'
|
||||
import Bluebird from 'bluebird'
|
||||
import forEach from 'lodash.foreach'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import view from './view';
|
||||
import _indexOf from 'lodash.indexof';
|
||||
import view from './view'
|
||||
import _indexOf from 'lodash.indexof'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.newSr', [
|
||||
uiRouter
|
||||
@@ -14,451 +15,421 @@ export default angular.module('xoWebApp.newSr', [
|
||||
$stateProvider.state('SRs_new', {
|
||||
url: '/srs/new/:container',
|
||||
controller: 'NewSrCtrl as newSr',
|
||||
template: view,
|
||||
});
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('NewSrCtrl', function ($scope, $state, $stateParams, xo, xoApi, notify, modal, bytesToSizeFilter) {
|
||||
|
||||
this.reset = function (data = {}) {
|
||||
|
||||
this.data = {};
|
||||
delete this.lockCreation;
|
||||
this.data = {}
|
||||
delete this.lockCreation
|
||||
this.lock = !(
|
||||
('Local' === data.srType) &&
|
||||
(data.srType === 'Local') &&
|
||||
(data.srPath && data.srPath.path)
|
||||
);
|
||||
)
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
this.resetLists = function() {
|
||||
this.resetLists = function () {
|
||||
delete this.data.nfsList
|
||||
delete this.data.scsiList
|
||||
delete this.lockCreation
|
||||
this.lock = true
|
||||
|
||||
delete this.data.nfsList;
|
||||
delete this.data.scsiList;
|
||||
delete this.lockCreation;
|
||||
this.lock = true;
|
||||
|
||||
this.resetErrors();
|
||||
|
||||
};
|
||||
this.resetErrors()
|
||||
}
|
||||
|
||||
this.resetErrors = function () {
|
||||
|
||||
delete this.data.error;
|
||||
|
||||
};
|
||||
delete this.data.error
|
||||
}
|
||||
|
||||
/*
|
||||
* Loads NFS paths and iScsi iqn`s
|
||||
*/
|
||||
this.populateSettings = function (type, server, auth, user, password) {
|
||||
|
||||
this.reset();
|
||||
this.loading = true;
|
||||
this.reset()
|
||||
this.loading = true
|
||||
|
||||
server = this._parseAddress(server);
|
||||
|
||||
if ('NFS' === type || 'NFS_ISO' === type) {
|
||||
server = this._parseAddress(server)
|
||||
|
||||
if (type === 'NFS' || type === 'NFS_ISO') {
|
||||
xoApi.call('sr.probeNfs', {
|
||||
host: this.container.UUID,
|
||||
host: this.container.id,
|
||||
server: server.host
|
||||
})
|
||||
.then(response => this.data.paths = response)
|
||||
.catch(error => notify.warning({
|
||||
title : 'NFS Detection',
|
||||
message : error.message
|
||||
title: 'NFS Detection',
|
||||
message: error.message
|
||||
}))
|
||||
.finally(() => this.loading = false)
|
||||
;
|
||||
|
||||
} else if ('iSCSI' === type) {
|
||||
|
||||
} else if (type === 'iSCSI') {
|
||||
let params = {
|
||||
host: this.container.UUID
|
||||
};
|
||||
|
||||
if (auth) {
|
||||
params.chapUser = user;
|
||||
params.chapPassword = password;
|
||||
host: this.container.id
|
||||
}
|
||||
|
||||
params.target = server.host;
|
||||
if (auth) {
|
||||
params.chapUser = user
|
||||
params.chapPassword = password
|
||||
}
|
||||
|
||||
params.target = server.host
|
||||
if (server.port) {
|
||||
params.port = server.port;
|
||||
params.port = server.port
|
||||
}
|
||||
|
||||
xoApi.call('sr.probeIscsiIqns', params)
|
||||
.then(response => {
|
||||
|
||||
if (response.length > 0) {
|
||||
this.data.iqns = response;
|
||||
this.data.iqns = response
|
||||
} else {
|
||||
notify.warning({
|
||||
title : 'iSCSI Detection',
|
||||
message : 'No IQNs found'
|
||||
});
|
||||
title: 'iSCSI Detection',
|
||||
message: 'No IQNs found'
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
.catch(error => notify.warning({
|
||||
title : 'iSCSI Detection',
|
||||
message : error.message
|
||||
title: 'iSCSI Detection',
|
||||
message: error.message
|
||||
}))
|
||||
.finally(() => this.loading = false)
|
||||
;
|
||||
|
||||
} else {
|
||||
|
||||
this.loading = false;
|
||||
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Loads iScsi LUNs
|
||||
*/
|
||||
this.populateIScsiIds = function (iqn, auth, user, password) {
|
||||
|
||||
delete this.data.iScsiIds;
|
||||
this.loading = true;
|
||||
delete this.data.iScsiIds
|
||||
this.loading = true
|
||||
|
||||
let params = {
|
||||
host: this.container.UUID,
|
||||
host: this.container.id,
|
||||
target: iqn.ip,
|
||||
targetIqn: iqn.iqn
|
||||
};
|
||||
}
|
||||
|
||||
if (auth) {
|
||||
params.chapUser = user;
|
||||
params.chapPassword = password;
|
||||
params.chapUser = user
|
||||
params.chapPassword = password
|
||||
}
|
||||
|
||||
xoApi.call('sr.probeIscsiLuns', params)
|
||||
.then(response => {
|
||||
|
||||
response.forEach(item => {
|
||||
forEach(response, item => {
|
||||
item.display = 'LUN ' + item.id + ': ' +
|
||||
item.serial + ' ' + bytesToSizeFilter(item.size) +
|
||||
' (' + item.vendor + ')';
|
||||
});
|
||||
' (' + item.vendor + ')'
|
||||
})
|
||||
|
||||
this.data.iScsiIds = response;
|
||||
this.data.iScsiIds = response
|
||||
})
|
||||
.catch(error => notify.warning({
|
||||
title : 'LUNs Detection',
|
||||
message : error.message
|
||||
title: 'LUNs Detection',
|
||||
message: error.message
|
||||
}))
|
||||
.finally(() => this.loading = false)
|
||||
;
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
this._parseAddress = function (address) {
|
||||
|
||||
let index = address.indexOf(':');
|
||||
let port = false;
|
||||
let host = address;
|
||||
if (-1 < index) {
|
||||
port = address.substring(index + 1);
|
||||
host = address.substring(0, index);
|
||||
let index = address.indexOf(':')
|
||||
let port = false
|
||||
let host = address
|
||||
if (index > -1) {
|
||||
port = address.substring(index + 1)
|
||||
host = address.substring(0, index)
|
||||
}
|
||||
return {
|
||||
host,
|
||||
port
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
this._prepareNfsParams = function (data) {
|
||||
|
||||
let server = this._parseAddress(data.srServer);
|
||||
let server = this._parseAddress(data.srServer)
|
||||
|
||||
let params = {
|
||||
host: this.container.UUID,
|
||||
host: this.container.id,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
server: server.host,
|
||||
serverPath: data.srPath.path
|
||||
};
|
||||
}
|
||||
|
||||
return params;
|
||||
|
||||
};
|
||||
|
||||
this._prepareScsiParams = function(data) {
|
||||
return params
|
||||
}
|
||||
|
||||
this._prepareScsiParams = function (data) {
|
||||
let params = {
|
||||
host: this.container.UUID,
|
||||
host: this.container.id,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
target: data.srIqn.ip,
|
||||
targetIqn: data.srIqn.iqn,
|
||||
scsiId: data.srIScsiId.scsiId,
|
||||
};
|
||||
scsiId: data.srIScsiId.scsiId
|
||||
}
|
||||
|
||||
let server = this._parseAddress(data.srServer);
|
||||
let server = this._parseAddress(data.srServer)
|
||||
if (server.port) {
|
||||
params.port = server.port;
|
||||
params.port = server.port
|
||||
}
|
||||
if (data.srAuth) {
|
||||
params.chapUser = data.srChapUser;
|
||||
params.chapPassword = data.srChapPassword;
|
||||
params.chapUser = data.srChapUser
|
||||
params.chapPassword = data.srChapPassword
|
||||
}
|
||||
|
||||
return params;
|
||||
|
||||
};
|
||||
return params
|
||||
}
|
||||
|
||||
this.createSR = function (data) {
|
||||
this.lock = true
|
||||
this.creating = true
|
||||
|
||||
this.lock = true;
|
||||
this.creating = true;
|
||||
let operationToPromise
|
||||
|
||||
let operationToPromise;
|
||||
|
||||
switch(data.srType) {
|
||||
switch (data.srType) {
|
||||
case 'NFS':
|
||||
|
||||
let nfsParams = this._prepareNfsParams(data);
|
||||
let nfsParams = this._prepareNfsParams(data)
|
||||
operationToPromise = this._checkNfsExistence(nfsParams)
|
||||
.then(() => xoApi.call('sr.createNfs', nfsParams))
|
||||
;
|
||||
break;
|
||||
.then(() => xoApi.call('sr.createNfs', nfsParams))
|
||||
break
|
||||
|
||||
case 'iSCSI':
|
||||
let scsiParams = this._prepareScsiParams(data)
|
||||
operationToPromise = this._checkScsiExistence(scsiParams)
|
||||
.then(() => xoApi.call('sr.createIscsi', scsiParams))
|
||||
break
|
||||
|
||||
let scsiParams = this._prepareScsiParams(data);
|
||||
operationToPromise = this._checkScsiExistence(scsiParams)
|
||||
.then(() => xoApi.call('sr.createIscsi', scsiParams))
|
||||
;
|
||||
break;
|
||||
case 'lvm':
|
||||
let device = data.srDevice.device
|
||||
|
||||
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 server = this._parseAddress(data.srServer || '');
|
||||
|
||||
let path = (('NFS_ISO' === data.srType) ?
|
||||
server.host + ':' :
|
||||
'') + data.srPath.path;
|
||||
let path = (
|
||||
data.srType === 'NFS_ISO' ?
|
||||
server.host + ':' :
|
||||
''
|
||||
) + data.srPath.path
|
||||
|
||||
operationToPromise = xoApi.call('sr.createIso', {
|
||||
host: this.container.UUID,
|
||||
host: this.container.id,
|
||||
nameLabel: data.srName,
|
||||
nameDescription: data.srDesc,
|
||||
path
|
||||
});
|
||||
break;
|
||||
default:
|
||||
})
|
||||
break
|
||||
|
||||
operationToPromise = Bluebird.reject({message: 'Unhanled SR Type'});
|
||||
break;
|
||||
default:
|
||||
operationToPromise = Bluebird.reject({message: 'Unhanled SR Type'})
|
||||
break
|
||||
}
|
||||
|
||||
operationToPromise
|
||||
.then(id => {
|
||||
$state.go('SRs_view', {id});
|
||||
$state.go('SRs_view', {id})
|
||||
})
|
||||
.catch(error => {
|
||||
notify.error({
|
||||
title : 'Storage Creation Error',
|
||||
message : error.message
|
||||
});
|
||||
title: 'Storage Creation Error',
|
||||
message: error.message
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
this.lock = false;
|
||||
this.creating = false;
|
||||
this.lock = false
|
||||
this.creating = false
|
||||
})
|
||||
;
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
this._checkScsiExistence = function (params) {
|
||||
|
||||
this.resetLists();
|
||||
this.resetLists()
|
||||
|
||||
return xoApi.call('sr.probeIscsiExists', params)
|
||||
.then(response => {
|
||||
if (response.length > 0) {
|
||||
this.data.scsiList = response;
|
||||
this.data.scsiList = response
|
||||
return modal.confirm({
|
||||
title: 'Previous LUN Usage',
|
||||
message: 'This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation. Are you sure?'
|
||||
});
|
||||
} else {
|
||||
return Bluebird.resolve(true);
|
||||
})
|
||||
}
|
||||
})
|
||||
;
|
||||
|
||||
};
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
this._checkNfsExistence = function (params) {
|
||||
|
||||
this.resetLists();
|
||||
this.resetLists()
|
||||
|
||||
return xoApi.call('sr.probeNfsExists', params)
|
||||
.then(response => {
|
||||
if (response.length > 0) {
|
||||
this.data.nfsList = response;
|
||||
this.data.nfsList = response
|
||||
return modal.confirm({
|
||||
title: 'Previous Path Usage',
|
||||
message: 'This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation. Are you sure?'
|
||||
});
|
||||
} else {
|
||||
return Bluebird.resolve(true);
|
||||
})
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
;
|
||||
}
|
||||
|
||||
};
|
||||
const hostsByPool = xoApi.getIndex('hostsByPool')
|
||||
const srsByContainer = xoApi.getIndex('srsByContainer')
|
||||
this._gatherConnectedUuids = function () {
|
||||
const srIds = []
|
||||
|
||||
this._gatherConnectedUuids = function() {
|
||||
// Shared SRs.
|
||||
forEach(srsByContainer[this.container.$poolId], sr => {
|
||||
srIds.push(sr.id)
|
||||
})
|
||||
|
||||
let SRs = [];
|
||||
// Local SRs.
|
||||
forEach(hostsByPool[this.container.$poolId], host => {
|
||||
forEach(srsByContainer[host.id], sr => {
|
||||
srIds.push(sr.id)
|
||||
})
|
||||
})
|
||||
|
||||
let pool = xoApi.get(this.container.poolRef);
|
||||
pool.SRs.forEach(ref => SRs.push(xoApi.get(ref).UUID));
|
||||
let hosts = [];
|
||||
pool.hosts.forEach(ref => hosts.push(xoApi.get(ref)));
|
||||
hosts.forEach(h => h.SRs.forEach(ref => SRs.push(xoApi.get(ref).UUID)));
|
||||
|
||||
return SRs;
|
||||
|
||||
};
|
||||
return srIds
|
||||
}
|
||||
|
||||
this._processSRList = function (list) {
|
||||
let inUse = false
|
||||
let SRs = this._gatherConnectedUuids()
|
||||
|
||||
let inUse = false;
|
||||
let SRs = this._gatherConnectedUuids();
|
||||
forEach(list, item => {
|
||||
inUse = (item.used = _indexOf(SRs, item.uuid) > -1) || inUse
|
||||
})
|
||||
|
||||
list.forEach(item => {
|
||||
inUse = (item.used = _indexOf(SRs, item.uuid) > -1) || inUse;
|
||||
});
|
||||
this.lockCreation = inUse
|
||||
|
||||
this.lockCreation = inUse;
|
||||
return list
|
||||
}
|
||||
|
||||
return list;
|
||||
this.loadScsiList = function (data) {
|
||||
this.resetLists()
|
||||
this.loading = true
|
||||
|
||||
};
|
||||
|
||||
this.loadScsiList = function(data) {
|
||||
|
||||
this.resetLists();
|
||||
|
||||
let params = this._prepareScsiParams(data);
|
||||
let params = this._prepareScsiParams(data)
|
||||
|
||||
xoApi.call('sr.probeIscsiExists', params)
|
||||
.then(response => {
|
||||
|
||||
if (response.length > 0) {
|
||||
this.data.scsiList = this._processSRList(response);
|
||||
this.data.scsiList = this._processSRList(response)
|
||||
}
|
||||
|
||||
this.lock = !Boolean(data.srIScsiId);
|
||||
this.lock = !Boolean(data.srIScsiId)
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
notify.error({
|
||||
title : 'iSCSI Error',
|
||||
message : error.message
|
||||
});
|
||||
title: 'iSCSI Error',
|
||||
message: error.message
|
||||
})
|
||||
})
|
||||
;
|
||||
|
||||
};
|
||||
.finally(() => this.loading = false)
|
||||
}
|
||||
|
||||
this.loadNfsList = function (data) {
|
||||
this.resetLists()
|
||||
|
||||
this.resetLists();
|
||||
|
||||
let server = this._parseAddress(data.srServer);
|
||||
let server = this._parseAddress(data.srServer)
|
||||
|
||||
xoApi.call('sr.probeNfsExists', {
|
||||
host: this.container.UUID,
|
||||
host: this.container.id,
|
||||
server: server.host,
|
||||
serverPath: data.srPath.path
|
||||
})
|
||||
.then(response => {
|
||||
|
||||
if (response.length > 0) {
|
||||
this.data.scsiList = this._processSRList(response);
|
||||
this.data.scsiList = this._processSRList(response)
|
||||
}
|
||||
|
||||
this.lock = !Boolean(data.srPath.path);
|
||||
this.lock = !Boolean(data.srPath.path)
|
||||
|
||||
})
|
||||
.catch(error => {
|
||||
notify.error({
|
||||
title : 'NFS error',
|
||||
message : error.message
|
||||
});
|
||||
title: 'NFS error',
|
||||
message: error.message
|
||||
})
|
||||
})
|
||||
;
|
||||
};
|
||||
}
|
||||
|
||||
this.reattachNfs = function (uuid, {name, nameError}, {desc, descError}, iso) {
|
||||
|
||||
this._reattach(uuid, 'nfs', {name, nameError}, {desc, descError}, iso);
|
||||
|
||||
};
|
||||
this._reattach(uuid, 'nfs', {name, nameError}, {desc, descError}, iso)
|
||||
}
|
||||
|
||||
this.reattachIScsi = function (uuid, {name, nameError}, {desc, descError}) {
|
||||
this._reattach(uuid, 'iscsi', {name, nameError}, {desc, descError})
|
||||
}
|
||||
|
||||
this._reattach(uuid, 'iscsi', {name, nameError}, {desc, descError});
|
||||
|
||||
};
|
||||
|
||||
this._reattach = function(uuid, type, {name, nameError}, {desc, descError}, iso = false) {
|
||||
|
||||
this.resetErrors();
|
||||
let method = 'sr.reattach' + (iso ? 'Iso' : '');
|
||||
this._reattach = function (uuid, type, {name, nameError}, {desc, descError}, iso = false) {
|
||||
this.resetErrors()
|
||||
let method = 'sr.reattach' + (iso ? 'Iso' : '')
|
||||
|
||||
if (nameError || descError) {
|
||||
this.data.error = {
|
||||
name: nameError,
|
||||
desc: descError
|
||||
};
|
||||
}
|
||||
notify.warning({
|
||||
title: 'Missing parameters',
|
||||
message: 'Complete the General section information, please'
|
||||
});
|
||||
})
|
||||
} else {
|
||||
this.lock = true;
|
||||
this.attaching = true;
|
||||
this.lock = true
|
||||
this.attaching = true
|
||||
xoApi.call(method, {
|
||||
host: this.container.UUID,
|
||||
host: this.container.id,
|
||||
uuid,
|
||||
nameLabel: name,
|
||||
nameDescription: desc,
|
||||
type
|
||||
})
|
||||
.then(id => {
|
||||
$state.go('SRs_view', {id});
|
||||
$state.go('SRs_view', {id})
|
||||
})
|
||||
.catch(error => notify.error({
|
||||
title : 'reattach',
|
||||
message : error.message
|
||||
title: 'reattach',
|
||||
message: error.message
|
||||
})
|
||||
)
|
||||
.finally(() => {
|
||||
this.lock = false;
|
||||
this.attaching = false;
|
||||
this.lock = false
|
||||
this.attaching = false
|
||||
})
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
this.reset();
|
||||
this.reset()
|
||||
|
||||
$scope.$watch(() => xoApi.get($stateParams.container), container => {
|
||||
this.container = container;
|
||||
});
|
||||
|
||||
this.container = container
|
||||
})
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
p.page-title
|
||||
i.xo-icon-sr
|
||||
| Add SR on
|
||||
a(ng-if="'pool' === newSr.container.type", ui-sref="pools_view({id: newSr.container.UUID})")
|
||||
a(ng-if="'pool' === newSr.container.type", ui-sref="pools_view({id: newSr.container.id})")
|
||||
| {{newSr.container.name_label}}
|
||||
a(ng-if="'host' === newSr.container.type", ui-sref="hosts_view({id: newSr.container.UUID})")
|
||||
a(ng-if="'host' === newSr.container.type", ui-sref="hosts_view({id: newSr.container.id})")
|
||||
| {{newSr.container.name_label}}
|
||||
form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
|
||||
.grid
|
||||
@@ -23,6 +23,7 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
|
||||
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
|
||||
@@ -40,7 +41,7 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
|
||||
i.fa.fa-cogs(style="color: #e25440;")
|
||||
| Settings
|
||||
.panel-body
|
||||
.form-group(ng-if = 'formData.srType && formData.srType !== "Local"')
|
||||
.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"')
|
||||
@@ -54,6 +55,20 @@ form.form-horizontal(name = 'srForm' ng-submit="newSr.createSR(formData)")
|
||||
button.btn.btn-default(type = 'button', ng-click = 'newSr.populateSettings(formData.srType, formData.srServer, formData.srAuth, formData.srChapUser, formData.srChapPassword)')
|
||||
i.fa.fa-search
|
||||
|
||||
//- For 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
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
angular = require 'angular'
|
||||
cloneDeep = require 'lodash.clonedeep'
|
||||
filter = require 'lodash.filter'
|
||||
forEach = require 'lodash.foreach'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
@@ -31,6 +34,57 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
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(
|
||||
-> get $stateParams.container
|
||||
(container) ->
|
||||
@@ -41,32 +95,13 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
|
||||
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 = {}
|
||||
@@ -80,6 +115,7 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
$scope.template = ''
|
||||
$scope.VDIs = []
|
||||
$scope.VIFs = []
|
||||
$scope.isDiskTemplate = false
|
||||
|
||||
$scope.addVIF = do ->
|
||||
id = 0
|
||||
@@ -124,7 +160,14 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
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
|
||||
@@ -143,9 +186,8 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
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
|
||||
@@ -158,7 +200,7 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
# 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
|
||||
@@ -187,7 +229,7 @@ module.exports = angular.module 'xoWebApp.newVm', [
|
||||
data = {
|
||||
installation
|
||||
name_label
|
||||
template: template.UUID
|
||||
template: template.id
|
||||
VDIs
|
||||
VIFs
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
p.page-title
|
||||
i.xo-icon-vm
|
||||
| Create VM on
|
||||
a(ng-if="'pool' === container.type", ui-sref="pools_view({id: container.UUID})")
|
||||
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.UUID})")
|
||||
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()")
|
||||
@@ -18,7 +18,7 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
.form-group
|
||||
label.col-sm-3.control-label Template
|
||||
.col-sm-9
|
||||
select.form-control(ng-model="template", ng-options="template.name_label for template in templates | orderBy:natural('name_label') track by template.UUID", required="")
|
||||
select.form-control(ng-model="template", ng-options="template.name_label for template in templates | orderBy:natural('name_label') track by template.id", required="")
|
||||
.form-group
|
||||
label.col-sm-3.control-label Name
|
||||
.col-sm-9
|
||||
@@ -40,7 +40,20 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
label.col-sm-3.control-label RAM
|
||||
.col-sm-9
|
||||
input.form-control(type="text", placeholder="{{template.memory.size | bytesToSize}}", ng-model="memory")
|
||||
.grid
|
||||
.grid(ng-if="isDiskTemplate")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-info-circle(style="color: #e25440;")
|
||||
| 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
|
||||
@@ -55,18 +68,18 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
input(
|
||||
type = 'radio'
|
||||
name = 'installation_method'
|
||||
ng-model = 'installation_method'
|
||||
ng-model = '$parent.installation_method'
|
||||
value = 'cdrom'
|
||||
required
|
||||
)
|
||||
select.form-control.disabled(
|
||||
ng-disabled="'cdrom' !== installation_method"
|
||||
ng-model="installation_cdrom"
|
||||
ng-model="$parent.installation_cdrom"
|
||||
required
|
||||
)
|
||||
option(value = '') Please select
|
||||
optgroup(ng-repeat="SR in ISO_SRs | orderBy:natural('name_label') track by SR.UUID", ng-if="SR.VDIs.length", label="{{SR.name_label}}")
|
||||
option(ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.UUID", ng-value="VDI.UUID")
|
||||
optgroup(ng-repeat="SR in ISO_SRs | orderBy:natural('name_label') track by SR.id", ng-if="SR.VDIs.length", label="{{SR.name_label}}")
|
||||
option(ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.id", ng-value="VDI.id")
|
||||
| {{VDI.name_label}}
|
||||
.form-group(ng-show="availableMethods.http || availableMethods.ftp || availableMethods.nfs")
|
||||
label.col-sm-3.control-label Network
|
||||
@@ -76,11 +89,11 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
input(
|
||||
type = 'radio'
|
||||
name = 'installation_method'
|
||||
ng-model = 'installation_method'
|
||||
ng-model = '$parent.installation_method'
|
||||
value = 'network'
|
||||
required
|
||||
)
|
||||
input.form-control(type="text", ng-disabled="'network' !== installation_method", placeholder="e.g: http://ftp.debian.org/debian", ng-model="installation_network")
|
||||
input.form-control(type="text", ng-disabled="'network' !== installation_method", placeholder="e.g: http://ftp.debian.org/debian", ng-model="$parent.installation_network")
|
||||
|
||||
//- <div class="form-group"> FIXME
|
||||
//- <label class="col-sm-3 control-label">Home server</label>
|
||||
@@ -108,7 +121,7 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
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.UUID as network.name_label for network in (networks | orderBy:natural("name_label"))'
|
||||
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
|
||||
)
|
||||
@@ -126,7 +139,7 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
| Add interface
|
||||
//- end of misc and interface panel
|
||||
//- Disk panel
|
||||
.grid
|
||||
.grid(ng-if="!isDiskTemplate")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-sr(style="color: #e25440;")
|
||||
@@ -143,7 +156,7 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
//- Buttons
|
||||
tr(ng-repeat="VDI in VDIs track by VDI.id")
|
||||
td
|
||||
select.form-control(ng-model="VDI.SR", ng-options="SR.UUID as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))")
|
||||
select.form-control(ng-model="VDI.SR", ng-options="SR.id as (SR.name_label + ' (' + (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
|
||||
@@ -177,26 +190,30 @@ form.form-horizontal(ng-submit="createVM()")
|
||||
.panel-body
|
||||
.grid
|
||||
.grid-cell
|
||||
p.stat-name
|
||||
| Name:
|
||||
p.center.big {{name_label}}
|
||||
.grid-cell
|
||||
p.stat-name
|
||||
| Template:
|
||||
p.center {{template.name_label}}
|
||||
|
|
||||
span.small(ng-if="template.name_label") ({{template.name_label}})
|
||||
.grid
|
||||
.grid-cell
|
||||
p.stat-name vCPUs
|
||||
p.center.big {{CPUs || template.CPUs.number}}
|
||||
//- p.stat-name vCPUs
|
||||
p.center.big(tooltip="vCPUs")
|
||||
| {{CPUs || template.CPUs.number || 0}}x
|
||||
i.xo-icon-cpu
|
||||
.grid-cell
|
||||
p.stat-name RAM
|
||||
p.center.big {{(memory) || (template.memory.size | bytesToSize)}}
|
||||
//- p.stat-name RAM
|
||||
p.center.big(tooltip="RAM")
|
||||
| {{(memory) || (template.memory.size | bytesToSize)}}
|
||||
i.xo-icon-memory
|
||||
.grid-cell
|
||||
p.stat-name Disks
|
||||
p.center.big {{(VDIs.length) || (template.$VBDs.length) || 0}}
|
||||
//- p.stat-name Disks
|
||||
p.center.big(tooltip="Disks")
|
||||
| {{(VDIs.length) || (template.$VBDs.length) || 0}}x
|
||||
i.fa.fa-hdd-o
|
||||
.grid-cell
|
||||
p.stat-name Interfaces
|
||||
p.center.big {{VIFs.length}}
|
||||
//- p.stat-name Interfaces
|
||||
p.center.big(tooltip="Network interfaces")
|
||||
| {{(VIFs.length) || (template.VIFs.length) || 0}}x
|
||||
i.xo-icon-network
|
||||
p.center
|
||||
button.btn.btn-lg.btn-primary(type="submit")
|
||||
i.fa.fa-play
|
||||
|
||||
@@ -1,60 +1,81 @@
|
||||
import angular from 'angular';
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import view from './view'
|
||||
|
||||
import view from './view';
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.pool', [
|
||||
uiRouter,
|
||||
uiRouter
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('pools_view', {
|
||||
url: '/pools/:id',
|
||||
controller: 'PoolCtrl',
|
||||
template: view,
|
||||
});
|
||||
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.pool = pool
|
||||
})
|
||||
|
||||
$scope.currentLogPage = 1
|
||||
|
||||
$scope.savePool = function ($data) {
|
||||
let {pool} = $scope;
|
||||
let {name_label, name_description} = $data;
|
||||
let {pool} = $scope
|
||||
let {name_label, name_description} = $data
|
||||
|
||||
$data = {
|
||||
id: pool.UUID,
|
||||
id: pool.id
|
||||
}
|
||||
if (name_label !== pool.name_label) {
|
||||
$data.name_label = name_label;
|
||||
$data.name_label = name_label
|
||||
}
|
||||
if (name_description !== pool.name_description) {
|
||||
$data.name_description = name_description;
|
||||
$data.name_description = name_description
|
||||
}
|
||||
|
||||
xoApi.call('pool.set', $data);
|
||||
};
|
||||
xoApi.call('pool.set', $data)
|
||||
}
|
||||
|
||||
$scope.deleteAllLog = function () {
|
||||
return modal.confirm({
|
||||
title: 'Log deletion',
|
||||
message: 'Are you sure you want to delete all the logs?',
|
||||
message: 'Are you sure you want to delete all the logs?'
|
||||
}).then(function () {
|
||||
// TODO: return all promises.
|
||||
angular.forEach($scope.pool.messages, function(value, key) {
|
||||
xo.log.delete(value);
|
||||
console.log('Remove log', value);
|
||||
});
|
||||
});
|
||||
};
|
||||
forEach($scope.pool.messages, function (message) {
|
||||
xo.log.delete(message.id)
|
||||
console.log('Remove log', message.id)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.deleteLog = function (id) {
|
||||
console.log('Remove log', id);
|
||||
return xo.log.delete(id);
|
||||
};
|
||||
console.log('Remove log', id)
|
||||
return xo.log.delete(id)
|
||||
}
|
||||
|
||||
// $scope.patchPool = ($files, id) ->
|
||||
// file = $files[0]
|
||||
@@ -84,4 +105,3 @@ export default angular.module('xoWebApp.pool', [
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
| {{pool.name_description}}
|
||||
dt Master
|
||||
dd(ng-repeat="master in [pool.master] | resolve")
|
||||
a(ui-sref="hosts_view({id: master.UUID})")
|
||||
a(ui-sref="hosts_view({id: master.id})")
|
||||
| {{master.name_label}}
|
||||
dt Tags
|
||||
dd
|
||||
@@ -32,7 +32,7 @@
|
||||
span.label.label-primary {{tag}}
|
||||
dt(ng-if="pool.default_SR") Default SR
|
||||
dd(ng-if="pool.default_SR", ng-init="default_SR = (pool.default_SR | resolve)")
|
||||
a(ui-sref="SRs_view({id: default_SR.UUID})") {{default_SR.name_label}}
|
||||
a(ui-sref="SRs_view({id: default_SR.id})") {{default_SR.name_label}}
|
||||
dt HA
|
||||
dd
|
||||
| {{pool.HA_enabled}}
|
||||
@@ -51,13 +51,13 @@
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-stats(style="color: #e25440;")
|
||||
| Stats
|
||||
.grid
|
||||
.grid-cell
|
||||
.row
|
||||
.col-sm-6.col-lg-4
|
||||
p.stat-name Hosts:
|
||||
p.center.big-stat {{pool.hosts.length}}
|
||||
.grid-cell
|
||||
p.center.big-stat {{hosts | count}}
|
||||
.col-sm-6.col-lg-4
|
||||
p.stat-name Running:
|
||||
p.center.big-stat {{pool.$running_hosts.length}}
|
||||
p.center.big-stat {{runningHosts | count}}
|
||||
|
||||
//- Action panel
|
||||
.grid
|
||||
@@ -69,13 +69,13 @@
|
||||
.grid-cell.text-center
|
||||
.grid
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Add SR", type="button", style="width: 90%")
|
||||
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.UUID})")
|
||||
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.UUID)")
|
||||
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%")
|
||||
@@ -95,12 +95,12 @@
|
||||
th Name
|
||||
th.col-md-4 Description
|
||||
th.col-md-6 Memory
|
||||
tr(xo-sref="hosts_view({id: host.UUID})", ng-repeat="host in pool.hosts | resolve | orderBy:natural('name_label') track by host.UUID")
|
||||
td {{host.name_label}}
|
||||
td {{host.name_description}}
|
||||
tr(xo-sref="hosts_view({id: host.id})", ng-repeat="host in hosts | map | orderBy:natural('name_label') track by host.id")
|
||||
td.oneliner {{host.name_label}}
|
||||
td.oneliner {{host.name_description}}
|
||||
td
|
||||
.progress-condensed
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{host.memory.usage}}", aria-valuemax="{{host.memory.size}}", style="width: {{[host.memory.usage, host.memory.size] | %}}")
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{host.memory.usage}}", aria-valuemax="{{host.memory.size}}", style="width: {{[host.memory.usage, host.memory.size] | percentage}}")
|
||||
|
||||
//- Shared SR panel
|
||||
.grid
|
||||
@@ -115,15 +115,18 @@
|
||||
th Type
|
||||
th Size
|
||||
th.col-md-4 Physical/Allocated usage
|
||||
tr(xo-sref="SRs_view({id: SR.UUID})", ng-repeat="SR in pool.SRs | resolve | orderBy:natural('name_label') track by SR.UUID")
|
||||
td {{SR.name_label}}
|
||||
td {{SR.name_description}}
|
||||
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] | %}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | %}}")
|
||||
.progress-bar.progress-bar-info(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[(SR.usage-SR.physical_usage), SR.size] | %}}", tooltip="Allocated: {{[(SR.usage), SR.size] | %}}")
|
||||
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.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
|
||||
@@ -131,17 +134,19 @@
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-comments(style="color: #e25440;")
|
||||
| Logs
|
||||
span.quick-edit(ng-if="pool.messages.length", tooltip="Remove all logs", xo-click="deleteAllLog()")
|
||||
span.quick-edit(ng-if="pool.messages | isNotEmpty", tooltip="Remove all logs", xo-click="deleteAllLog()")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
.panel-body
|
||||
p.center(ng-if="!pool.messages.length") No recent logs
|
||||
table.table.table-hover(ng-if="pool.messages.length")
|
||||
p.center(ng-if="pool.messages | isEmpty") No recent logs
|
||||
table.table.table-hover(ng-if="pool.messages | isNotEmpty")
|
||||
th Date
|
||||
th Name
|
||||
tr(ng-repeat="message in pool.messages | resolve | orderBy:'-time' track by message.UUID")
|
||||
tr(ng-repeat="message in pool.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
| {{message.name}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="deleteLog(message.UUID)")
|
||||
a(xo-click="deleteLog(message.id)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
|
||||
.center(ng-if = '(pool.messages | count) > 5')
|
||||
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=">>")
|
||||
|
||||
31
app/modules/scheduler/index.js
Normal file
31
app/modules/scheduler/index.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import management from './management'
|
||||
import rollingSnapshot from './rolling-snapshot'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('scheduler', [
|
||||
uiRouter,
|
||||
|
||||
management,
|
||||
rollingSnapshot
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('scheduler', {
|
||||
abstract: true,
|
||||
template: view,
|
||||
url: '/scheduler'
|
||||
})
|
||||
|
||||
// Redirect to default sub-state.
|
||||
$stateProvider.state('scheduler.index', {
|
||||
url: '',
|
||||
controller: function ($state) {
|
||||
$state.go('scheduler.management')
|
||||
}
|
||||
})
|
||||
})
|
||||
.name
|
||||
|
||||
78
app/modules/scheduler/management/index.js
Normal file
78
app/modules/scheduler/management/index.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import angular from 'angular'
|
||||
import assign from 'lodash.assign'
|
||||
import find from 'lodash.find'
|
||||
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 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 refreshSchedules = () => {
|
||||
xo.schedule.getAll()
|
||||
.then(schedules => {
|
||||
const s = {}
|
||||
forEach(schedules, schedule => s[schedule.id] = schedule)
|
||||
this.schedules = s
|
||||
})
|
||||
xo.scheduler.getScheduleTable()
|
||||
.then(table => this.scheduleTable = table)
|
||||
}
|
||||
|
||||
this.prettyCron = prettyCron.toString.bind(prettyCron)
|
||||
|
||||
const refreshJobs = () => {
|
||||
xo.job.getAll()
|
||||
.then(jobs => {
|
||||
const j = {}
|
||||
forEach(jobs, job => j[job.id] = job)
|
||||
this.jobs = j
|
||||
})
|
||||
}
|
||||
refreshSchedules()
|
||||
refreshJobs()
|
||||
|
||||
const interval = $interval(() => {
|
||||
refreshSchedules()
|
||||
refreshJobs()
|
||||
}, 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.collectionLength = col => Object.keys(col).length
|
||||
this.working = {}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
29
app/modules/scheduler/management/view.jade
Normal file
29
app/modules/scheduler/management/view.jade
Normal file
@@ -0,0 +1,29 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-clock-o(style="color: #e25440;")
|
||||
| Scheduler
|
||||
.panel-body
|
||||
.text-center(ng-if = '!ctrl.schedules'): i.fa.fa-circle-o-notch.fa-2x.fa-spin
|
||||
.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 Job
|
||||
th Scheduling
|
||||
th State
|
||||
th
|
||||
tr(ng-repeat = 'schedule in ctrl.schedules track by schedule.id')
|
||||
td: a(ui-sref = 'scheduler.rollingsnapshot({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
|
||||
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
|
||||
485
app/modules/scheduler/rolling-snapshot/index.js
Normal file
485
app/modules/scheduler/rolling-snapshot/index.js
Normal file
@@ -0,0 +1,485 @@
|
||||
import angular from 'angular'
|
||||
import assign from 'lodash.assign'
|
||||
import find from 'lodash.find'
|
||||
import indexOf from 'lodash.indexof'
|
||||
import later from 'later'
|
||||
import moment from 'moment'
|
||||
import prettyCron from 'prettycron'
|
||||
import remove from 'lodash.remove'
|
||||
import uiBootstrap from 'angular-ui-bootstrap'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import forEach from 'lodash.foreach'
|
||||
|
||||
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, $state, $stateParams, $interval, xo, xoApi, notify, selectHighLevelFilter, filterFilter) {
|
||||
this.comesForEditing = $stateParams.id
|
||||
|
||||
this.selectMinute = function (minute) {
|
||||
if (this.isSelectedMinute(minute)) {
|
||||
remove(this.formData.minSelect, v => String(v) === String(minute))
|
||||
} else {
|
||||
this.formData.minSelect.push(minute)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedMinute = function (minute) {
|
||||
return indexOf(this.formData.minSelect, minute) > -1 || indexOf(this.formData.minSelect, String(minute)) > -1
|
||||
}
|
||||
|
||||
this.selectHour = function (hour) {
|
||||
if (this.isSelectedHour(hour)) {
|
||||
remove(this.formData.hourSelect, v => String(v) === String(hour))
|
||||
} else {
|
||||
this.formData.hourSelect.push(hour)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedHour = function (hour) {
|
||||
return indexOf(this.formData.hourSelect, hour) > -1 || indexOf(this.formData.hourSelect, String(hour)) > -1
|
||||
}
|
||||
|
||||
this.selectDay = function (day) {
|
||||
if (this.isSelectedDay(day)) {
|
||||
remove(this.formData.daySelect, v => String(v) === String(day))
|
||||
} else {
|
||||
this.formData.daySelect.push(day)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedDay = function (day) {
|
||||
return indexOf(this.formData.daySelect, day) > -1 || indexOf(this.formData.daySelect, String(day)) > -1
|
||||
}
|
||||
|
||||
this.selectMonth = function (month) {
|
||||
if (this.isSelectedMonth(month)) {
|
||||
remove(this.formData.monthSelect, v => String(v) === String(month))
|
||||
} else {
|
||||
this.formData.monthSelect.push(month)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedMonth = function (month) {
|
||||
return indexOf(this.formData.monthSelect, month) > -1 || indexOf(this.formData.monthSelect, String(month)) > -1
|
||||
}
|
||||
|
||||
this.selectDayWeek = function (dayWeek) {
|
||||
if (this.isSelectedDayWeek(dayWeek)) {
|
||||
remove(this.formData.dayWeekSelect, v => String(v) === String(dayWeek))
|
||||
} else {
|
||||
this.formData.dayWeekSelect.push(dayWeek)
|
||||
}
|
||||
}
|
||||
|
||||
this.isSelectedDayWeek = function (dayWeek) {
|
||||
return indexOf(this.formData.dayWeekSelect, dayWeek) > -1 || indexOf(this.formData.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.formData.min === 'select' && this.formData.minSelect.length === 1 && String(this.formData.minSelect[0]) === '0'
|
||||
} else {
|
||||
this.formData.minSelect = [0]
|
||||
this.formData.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.formData.hour === 'select' && this.formData.hourSelect.length === 1 && String(this.formData.hourSelect[0]) === '0'
|
||||
} else {
|
||||
this.formData.hourSelect = [0]
|
||||
this.formData.hour = 'select'
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
this.update = function () {
|
||||
const d = this.formData
|
||||
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.formData.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 = function () {
|
||||
const schedule = later.parse.cron(this.formData.cronPattern)
|
||||
const occurences = later.schedule(schedule).next(25)
|
||||
this.formData.summary = []
|
||||
forEach(occurences, occurence => {
|
||||
this.formData.summary.push(moment(occurence).format('LLLL'))
|
||||
})
|
||||
}
|
||||
|
||||
const cronToFormData = (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)
|
||||
|
||||
const refreshSchedules = () => {
|
||||
return xo.schedule.getAll()
|
||||
.then(schedules => {
|
||||
const s = {}
|
||||
forEach(schedules, schedule => 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 interval = $interval(() => {
|
||||
refreshSchedules()
|
||||
refreshJobs()
|
||||
}, 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.resetFormData()
|
||||
const formData = this.formData
|
||||
formData.selectedVms = selectedVms
|
||||
formData.tag = tag
|
||||
formData.depth = depth
|
||||
formData.scheduleId = schedule.id
|
||||
cronToFormData(formData, cronPattern)
|
||||
this.formData = formData
|
||||
this.update()
|
||||
}
|
||||
|
||||
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.resetFormData()
|
||||
this.update()
|
||||
})
|
||||
.finally(() => {
|
||||
refreshJobs()
|
||||
refreshSchedules()
|
||||
})
|
||||
}
|
||||
|
||||
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: 'rollingSnapshot',
|
||||
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(() => {
|
||||
refreshJobs()
|
||||
refreshSchedules()
|
||||
})
|
||||
}
|
||||
|
||||
this.collectionLength = col => Object.keys(col).length
|
||||
|
||||
let i, j
|
||||
this.minutes = []
|
||||
for (i = 0; i < 6; i++) {
|
||||
this.minutes[i] = []
|
||||
for (j = 0; j < 10; j++) {
|
||||
this.minutes[i].push(10 * i + j)
|
||||
}
|
||||
}
|
||||
this.hours = []
|
||||
for (i = 0; i < 3; i++) {
|
||||
this.hours[i] = []
|
||||
for (j = 0; j < 8; j++) {
|
||||
this.hours[i].push(8 * i + j)
|
||||
}
|
||||
}
|
||||
this.days = []
|
||||
for (i = 0; i < 4; i++) {
|
||||
this.days[i] = []
|
||||
for (j = 1; j < 8; j++) {
|
||||
this.days[i].push(7 * i + j)
|
||||
}
|
||||
}
|
||||
this.days.push([29, 30, 31])
|
||||
|
||||
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.resetFormData = () => this.formData = {
|
||||
minRange: 5,
|
||||
hourRange: 2,
|
||||
minSelect: [0],
|
||||
hourSelect: [],
|
||||
daySelect: [],
|
||||
monthSelect: [],
|
||||
dayWeekSelect: [],
|
||||
min: 'select',
|
||||
hour: 'all',
|
||||
day: 'all',
|
||||
month: 'all',
|
||||
dayWeek: 'all',
|
||||
cronPattern: '* * * * *',
|
||||
summary: [],
|
||||
allRunning: false,
|
||||
allHalted: false,
|
||||
selectedVms: [],
|
||||
scheduleId: undefined,
|
||||
tag: undefined,
|
||||
depth: undefined,
|
||||
enabled: false,
|
||||
previewLimit: 0
|
||||
}
|
||||
|
||||
if (!this.comesForEditing) {
|
||||
refreshSchedules()
|
||||
refreshJobs()
|
||||
this.resetFormData()
|
||||
this.update()
|
||||
} else {
|
||||
refreshJobs()
|
||||
.then(() => refreshSchedules())
|
||||
.then(() => {
|
||||
this.edit(this.schedules[this.comesForEditing])
|
||||
delete this.comesForEditing
|
||||
})
|
||||
}
|
||||
this.objects = xoApi.all
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
226
app/modules/scheduler/rolling-snapshot/view.jade
Normal file
226
app/modules/scheduler/rolling-snapshot/view.jade
Normal file
@@ -0,0 +1,226 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-camera-retro(style="color: #e25440;")
|
||||
| Rolling VM Snapshots
|
||||
form(ng-submit = 'ctrl.save(ctrl.formData.scheduleId, ctrl.formData.selectedVms, ctrl.formData.tag, ctrl.formData.depth, ctrl.formData.cronPattern, ctrl.formData.enabled)')
|
||||
.grid
|
||||
.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.fa.fa-circle-o-notch.fa-spin.fa-2x
|
||||
.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-8
|
||||
input#tag.form-control(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(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(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(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-8
|
||||
input#depth.form-control(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(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(style="color: #e25440;")
|
||||
| Schedule
|
||||
.panel-body.form-horizontal
|
||||
.text-center(ng-if = '!ctrl.formData'): i.fa.fa-circle-o-notch.fa-spin.fa-2x
|
||||
accordion(ng-if = 'ctrl.formData', close-others= 'false', ng-click = 'ctrl.update()')
|
||||
accordion-group
|
||||
accordion-heading Month
|
||||
tabset
|
||||
tab(select = 'ctrl.formData.month = "all"', active = 'ctrl.tabs.month.all')
|
||||
tab-heading every month
|
||||
tab(select = 'ctrl.formData.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.formData.day = "all"', active = 'ctrl.tabs.day.all')
|
||||
tab-heading every day
|
||||
tab(select = 'ctrl.formData.day = "select"', active = 'ctrl.tabs.day.select')
|
||||
tab-heading each selected day
|
||||
br
|
||||
p.text-warning
|
||||
i.fa.fa-warning
|
||||
| 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.formData.dayWeek = "all"', active = 'ctrl.tabs.dayWeek.all')
|
||||
tab-heading every day of week
|
||||
tab(select = 'ctrl.formData.dayWeek = "select"', active = 'ctrl.tabs.dayWeek.select')
|
||||
tab-heading each selected day of week
|
||||
br
|
||||
p.text-warning
|
||||
i.fa.fa-warning
|
||||
| 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
|
||||
| Nothing planned on a hourly grain
|
||||
br
|
||||
br
|
||||
tabset
|
||||
tab(select = 'ctrl.formData.hour = "all"', active = 'ctrl.tabs.hour.all')
|
||||
tab-heading every hour
|
||||
tab(select = 'ctrl.formData.hour = "range"', active = 'ctrl.tabs.hour.range')
|
||||
tab-heading every N hour
|
||||
br
|
||||
.form-group
|
||||
label.col-sm-2.control-label {{ ctrl.formData.hourRange }}
|
||||
.col-sm-10
|
||||
input.form-control(type = 'range', min = '2', max = '23', step = '1', ng-model = 'ctrl.formData.hourRange', ng-change = 'ctrl.update()')
|
||||
tab(select = 'ctrl.formData.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
|
||||
| Nothing planned on a minute grain
|
||||
br
|
||||
br
|
||||
tabset
|
||||
tab(select = 'ctrl.formData.min = "all"', active = 'ctrl.tabs.min.all')
|
||||
tab-heading every minute
|
||||
tab(select = 'ctrl.formData.min = "range"', active = 'ctrl.tabs.min.range')
|
||||
tab-heading every N minutes
|
||||
br
|
||||
.form-group
|
||||
label.col-sm-2.control-label {{ ctrl.formData.minRange }}
|
||||
.col-sm-10
|
||||
input.form-control(type = 'range', min = '2', max = '59', step = '1', ng-model = 'ctrl.formData.minRange', ng-change = 'ctrl.update()')
|
||||
tab(select = 'ctrl.formData.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.formData.cronPattern')
|
||||
//- Summary
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-flag-checkered(style="color: #e25440;")
|
||||
| Summary
|
||||
.panel-body
|
||||
.text-center(ng-if = '!ctrl.formData'): i.fa.fa-circle-o-notch.fa-spin.fa-2x
|
||||
div(ng-if = 'ctrl.formData')
|
||||
p
|
||||
//- | {{ schAction.info.actionName }}
|
||||
strong Scheduled to run:
|
||||
| {{ ctrl.prettyCron(ctrl.formData.cronPattern) }}
|
||||
.form-inline
|
||||
.form-group
|
||||
label Preview:
|
||||
input.form-control(type = 'range', min = '0', max = '{{ ctrl.formData.summary.length - 3 }}', step = '1', ng-model = 'ctrl.formData.previewLimit')
|
||||
br
|
||||
ul
|
||||
li(ng-repeat = 'occurence in ctrl.formData.summary | limitTo: +ctrl.formData.previewLimit+3') {{ occurence }}
|
||||
li ...
|
||||
p.center
|
||||
button.btn.btn-lg.btn-primary(type = 'submit')
|
||||
i.fa.fa-clock-o
|
||||
|
|
||||
i.fa.fa-arrow-right
|
||||
|
|
||||
i.fa.fa-database
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-lg.btn-default(type = 'button', ng-click = 'ctrl.resetFormData();ctrl.update()')
|
||||
| Reset
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-list-ul(style="color: #e25440;")
|
||||
| Schedules
|
||||
.panel-body
|
||||
.text-center(ng-if = '!ctrl.schedules'): i.fa.fa-circle-o-notch.fa-2x.fa-spin
|
||||
.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 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
|
||||
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') ({{ ((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
|
||||
|
|
||||
button.btn.btn-danger(type = 'button', ng-click = 'ctrl.delete(schedule)'): i.fa.fa-trash-o
|
||||
13
app/modules/scheduler/view.jade
Normal file
13
app/modules/scheduler/view.jade
Normal file
@@ -0,0 +1,13 @@
|
||||
.grid(style = 'height: 100%')
|
||||
//- Side menu
|
||||
.settings-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
|
||||
a(ui-sref = '.rollingsnapshot')
|
||||
i.fa.fa-fw.fa-camera-retro.fa-menu
|
||||
span.menu-entry Rolling snapshots
|
||||
//- Content
|
||||
div.settings-content(ui-view = '')
|
||||
98
app/modules/settings/acls/index.js
Normal file
98
app/modules/settings/acls/index.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import angular from 'angular'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
import uiSelect from 'angular-ui-select'
|
||||
|
||||
import filter from 'lodash.filter'
|
||||
|
||||
import xoApi from 'xo-api'
|
||||
import xoServices from 'xo-services'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('settings.acls', [
|
||||
uiRouter,
|
||||
uiSelect,
|
||||
|
||||
xoApi,
|
||||
xoServices
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings.acls', {
|
||||
controller: 'SettingsAcls as ctrl',
|
||||
url: '/acls',
|
||||
resolve: {
|
||||
acls (xo) {
|
||||
return xo.acl.get()
|
||||
},
|
||||
users (xo) {
|
||||
return xo.user.getAll()
|
||||
},
|
||||
groups (xo) {
|
||||
return xo.group.getAll()
|
||||
},
|
||||
roles (xo) {
|
||||
return xo.role.getAll()
|
||||
}
|
||||
},
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SettingsAcls', function ($scope, acls, users, groups, roles, xoApi, xo) {
|
||||
this.acls = acls
|
||||
|
||||
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
|
||||
|
||||
let refreshAcls = () => {
|
||||
xo.acl.get().then(acls => {
|
||||
this.acls = acls
|
||||
})
|
||||
}
|
||||
|
||||
this.getUser = (id) => {
|
||||
for (let user of this.users) {
|
||||
if (user.id === id) {
|
||||
return user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.addAcl = () => {
|
||||
xo.acl.add(this.subject.id, this.object.id, this.role.id).then(refreshAcls)
|
||||
this.subject = this.object = this.role = null
|
||||
}
|
||||
this.removeAcl = (subject, object, role) => {
|
||||
xo.acl.remove(subject, object, role).then(refreshAcls)
|
||||
}
|
||||
})
|
||||
.filter('selectHighLevel', () => {
|
||||
const HIGH_LEVEL_OBJECTS = {
|
||||
pool: true,
|
||||
host: true,
|
||||
VM: true,
|
||||
SR: true
|
||||
}
|
||||
let isHighLevel = (object) => HIGH_LEVEL_OBJECTS[object.type]
|
||||
|
||||
return (objects) => filter(objects, isHighLevel)
|
||||
})
|
||||
.name
|
||||
95
app/modules/settings/acls/view.jade
Normal file
95
app/modules/settings/acls/view.jade
Normal file
@@ -0,0 +1,95 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-key(style="color: #e25440;")
|
||||
| ACLs
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-plus-circle(style="color: #e25440;")
|
||||
| Create
|
||||
.panel-body.text-center
|
||||
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.fa.fa-fw.fa-user
|
||||
| {{$select.selected.email}}
|
||||
span(ng-if = '$select.selected.name')
|
||||
i.fa.fa-fw.fa-users
|
||||
| {{$select.selected.name}}
|
||||
ui-select-choices(
|
||||
repeat = 'entity in ctrl.entities | filter:{ permission: "!admin" } | filter:$select.search'
|
||||
)
|
||||
div
|
||||
span(ng-if = 'entity.email')
|
||||
i.fa.fa-fw.fa-user
|
||||
| {{entity.email}}
|
||||
span(ng-if = 'entity.name')
|
||||
i.fa.fa-fw.fa-users
|
||||
| {{entity.name}}
|
||||
.form-group
|
||||
ui-select(
|
||||
ng-model = 'ctrl.object'
|
||||
)
|
||||
ui-select-match(
|
||||
placeholder = 'Choose an object'
|
||||
)
|
||||
div
|
||||
i(class = 'xo-icon-{{$select.selected.type | lowercase}}')
|
||||
| {{$select.selected.name_label}}
|
||||
ui-select-choices(
|
||||
repeat = 'object in ctrl.objects | selectHighLevel | filter:$select.search | orderBy:["type", "name_label"]'
|
||||
)
|
||||
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}})
|
||||
.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}}
|
||||
button.btn.btn-success
|
||||
i.fa.fa-plus
|
||||
| Create
|
||||
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-street-view(style="color: #e25440;")
|
||||
| Manage
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th User
|
||||
th Object
|
||||
th Role
|
||||
th
|
||||
tr(ng-repeat = 'acl in ctrl.acls')
|
||||
td {{ ctrl.usersById[acl.subject].email || ctrl.groupsById[acl.subject].name }}
|
||||
td {{(acl.object | resolve).name_label}}
|
||||
td {{ ctrl.rolesById[acl.action].name }}
|
||||
td
|
||||
button.btn.btn-danger(ng-click = 'ctrl.removeAcl(acl.subject, acl.object, acl.action)')
|
||||
i.fa.fa-trash
|
||||
|
||||
160
app/modules/settings/group/index.js
Normal file
160
app/modules/settings/group/index.js
Normal 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
|
||||
69
app/modules/settings/group/view.jade
Normal file
69
app/modules/settings/group/view.jade
Normal file
@@ -0,0 +1,69 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-users(style="color: #e25440;")
|
||||
| {{ ctrl.group.name }}
|
||||
a.btn.btn-default(ui-sref = 'settings.groups')
|
||||
i.fa.fa-level-up
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-street-view(style="color: #e25440;")
|
||||
| Members
|
||||
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] }}
|
||||
del(ng-if = 'ctrl.removals[user]') {{ ctrl.userEmails[user] }}
|
||||
span.pull-right
|
||||
label
|
||||
input.hidden(type = 'checkbox', ng-model = 'ctrl.removals[user]')
|
||||
|
|
||||
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
|
||||
|
|
||||
button.btn.btn-default(ng-if = 'ctrl.isModified()', type="button", ng-click = 'ctrl.cancelEdition()')
|
||||
i.fa.fa-times
|
||||
| Cancel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-key(style="color: #e25440;")
|
||||
| ACLs
|
||||
.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(style="color: #e25440;")
|
||||
| Accessible objects
|
||||
.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}})
|
||||
|
||||
189
app/modules/settings/groups/index.js
Normal file
189
app/modules/settings/groups/index.js
Normal 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
|
||||
12
app/modules/settings/groups/modal.jade
Normal file
12
app/modules/settings/groups/modal.jade
Normal file
@@ -0,0 +1,12 @@
|
||||
.modal-header
|
||||
button.close(
|
||||
type = 'button',
|
||||
ng-click = '$dismiss()'
|
||||
)
|
||||
span(aria-hidden = 'true') ×
|
||||
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
|
||||
|  
|
||||
button.btn.btn-default(type = 'button', ng-click = '$dismiss()') Cancel
|
||||
49
app/modules/settings/groups/view.jade
Normal file
49
app/modules/settings/groups/view.jade
Normal file
@@ -0,0 +1,49 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-users(style="color: #e25440;")
|
||||
| Groups
|
||||
.grid
|
||||
.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:
|
||||
span(ng-repeat = 'user in group.users | limitTo:4')
|
||||
| {{ ctrl.userEmails[user] }}{{ $last ? (group.users.length > 4 ? ',...' : '') : ', ' }}
|
||||
|
|
||||
td
|
||||
a.btn.btn-primary(ui-sref = 'settings.group({groupId: group.id})')
|
||||
| Edit
|
||||
i.fa.fa-pencil
|
||||
|
|
||||
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  
|
||||
p
|
||||
button.btn.btn-success(type="button", ng-click="ctrl.addGroup()")
|
||||
i.fa.fa-plus
|
||||
|
|
||||
span(ng-if = 'ctrl.modified')
|
||||
button.btn.btn-primary(type="submit")
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-default(type="button", ng-click = "ctrl.cancelModifications()")
|
||||
i.fa.fa-times
|
||||
| Cancel
|
||||
@@ -1,176 +0,0 @@
|
||||
angular = require 'angular'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# FIXME: Mutualize the code between users and servers.
|
||||
|
||||
# FIXME: should be merged in admin module.
|
||||
|
||||
module.exports = angular.module 'xoWebApp.settings', [
|
||||
require 'angular-ui-router'
|
||||
]
|
||||
.config ($stateProvider) ->
|
||||
$stateProvider.state 'settings',
|
||||
url: '/settings'
|
||||
controller: 'SettingsCtrl'
|
||||
template: require './view'
|
||||
.controller 'SettingsCtrl', ($scope, xo) ->
|
||||
$scope.permissions = [
|
||||
{
|
||||
label: 'User'
|
||||
value: 'none'
|
||||
}
|
||||
{
|
||||
label: 'Admin'
|
||||
value: 'admin'
|
||||
}
|
||||
]
|
||||
|
||||
# Users
|
||||
do ->
|
||||
# Fetches them.
|
||||
$scope.users = []
|
||||
xo.user.getAll().then (users) ->
|
||||
$scope.users = users
|
||||
|
||||
# Which ones are selected?
|
||||
selected = $scope.selectedUsers = {}
|
||||
|
||||
# New users to create.
|
||||
$scope.newUsers = []
|
||||
|
||||
# Add a new user to be created.
|
||||
$scope.addUser = ->
|
||||
$scope.newUsers.push {
|
||||
# Fake (unique) identifier needed by Angular.JS
|
||||
id: Math.random()
|
||||
|
||||
# Default permission.
|
||||
permission: 'none'
|
||||
}
|
||||
$scope.addUser()
|
||||
|
||||
# Saves any modifications.
|
||||
$scope.saveUsers = ->
|
||||
{users, newUsers} = $scope
|
||||
|
||||
# This will be the new list of users with those marked to
|
||||
# delete removed.
|
||||
updateUsers = []
|
||||
|
||||
for user in users
|
||||
{id} = user
|
||||
if selected[id]
|
||||
delete selected[id]
|
||||
|
||||
# FIXME: this cast should not be necessary.
|
||||
xo.user.delete "#{id}"
|
||||
else
|
||||
# Only sets the password if not empty.
|
||||
delete user.password unless user.password
|
||||
|
||||
# TODO: only update users which have been modified.
|
||||
xo.user.set user
|
||||
|
||||
# Remove the password from the interface.
|
||||
delete user.password
|
||||
|
||||
updateUsers.push user
|
||||
|
||||
for user in newUsers
|
||||
{email, permission, password} = user
|
||||
|
||||
# Required field.
|
||||
continue unless email
|
||||
|
||||
# Sends the order to XO-Server.
|
||||
xo.user.create {email, permission, password}
|
||||
.then (id) ->
|
||||
# Update user identifier.
|
||||
user.id = id
|
||||
return
|
||||
|
||||
# The password should not be displayed.
|
||||
delete user.password
|
||||
|
||||
# Adds the user to out local list.
|
||||
updateUsers.push user
|
||||
|
||||
$scope.users = updateUsers
|
||||
$scope.newUsers = []
|
||||
$scope.addUser()
|
||||
|
||||
# TODO: Retrieves an up to date users list from the server.
|
||||
|
||||
# Servers
|
||||
do ->
|
||||
# Fetches them.
|
||||
$scope.servers = []
|
||||
xo.server.getAll().then (servers) ->
|
||||
$scope.servers = servers
|
||||
|
||||
# Which ones are selected?
|
||||
selected = $scope.selectedServers = {}
|
||||
|
||||
# New servers to create.
|
||||
$scope.newServers = []
|
||||
|
||||
# Add a new server to be created.
|
||||
$scope.addServer = ->
|
||||
$scope.newServers.push {
|
||||
# Fake (unique) identifier needed by Angular.JS
|
||||
id: Math.random()
|
||||
}
|
||||
$scope.addServer()
|
||||
|
||||
# Saves any modifications.
|
||||
$scope.saveServers = ->
|
||||
{servers, newServers} = $scope
|
||||
|
||||
# This will be the new list of servers with those marked to
|
||||
# delete removed.
|
||||
updateServers = []
|
||||
|
||||
for server in servers
|
||||
{id} = server
|
||||
if selected[id]
|
||||
delete selected[id]
|
||||
xo.server.remove id
|
||||
else
|
||||
# Only sets the password if not empty.
|
||||
delete server.password unless server.password
|
||||
|
||||
# TODO: only update servers which have been modified.
|
||||
xo.server.set server
|
||||
|
||||
# Remove the password from the interface.
|
||||
delete server.password
|
||||
|
||||
updateServers.push server
|
||||
|
||||
for server in newServers
|
||||
{host, username, password} = server
|
||||
|
||||
# Required field.
|
||||
continue unless host
|
||||
|
||||
# Sends the order to XO-Server.
|
||||
xo.server.add {host, username, password}
|
||||
.then (id) ->
|
||||
server.id = id
|
||||
return
|
||||
|
||||
# The password should not be displayed.
|
||||
delete server.password
|
||||
|
||||
# Adds the server to out local list.
|
||||
updateServers.push server
|
||||
|
||||
$scope.servers = updateServers
|
||||
$scope.newServers = []
|
||||
$scope.addServer()
|
||||
|
||||
# TODO: Retrieves an up to date servers list from the server.
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
39
app/modules/settings/index.js
Normal file
39
app/modules/settings/index.js
Normal file
@@ -0,0 +1,39 @@
|
||||
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 users from './users'
|
||||
|
||||
import view from './view'
|
||||
|
||||
export default angular.module('settings', [
|
||||
uiRouter,
|
||||
|
||||
acls,
|
||||
group,
|
||||
groups,
|
||||
servers,
|
||||
update,
|
||||
users
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('settings', {
|
||||
abstract: true,
|
||||
template: view,
|
||||
url: '/settings'
|
||||
})
|
||||
|
||||
// Redirect to default sub-state.
|
||||
$stateProvider.state('settings.index', {
|
||||
url: '',
|
||||
controller: function ($state) {
|
||||
$state.go('settings.servers')
|
||||
}
|
||||
})
|
||||
})
|
||||
.name
|
||||
|
||||
123
app/modules/settings/servers/index.js
Normal file
123
app/modules/settings/servers/index.js
Normal 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
|
||||
80
app/modules/settings/servers/view.jade
Normal file
80
app/modules/settings/servers/view.jade
Normal file
@@ -0,0 +1,80 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-cloud(style="color: #e25440;")
|
||||
| Servers
|
||||
.grid
|
||||
.panel.panel-default
|
||||
//- .panel-heading.panel-title
|
||||
//- i.fa.fa-cloud(style="color: #e25440;")
|
||||
//- | Connections
|
||||
form(ng-submit="ctrl.saveServers()", autocomplete="off").panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th.col-md-5 Host
|
||||
th.col-md-2 User
|
||||
th.col-md-3 Password
|
||||
th.col-md-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(ng-if="server.status === 'connected'")
|
||||
i.fa.fa-check-circle.fa-lg.text-success(tooltip="Connected")
|
||||
span.input-group-addon(ng-if="server.status === 'disconnected'")
|
||||
i.fa.fa-times-circle.fa-lg.text-danger(tooltip="Disconnected")
|
||||
span.input-group-addon(ng-if="server.status === 'connecting'")
|
||||
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  
|
||||
td  
|
||||
p.text-center
|
||||
button.btn.btn-primary(type="submit")
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-success(type="button", ng-click="ctrl.addServer()")
|
||||
i.fa.fa-plus
|
||||
93
app/modules/settings/update/index.js
Normal file
93
app/modules/settings/update/index.js
Normal file
@@ -0,0 +1,93 @@
|
||||
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, () => {})
|
||||
}
|
||||
|
||||
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
|
||||
112
app/modules/settings/update/view.jade
Normal file
112
app/modules/settings/update/view.jade
Normal file
@@ -0,0 +1,112 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-refresh(style="color: #e25440;")
|
||||
| Update
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-globe(style="color: #e25440;")
|
||||
| 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')
|
||||
| No update information available
|
||||
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')
|
||||
| Registration needed
|
||||
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')
|
||||
|
|
||||
button#update.btn.btn-info(type = 'button', ng-click = 'ctrl.update()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}')
|
||||
| Check for updates
|
||||
i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.updating}')
|
||||
|
|
||||
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
|
||||
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 }}
|
||||
| :
|
||||
span(ng-bind-html = 'entry.message | ansitohtml')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-pencil(style="color: #e25440;")
|
||||
| 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
|
||||
.grid-cell(ng-if = 'ctrl.updater.state && ctrl.configuration')
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-cogs(style="color: #e25440;")
|
||||
| Settings
|
||||
.panel-body
|
||||
form.form-inline(ng-submit = 'ctrl.configure(ctrl.configuration.proxyHost, ctrl.configuration.proxyPort)')
|
||||
fieldset
|
||||
h4
|
||||
i.fa.fa-globe
|
||||
| Proxy settings
|
||||
p If you need a proxy to access the Internet
|
||||
.form-group
|
||||
label.control-label Host:
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyHost', placeholder = 'myproxy.example.org')
|
||||
|
|
||||
|
|
||||
.form-group
|
||||
label.control-label Port:
|
||||
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyPort', placeholder = '3128')
|
||||
|
|
||||
|
|
||||
|
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
i.fa.fa-floppy-o
|
||||
| Save
|
||||
126
app/modules/settings/users/index.js
Normal file
126
app/modules/settings/users/index.js
Normal 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
|
||||
58
app/modules/settings/users/view.jade
Normal file
58
app/modules/settings/users/view.jade
Normal file
@@ -0,0 +1,58 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-user(style="color: #e25440;")
|
||||
| Users
|
||||
.grid
|
||||
.panel.panel-default
|
||||
//- .panel-heading.panel-title
|
||||
//- i.fa.fa-users(style="color: #e25440;")
|
||||
//- | Users
|
||||
form(ng-submit="ctrl.saveUsers()", autocomplete="off").panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th.col-md-4 Email
|
||||
th.col-md-4 Permissions
|
||||
th.col-md-3 Password
|
||||
th.col-md-1.text-center
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove user")
|
||||
tr(ng-repeat="user in ctrl.users | orderBy:natural('id') track by user.id")
|
||||
td
|
||||
input.form-control(type="text", ng-model="user.email", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
|
||||
td
|
||||
select.form-control(ng-options="p.value as p.label for p in ctrl.permissions", ng-model="user.permission", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
|
||||
td
|
||||
input.form-control(type="password", ng-model="user.password", placeholder="Fill to change the password", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
|
||||
td.text-center
|
||||
input(type="checkbox", ng-model="ctrl.selectedUsers[user.id]", ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}')
|
||||
tr(ng-repeat="user in ctrl.newUsers")
|
||||
td
|
||||
input.form-control(
|
||||
type = "text"
|
||||
ng-model = "user.email"
|
||||
placeholder = "email"
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
td
|
||||
select.form-control(
|
||||
ng-options = "p.value as p.label for p in ctrl.permissions"
|
||||
ng-model = "user.permission"
|
||||
ng-required = "user.email"
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
td
|
||||
input.form-control(
|
||||
type = "password"
|
||||
ng-model = "user.password"
|
||||
ng-required = "user.email"
|
||||
placeholder = "password"
|
||||
ui-event = '{focus: "ctrl.editingUser(true)", blur: "ctrl.editingUser(false)"}'
|
||||
)
|
||||
td  
|
||||
p.text-center
|
||||
button.btn.btn-primary(type="submit")
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-success(type="button", ng-click="ctrl.addUser()")
|
||||
i.fa.fa-plus
|
||||
@@ -1,105 +1,23 @@
|
||||
//- TODO: lots of stuff.
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.fa.fa-cog
|
||||
| XO Settings
|
||||
//- Add server panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-link(style="color: #e25440;")
|
||||
| Connected servers
|
||||
form(ng-submit="saveServers()", autocomplete="off").panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th Host
|
||||
th User
|
||||
th Password
|
||||
th Delete
|
||||
tr(ng-repeat="server in servers | orderBy:natural('host') track by server.id")
|
||||
td
|
||||
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
|
||||
input(type="checkbox", ng-model="selectedServers[server.id]")
|
||||
tr(ng-repeat="server in 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  
|
||||
p.text-center
|
||||
button.btn.btn-primary(type="submit")
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-success(type="button", ng-click="addServer()")
|
||||
i.fa.fa-plus
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-users(style="color: #e25440;")
|
||||
| Users
|
||||
form(ng-submit="saveUsers()", autocomplete="off").panel-body
|
||||
table.table.table-hover
|
||||
tr
|
||||
th Email
|
||||
th Permissions
|
||||
th Password
|
||||
th Delete
|
||||
tr(ng-repeat="user in users | orderBy:natural('email') track by user.id")
|
||||
td
|
||||
input.form-control(type="text", ng-model="user.email")
|
||||
td
|
||||
select.form-control(ng-options="p.value as p.label for p in permissions", ng-model="user.permission")
|
||||
td
|
||||
input.form-control(type="password", ng-model="user.password", placeholder="Fill to change the password")
|
||||
td
|
||||
input(type="checkbox", ng-model="selectedUsers[user.id]")
|
||||
tr(ng-repeat="user in newUsers")
|
||||
td
|
||||
input.form-control(
|
||||
type = "text"
|
||||
ng-model = "user.email"
|
||||
placeholder = "email"
|
||||
)
|
||||
td
|
||||
select.form-control(
|
||||
ng-options = "p.value as p.label for p in permissions"
|
||||
ng-model = "user.permission"
|
||||
ng-required = "user.email"
|
||||
)
|
||||
td
|
||||
input.form-control(
|
||||
type = "password"
|
||||
ng-model = "user.password"
|
||||
ng-required = "user.email"
|
||||
placeholder = "password"
|
||||
)
|
||||
td  
|
||||
p.text-center
|
||||
button.btn.btn-primary(type="submit")
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
|
|
||||
button.btn.btn-success(type="button", ng-click="addUser()")
|
||||
i.fa.fa-plus
|
||||
.grid(style = 'height: 100%')
|
||||
//- Side menu
|
||||
.settings-menu
|
||||
ul.nav
|
||||
li
|
||||
a(ui-sref = '.servers', ui-sref-active = 'active')
|
||||
i.fa.fa-fw.fa-cloud.fa-menu
|
||||
span.menu-entry Servers
|
||||
a(ui-sref = '.users')
|
||||
i.fa.fa-fw.fa-user.fa-menu
|
||||
span.menu-entry Users
|
||||
a(ui-sref = '.groups')
|
||||
i.fa.fa-fw.fa-users.fa-menu
|
||||
span.menu-entry Groups
|
||||
a(ui-sref = '.acls')
|
||||
i.fa.fa-fw.fa-key.fa-menu
|
||||
span.menu-entry ACLs
|
||||
a(ui-sref = '.update')
|
||||
i.fa.fa-fw.fa-refresh.fa-menu
|
||||
span.menu-entry Update
|
||||
|
||||
//- Content
|
||||
div.settings-content(ui-view = '')
|
||||
|
||||
@@ -1,189 +1,192 @@
|
||||
import angular from 'angular';
|
||||
import isEmpty from 'isempty';
|
||||
import uiRouter from 'angular-ui-router';
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import isEmpty from 'lodash.isempty'
|
||||
import uiRouter from 'angular-ui-router'
|
||||
|
||||
import Bluebird from 'bluebird';
|
||||
import Bluebird from 'bluebird'
|
||||
|
||||
import view from './view';
|
||||
import view from './view'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.sr', [
|
||||
uiRouter,
|
||||
uiRouter
|
||||
])
|
||||
.config(function ($stateProvider) {
|
||||
$stateProvider.state('SRs_view', {
|
||||
url: '/srs/:id',
|
||||
controller: 'SrCtrl',
|
||||
template: view,
|
||||
});
|
||||
template: view
|
||||
})
|
||||
})
|
||||
.controller('SrCtrl', function ($scope, $stateParams, $state, $q, notify, xoApi, xo, modal, $window, bytesToSizeFilter) {
|
||||
|
||||
$window.bytesToSize = bytesToSizeFilter; // FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
|
||||
let {get} = xoApi;
|
||||
$scope.currentLogPage = 1
|
||||
$scope.currentVDIPage = 1
|
||||
|
||||
let {get} = xoApi
|
||||
$scope.$watch(() => xoApi.get($stateParams.id), function (SR) {
|
||||
$scope.SR = SR;
|
||||
});
|
||||
$scope.SR = SR
|
||||
})
|
||||
|
||||
$scope.saveSR = function ($data) {
|
||||
let {SR} = $scope;
|
||||
let {name_label, name_description} = $data;
|
||||
let {SR} = $scope
|
||||
let {name_label, name_description} = $data
|
||||
|
||||
$data = {
|
||||
id: SR.UUID,
|
||||
};
|
||||
id: SR.id
|
||||
}
|
||||
if (name_label !== SR.name_label) {
|
||||
$data.name_label = name_label;
|
||||
$data.name_label = name_label
|
||||
}
|
||||
if (name_description !== SR.name_description) {
|
||||
$data.name_description = name_description;
|
||||
$data.name_description = name_description
|
||||
}
|
||||
|
||||
return xoApi.call('sr.set', $data);
|
||||
};
|
||||
return xoApi.call('sr.set', $data)
|
||||
}
|
||||
|
||||
$scope.deleteVDI = function (UUID) {
|
||||
console.log('Delete VDI', UUID);
|
||||
$scope.deleteVDI = function (id) {
|
||||
console.log('Delete VDI', id)
|
||||
|
||||
return modal.confirm({
|
||||
title: 'VDI deletion',
|
||||
message: 'Are you sure you want to delete this VDI? This operation is irreversible.',
|
||||
message: 'Are you sure you want to delete this VDI? This operation is irreversible.'
|
||||
}).then(function () {
|
||||
return xo.vdi.delete(UUID);
|
||||
});
|
||||
};
|
||||
return xo.vdi.delete(id)
|
||||
})
|
||||
}
|
||||
|
||||
$scope.disconnectVBD = function (UUID) {
|
||||
console.log('Disconnect VBD', UUID);
|
||||
$scope.disconnectVBD = function (id) {
|
||||
console.log('Disconnect VBD', id)
|
||||
|
||||
return xoApi.call('vbd.disconnect', {id: UUID});
|
||||
};
|
||||
return xoApi.call('vbd.disconnect', {id: id})
|
||||
}
|
||||
|
||||
$scope.connectPBD = function (UUID) {
|
||||
console.log('Connect PBD', UUID);
|
||||
$scope.connectPBD = function (id) {
|
||||
console.log('Connect PBD', id)
|
||||
|
||||
return xoApi.call('pbd.connect', {id: UUID});
|
||||
};
|
||||
return xoApi.call('pbd.connect', {id: id})
|
||||
}
|
||||
|
||||
$scope.disconnectPBD = function (UUID) {
|
||||
console.log('Disconnect PBD', UUID);
|
||||
$scope.disconnectPBD = function (id) {
|
||||
console.log('Disconnect PBD', id)
|
||||
|
||||
return xoApi.call('pbd.disconnect', {id: UUID});
|
||||
};
|
||||
return 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);
|
||||
let pbd = xoApi.get(id)
|
||||
|
||||
xoApi.call('pbd.connect', {id: pbd.ref});
|
||||
xoApi.call('pbd.connect', {id: pbd.id})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$scope.disconnectAllHosts = function () {
|
||||
return modal.confirm({
|
||||
title: 'Disconnect hosts',
|
||||
message: 'Are you sure you want to disconnect all hosts to this SR?',
|
||||
message: 'Are you sure you want to disconnect all hosts to this SR?'
|
||||
}).then(function () {
|
||||
for (let id of $scope.SR.$PBDs) {
|
||||
let pbd = xoApi.get(id);
|
||||
let pbd = xoApi.get(id)
|
||||
|
||||
xoApi.call('pbd.disconnect', {id: pbd.ref});
|
||||
console.log(pbd.ref)
|
||||
xoApi.call('pbd.disconnect', {id: pbd.id})
|
||||
console.log(pbd.id)
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
|
||||
$scope.rescanSr = function (UUID) {
|
||||
console.log('Rescan SR', UUID);
|
||||
$scope.rescanSr = function (id) {
|
||||
console.log('Rescan SR', id)
|
||||
|
||||
return xoApi.call('sr.scan', {id: UUID});
|
||||
};
|
||||
return xoApi.call('sr.scan', {id: id})
|
||||
}
|
||||
|
||||
$scope.removeSR = function (UUID) {
|
||||
console.log('Remove SR', UUID);
|
||||
$scope.removeSR = function (id) {
|
||||
console.log('Remove SR', id)
|
||||
|
||||
return modal.confirm({
|
||||
title: 'SR deletion',
|
||||
message: 'Are you sure you want to delete this SR? This operation is irreversible.',
|
||||
message: 'Are you sure you want to delete this SR? This operation is irreversible.'
|
||||
}).then(function () {
|
||||
return Bluebird.map($scope.SR.$PBDs, pbdId => {
|
||||
let pbd = xoApi.get(pbdId);
|
||||
let pbd = xoApi.get(pbdId)
|
||||
|
||||
return xoApi.call('pbd.disconnect', { id: pbd.id });
|
||||
});
|
||||
return xoApi.call('pbd.disconnect', { id: pbd.id })
|
||||
})
|
||||
}).then(function () {
|
||||
return xoApi.call('sr.destroy', {id: UUID});
|
||||
return xoApi.call('sr.destroy', {id: id})
|
||||
}).then(function () {
|
||||
$state.go('index');
|
||||
$state.go('index')
|
||||
notify.info({
|
||||
title: 'SR remove',
|
||||
message: 'SR is removed',
|
||||
});
|
||||
});
|
||||
};
|
||||
message: 'SR is removed'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.forgetSR = function (UUID) {
|
||||
console.log('Forget SR', UUID);
|
||||
$scope.forgetSR = function (id) {
|
||||
console.log('Forget SR', id)
|
||||
|
||||
return modal.confirm({
|
||||
title: 'SR forget',
|
||||
message: 'Are you sure you want to forget this SR? No VDI on this SR will be removed.',
|
||||
message: 'Are you sure you want to forget this SR? No VDI on this SR will be removed.'
|
||||
}).then(function () {
|
||||
return Bluebird.map($scope.SR.$PBDs, pbdId => {
|
||||
let pbd = xoApi.get(pbdId);
|
||||
let pbd = xoApi.get(pbdId)
|
||||
|
||||
return xoApi.call('pbd.disconnect', { id: pbd.id });
|
||||
});
|
||||
return xoApi.call('pbd.disconnect', { id: pbd.id })
|
||||
})
|
||||
}).then(function () {
|
||||
return xoApi.call('sr.forget', {id: UUID});
|
||||
return xoApi.call('sr.forget', {id: id})
|
||||
}).then(function () {
|
||||
$state.go('index');
|
||||
$state.go('index')
|
||||
notify.info({
|
||||
title: 'SR forget',
|
||||
message: 'SR is forgotten',
|
||||
});
|
||||
});
|
||||
};
|
||||
message: 'SR is forgotten'
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
$scope.saveDisks = function (data) {
|
||||
// Group data by disk.
|
||||
let disks = {};
|
||||
angular.forEach(data, function (value, key) {
|
||||
let i = key.indexOf('/');
|
||||
let disks = {}
|
||||
forEach(data, function (value, key) {
|
||||
let i = key.indexOf('/')
|
||||
|
||||
let id = key.slice(0, i);
|
||||
let prop = key.slice(i + 1);
|
||||
let id = key.slice(0, i)
|
||||
let prop = key.slice(i + 1)
|
||||
|
||||
(disks[id] || (disks[id] = {}))[prop] = value;
|
||||
});
|
||||
;(disks[id] || (disks[id] = {}))[prop] = value
|
||||
})
|
||||
|
||||
let promises = [];
|
||||
angular.forEach(disks, function (attributes, id) {
|
||||
let promises = []
|
||||
forEach(disks, function (attributes, id) {
|
||||
// Keep only changed attributes.
|
||||
let disk = get(id);
|
||||
let disk = get(id)
|
||||
|
||||
angular.forEach(attributes, function (value, name) {
|
||||
forEach(attributes, function (value, name) {
|
||||
if (value === disk[name]) {
|
||||
delete attributes[name];
|
||||
delete attributes[name]
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
if (!isEmpty(attributes)) {
|
||||
// Inject id.
|
||||
attributes.id = id;
|
||||
attributes.id = id
|
||||
|
||||
// Ask the server to update the object.
|
||||
promises.push(xoApi.call('vdi.set', attributes));
|
||||
promises.push(xoApi.call('vdi.set', attributes))
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
return $q.all(promises);
|
||||
};
|
||||
return $q.all(promises)
|
||||
}
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
;
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
div(ng-repeat="container in [SR.$container] | resolve")
|
||||
dd(ng-if="'pool' === container.type")
|
||||
| Yes (
|
||||
a(ui-sref="pools_view({id: container.UUID})") {{container.name_label}}
|
||||
a(ui-sref="pools_view({id: container.id})") {{container.name_label}}
|
||||
| )
|
||||
dd(ng-if="'host' === container.type") No
|
||||
dt Size
|
||||
@@ -53,16 +53,21 @@
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-stats(style="color: #e25440;")
|
||||
| Stats
|
||||
.grid
|
||||
.grid-cell
|
||||
p.stat-name Physical Alloc:
|
||||
canvas(id="doughnut", class="chart chart-doughnut", data="[(SR.physical_usage), (SR.size - SR.physical_usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
|
||||
.grid-cell
|
||||
p.stat-name Virtual Alloc:
|
||||
canvas(id="doughnut", class="chart chart-doughnut", data="[(SR.usage), (SR.size - SR.usage)]", labels="['Used', 'Free']", options='{tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>"}')
|
||||
.grid-cell
|
||||
p.stat-name VDIs:
|
||||
p.center.big-stat {{SR.VDIs.length}}
|
||||
.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
|
||||
@@ -72,7 +77,7 @@
|
||||
.panel-body.text-center
|
||||
.grid
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Rescan all the VDI", type="button", style="width: 90%", ng-click="rescanSr(SR.UUID)")
|
||||
button.btn(tooltip="Rescan all the VDI", 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()")
|
||||
@@ -81,10 +86,10 @@
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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
|
||||
@@ -94,7 +99,7 @@
|
||||
| VDI Map
|
||||
.panel-body
|
||||
.progress
|
||||
.progress-bar.progress-bar-vm(ng-if="((VDI.size/SR.size)*100) > 0.5", ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.UUID", role="progressbar", aria-valuemin="0", aria-valuenow="{{VDI.size}}", aria-valuemax="{{SR.size}}", style="width: {{[VDI.size, SR.size] | %}}", tooltip="{{VDI.name_label}} ({{[VDI.size, SR.size] | %}})")
|
||||
.progress-bar.progress-bar-vm(ng-if="((VDI.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
|
||||
@@ -109,7 +114,7 @@
|
||||
| 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.UUID)")
|
||||
span.quick-edit(tooltip="Rescan", ng-click="rescanSr(SR.id)")
|
||||
i.fa.fa-refresh.fa-fw
|
||||
.panel-body
|
||||
table.table.table-hover
|
||||
@@ -118,31 +123,33 @@
|
||||
th Description
|
||||
th Size
|
||||
th Virtual Machine:
|
||||
tr(ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label')")
|
||||
td
|
||||
tr(ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') | slice:(10*(currentVDIPage-1)):(10*currentVDIPage)")
|
||||
td.oneliner
|
||||
span(
|
||||
editable-text="VDI.name_label"
|
||||
e-name = '{{VDI.UUID}}/name_label'
|
||||
e-name = '{{VDI.id}}/name_label'
|
||||
)
|
||||
| {{VDI.name_label}}
|
||||
span.label.label-info(ng-if="VDI.$snapshot_of") snapshot
|
||||
td
|
||||
td.oneliner
|
||||
span(
|
||||
editable-text="VDI.name_description"
|
||||
e-name = '{{VDI.UUID}}/name_description'
|
||||
e-name = '{{VDI.id}}/name_description'
|
||||
)
|
||||
| {{VDI.name_description}}
|
||||
td
|
||||
//- FIXME: should be editable, but the server needs first
|
||||
//- to accept a human readable string.
|
||||
| {{VDI.size | bytesToSize}}
|
||||
td {{((VDI.$VBD | resolve).VM | resolve).name_label}}
|
||||
td.oneliner {{((VDI.$VBDs[0] | resolve).VM | resolve).name_label}}
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(ng-if="(VDI.$VBD | resolve).attached", xo-click="disconnectVBD(VBD.UUID)")
|
||||
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.$VBD | resolve).attached", xo-click="deleteVDI(VDI.UUID)")
|
||||
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.
|
||||
.center(ng-if = '(SR.VDIs | resolve).length > 10')
|
||||
pagination(boundary-links="true", total-items="(SR.VDIs | resolve).length", ng-model="$parent.currentVDIPage", items-per-page="10", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
|
||||
.btn-form(ng-show="disksForm.$visible")
|
||||
p.center
|
||||
button.btn.btn-default(
|
||||
@@ -172,31 +179,33 @@
|
||||
table.table.table-hover
|
||||
th Name
|
||||
th Status
|
||||
tr(ng-repeat="PBD in SR.$PBDs | resolve", xo-sref="hosts_view({id: (PBD.host | resolve).UUID})")
|
||||
tr(ng-repeat="PBD in SR.$PBDs | resolve", xo-sref="hosts_view({id: (PBD.host | resolve).id})")
|
||||
td {{(PBD.host | resolve).name_label}}
|
||||
td(ng-if="PBD.attached")
|
||||
span.label.label-success Connected
|
||||
span.pull-right.btn-group.quick-buttons
|
||||
a(xo-click="disconnectPBD(PBD.UUID)")
|
||||
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.UUID)")
|
||||
a(xo-click="connectPBD(PBD.id)")
|
||||
i.fa.fa-link.fa-lg(tooltip="Reconnect to this host")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-comments(style="color: #e25440;")
|
||||
| Logs
|
||||
.panel-body
|
||||
p.center(ng-if="!SR.messages.length") No recent logs
|
||||
table.table.table-hover(ng-if="SR.messages.length")
|
||||
p.center(ng-if="SR.messages | isEmpty") No recent logs
|
||||
table.table.table-hover(ng-if="SR.messages | isNotEmpty")
|
||||
th.col-md-1 Date
|
||||
th.col-md-1 Name
|
||||
tr(ng-repeat="message in SR.messages | resolve | orderBy:'-time' track by message.UUID")
|
||||
tr(ng-repeat="message in SR.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
| {{message.name}}
|
||||
a.quick-remove(tooltip="Remove log")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
.center(ng-if = '(SR.messages | count) > 5')
|
||||
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.
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
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'
|
||||
@@ -27,12 +30,21 @@ module.exports = angular.module 'xoWebApp.tree', [
|
||||
xo
|
||||
xoApi
|
||||
) ->
|
||||
Object.defineProperties($scope, {
|
||||
xo: { get: -> xoApi.byTypes.xo?[0] },
|
||||
pools: { get: -> xoApi.byTypes.pool },
|
||||
hosts: { get: -> xoApi.byTypes.host },
|
||||
VMs: { get: -> xoApi.byTypes.VM },
|
||||
})
|
||||
$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
|
||||
@@ -124,24 +136,11 @@ module.exports = angular.module 'xoWebApp.tree', [
|
||||
return true for _ of VM.current_operations
|
||||
false
|
||||
|
||||
# extract a value in a object
|
||||
$scope.values = (object) ->
|
||||
value for _, value of object
|
||||
|
||||
$scope.deleteVMs = ->
|
||||
{selected_VMs} = $scope
|
||||
|
||||
deleteVmsModal (id for id, selected of selected_VMs when selected)
|
||||
|
||||
$scope.osType = (osName) ->
|
||||
switch osName
|
||||
when 'debian','ubuntu','centos','redhat','oracle','gentoo','suse','fedora','sles'
|
||||
'linux'
|
||||
when 'windows'
|
||||
'windows'
|
||||
else
|
||||
'other'
|
||||
|
||||
# VMs checkboxes.
|
||||
do ->
|
||||
# This map marks which VMs are selected.
|
||||
@@ -162,7 +161,7 @@ module.exports = angular.module 'xoWebApp.tree', [
|
||||
|
||||
# Updates `all`, `none` and `master_selection` when necessary.
|
||||
$scope.$watch 'n_selected_VMs', (n) ->
|
||||
$scope.all = (xoApi.byTypes.VM?.length is n)
|
||||
$scope.all = (VMs.size is n)
|
||||
$scope.none = (n is 0)
|
||||
|
||||
# When the master checkbox is clicked from indeterminate
|
||||
@@ -176,22 +175,26 @@ module.exports = angular.module 'xoWebApp.tree', [
|
||||
true
|
||||
|
||||
$scope.selectVMs = (sieve) ->
|
||||
VMs = xoApi.byTypes.VM
|
||||
|
||||
if (sieve is true) or (sieve is false)
|
||||
$scope.n_selected_VMs = if sieve then VMs.length else 0
|
||||
selected_VMs[VM.UUID] = sieve for VM in VMs
|
||||
forEach(VMs.all, (VM) ->
|
||||
selected_VMs[VM.id] = sieve
|
||||
return
|
||||
)
|
||||
$scope.n_selected_VMs = if sieve then VMs.size else 0
|
||||
return
|
||||
|
||||
n = 0
|
||||
|
||||
matcher = make_matcher sieve
|
||||
++n for VM in VMs when (selected_VMs[VM.UUID] = matcher VM)
|
||||
n = 0
|
||||
forEach(VMs.all, (VM) ->
|
||||
if (selected_VMs[VM.id] = matcher(VM))
|
||||
++n
|
||||
return
|
||||
)
|
||||
|
||||
$scope.n_selected_VMs = n
|
||||
|
||||
$scope.updateVMSelection = (UUID) ->
|
||||
if selected_VMs[UUID]
|
||||
$scope.updateVMSelection = (id) ->
|
||||
if selected_VMs[id]
|
||||
++$scope.n_selected_VMs
|
||||
else
|
||||
--$scope.n_selected_VMs
|
||||
@@ -201,14 +204,18 @@ module.exports = angular.module 'xoWebApp.tree', [
|
||||
unless angular.isFunction fn
|
||||
throw new Error "invalid action #{action}"
|
||||
|
||||
for UUID, selected of selected_VMs
|
||||
fn UUID, args... if selected
|
||||
for id, selected of selected_VMs
|
||||
fn id, args... if selected
|
||||
|
||||
# Unselects all VMs.
|
||||
$scope.selectVMs false
|
||||
|
||||
$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 }) ->
|
||||
@@ -217,15 +224,6 @@ module.exports = angular.module 'xoWebApp.tree', [
|
||||
url
|
||||
data: file
|
||||
}
|
||||
.progress throttle(
|
||||
(event) ->
|
||||
percentage = (100 * event.loaded / event.total)|0
|
||||
|
||||
notify.info
|
||||
title: 'VM import'
|
||||
message: "#{percentage}%"
|
||||
6e3
|
||||
)
|
||||
.then (result) ->
|
||||
throw result.status if result.status isnt 200
|
||||
notify.info
|
||||
@@ -255,6 +253,63 @@ module.exports = angular.module 'xoWebApp.tree', [
|
||||
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
|
||||
notify.info({
|
||||
title: 'VM Migration'
|
||||
message: 'Starting your VM migration'
|
||||
})
|
||||
(xo.vm.migrate vm, targetHost).catch (error) ->
|
||||
modal.confirm
|
||||
title: 'VM migrate'
|
||||
message: 'This VM can\'t be migrated with Xen Motion to this host because they don\'t share any storage. Do you want to try a Xen Storage Motion?'
|
||||
|
||||
.then ->
|
||||
notify.info {
|
||||
title: 'VM migration'
|
||||
message: 'The migration process started'
|
||||
}
|
||||
|
||||
xo.vm.migratePool {
|
||||
id: vm
|
||||
target_host_id: targetHost
|
||||
}
|
||||
restrict: 'A'
|
||||
}
|
||||
# A module exports its name.
|
||||
.name
|
||||
|
||||
@@ -3,38 +3,43 @@
|
||||
.grid
|
||||
.grid-cell.overview
|
||||
//- Stats
|
||||
i(tooltip="{{xo.pools.length}} pools")
|
||||
i.small {{xo.pools.length}}x
|
||||
i(tooltip="{{pools.size}} pools").hidden-xs
|
||||
i.small {{pools.size}}x
|
||||
|
|
||||
i.xo-icon-pool
|
||||
|
|
||||
|
|
||||
i(tooltip="{{hosts.length}} hosts")
|
||||
i.small {{hosts.length}}x
|
||||
i(tooltip="{{hosts.size}} hosts").hidden-xs
|
||||
i.small {{hosts.size}}x
|
||||
|
|
||||
i.xo-icon-host
|
||||
|
|
||||
|
|
||||
i(tooltip="{{xo.$running_VMs.length}} of {{VMs.length}} VMs running")
|
||||
i.small {{xo.$running_VMs.length}}x
|
||||
i(tooltip="{{runningVms.size}} of {{VMs.size}} VMs running")
|
||||
i.small {{runningVms.size}}x
|
||||
|
|
||||
i.xo-icon-vm
|
||||
|
|
||||
|
|
||||
i(tooltip="{{xo.$vCPUs}} vCPUs used of {{xo.$CPUs}} CPUs")
|
||||
i.small {{xo.$vCPUs}}x
|
||||
i(tooltip="{{stats.$vCPUs}} vCPUs used of {{stats.$CPUs}} CPUs")
|
||||
i.small {{stats.$vCPUs}}x
|
||||
|
|
||||
i.xo-icon-cpu
|
||||
|
|
||||
|
|
||||
i(tooltip="{{xo.$memory.usage | bytesToSize}} RAM allocated of {{xo.$memory.size | bytesToSize}}")
|
||||
i.small {{xo.$memory.usage | bytesToSize}}
|
||||
i(tooltip="{{stats.$memory.usage | bytesToSize}} RAM allocated of {{stats.$memory.size | bytesToSize}}")
|
||||
i.small {{stats.$memory.usage | bytesToSize}}
|
||||
|
|
||||
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")
|
||||
input.inverse(
|
||||
type="checkbox",
|
||||
ng-model="master_selection",
|
||||
ng-change="selectVMs(master_selection)",
|
||||
ui-indeterminate="!(all || none)", stop-event="click"
|
||||
)
|
||||
|
|
||||
i.fa.fa-caret-down
|
||||
ul.dropdown-menu.inverse(role="menu")
|
||||
@@ -43,8 +48,11 @@
|
||||
i.fa-fw(class="xo-icon-{{power_state | lowercase}}")
|
||||
| {{power_state}}
|
||||
li.divider
|
||||
li(ng-repeat="host in hosts | orderBy:natural('name_label') track by host.UUID", ng-if="host.VMs.length")
|
||||
a(ng-click="selectVMs({$container: host.ref})")
|
||||
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")
|
||||
@@ -67,8 +75,8 @@
|
||||
|
|
||||
i.fa.fa-caret-down
|
||||
ul.dropdown-menu.inverse(role="menu")
|
||||
li(ng-repeat="host in hosts | orderBy:natural('name_label') track by host.UUID")
|
||||
a(ng-click="bulkAction('migrateVM',host.UUID)")
|
||||
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}}
|
||||
|
|
||||
@@ -112,13 +120,13 @@
|
||||
div(style="margin-top: 57px; visibility: hidden; height: 0") .
|
||||
|
||||
//- If we haven't any data
|
||||
div(ng-if="!pools.length")
|
||||
div(ng-if="!pools.size")
|
||||
.grid
|
||||
.panel.panel-default.text-center
|
||||
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")
|
||||
a.btn.btn-success.big(ui-sref="settings.index")
|
||||
i.fa.fa-plus-circle
|
||||
| Add server
|
||||
br
|
||||
@@ -131,17 +139,19 @@ div(ng-if="!pools.length")
|
||||
| Settings"
|
||||
p Enjoy Xen Orchestra!
|
||||
//- If we have data
|
||||
div(ng-if="pools.length")
|
||||
div(ng-if="pools.size")
|
||||
//- Contains a pool and all its children (hosts).
|
||||
.grid.pool-block(ng-repeat="pool in pools | orderBy:[natural('name_label'), 'id'] track by pool.UUID")
|
||||
.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.grid--gutters.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.grid--gutters.pool-cell
|
||||
.grid-cell.grid--gutters.pool-cell.hidden-xs
|
||||
//- Header (name + dropdown menu).
|
||||
.dropdown.dropdown-pool(dropdown)
|
||||
a.pool-name(ui-sref="pools_view({id: pool.UUID})")
|
||||
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")
|
||||
@@ -152,21 +162,21 @@ div(ng-if="pools.length")
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
//- TODO: remove until handled this properly
|
||||
//- li
|
||||
//- a(xo-sref="SRs_new({container: pool.UUID})")
|
||||
//- a(xo-sref="SRs_new({container: pool.id})")
|
||||
//- i.xo-icon-sr.fa-fw
|
||||
//- | Add SR
|
||||
li
|
||||
a(xo-sref="VMs_new({container: pool.UUID})")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
a.disabled(xo-click="pool_disconnect(pool.id)")
|
||||
i.fa.fa-unlink.fa-fw
|
||||
| Disconnect
|
||||
//- /Header.
|
||||
@@ -175,94 +185,94 @@ div(ng-if="pools.length")
|
||||
//- Stats.
|
||||
ul.list-unstyled.stats
|
||||
li
|
||||
i(tooltip="{{pool.hosts.length}} hosts connected")
|
||||
i.small {{pool.hosts.length}}x
|
||||
i(tooltip="{{hostsByPool[pool.id] | count}} hosts connected")
|
||||
i.small {{hostsByPool[pool.id] | count}}x
|
||||
|
|
||||
i.xo-icon-host
|
||||
|
|
||||
|
|
||||
i(tooltip="{{pool.$running_VMs.length}} of {{pool.$VMs.length}} VMs running")
|
||||
i.small {{pool.$running_VMs.length}}x
|
||||
i(tooltip="{{runningVmsByPool[pool.id] | count}} of {{vmsByPool[pool.id] | count}} VMs running")
|
||||
i.small {{runningVmsByPool[pool.id] | count}}x
|
||||
|
|
||||
i.xo-icon-vm
|
||||
li(ng-if="pool.master")
|
||||
| Master:
|
||||
|
|
||||
a(ui-sref="hosts_view({id: (pool.master | resolve).UUID})") {{(pool.master | resolve).name_label}}
|
||||
a(ui-sref="hosts_view({id: (pool.master | resolve).id})") {{(pool.master | resolve).name_label}}
|
||||
//- /Stats.
|
||||
//- SRs.
|
||||
div(ng-if="pool.SRs.length")
|
||||
div(ng-if="!(srsByContainer[pool.id] | isEmpty)")
|
||||
p.center.small-caps SRs:
|
||||
table.table.table-hover.table-condensed
|
||||
tr(ng-repeat="SR in pool.SRs | resolve | orderBy:natural('name_label') track by SR.UUID", xo-sref="SRs_view({id: SR.UUID})")
|
||||
td.col-md-6.sr-name.no-border(ng-class="{'default-sr': SR.ref === pool.default_SR}", title="{{SR.name_label}}")
|
||||
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] | %}} allocated")
|
||||
.progress-bar(role="progressbar", aria-valuenow="{{100*SR.usage/SR.size}}", aria-valuemin="0", aria-valuemax="100", style="width: {{[SR.usage, SR.size] | %}}")
|
||||
.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 pool.hosts | resolve | orderBy:natural('name_label') track by host.UUID")
|
||||
.grid(ng-repeat="host in hostsByPool[pool.id] | map | orderBy:natural('name_label') track by host.id")
|
||||
//- Contains information about the host.
|
||||
.grid-cell.grid--gutters.host-cell
|
||||
//- Header (name + dropdown menu).
|
||||
.dropdown.dropdown-pool(dropdown)
|
||||
a.host-name(ui-sref="hosts_view({id: host.UUID})")
|
||||
a.host-name(ui-sref="hosts_view({id: host.id})")
|
||||
| {{host.name_label}}
|
||||
a.dropdown-toggle(dropdown-toggle)
|
||||
|
|
||||
i.fa.fa-caret-down
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
li
|
||||
a(xo-sref="SRs_new({container: host.UUID})")
|
||||
a(xo-sref="SRs_new({container: host.id})")
|
||||
i.xo-icon-sr.fa-fw
|
||||
| Add SR
|
||||
li
|
||||
a(xo-sref="VMs_new({container: host.UUID})")
|
||||
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.UUID)")
|
||||
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 [host.controller] | resolve track by controller.UUID", xo-sref="consoles_view({id: controller.UUID})")
|
||||
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.UUID)")
|
||||
a(xo-click="enableHost(host.id)")
|
||||
i.fa.fa-check-circle.fa-fw
|
||||
| Enable
|
||||
li(ng-if="host.enabled")
|
||||
a(xo-click="disableHost(host.UUID)")
|
||||
a(xo-click="disableHost(host.id)")
|
||||
i.fa.fa-times-circle.fa-fw
|
||||
| Disable
|
||||
li
|
||||
a(xo-click="rebootHost(host.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
a(xo-click="shutdownHost(host.id)")
|
||||
i.fa.fa-power-off.fa-fw
|
||||
| Shutdown
|
||||
li
|
||||
a(xo-click="restartToolStack(host.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
a(xo-click="pool_addHost(host.id)")
|
||||
i.fa.fa-cloud-download.fa-fw
|
||||
| Add to pool
|
||||
//- /Header.
|
||||
@@ -278,16 +288,16 @@ div(ng-if="pools.length")
|
||||
//- 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] | %}} 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] | %}}")
|
||||
.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.grid--gutters.vm-cell
|
||||
.grid-cell.grid--gutters.vm-cell(droppable = 'true', host = '{{ host.id }}')
|
||||
//- If no VMs, fill the space with a message.
|
||||
.vms-notice(ng-if="!host.VMs.length")
|
||||
.vms-notice(ng-if="vmsByContainer[host.id] | isEmpty")
|
||||
//- | Host halted.
|
||||
p(ng-if="host.power_state === 'Halted'")
|
||||
| Host halted.
|
||||
@@ -298,52 +308,53 @@ div(ng-if="pools.length")
|
||||
| No VMs on this host.
|
||||
//- /Message if no VMs.
|
||||
//- TODO: comment
|
||||
.table-responsive(ng-if="host.VMs.length")
|
||||
.table-responsive(ng-if="!(vmsByContainer[host.id] | isEmpty)")
|
||||
table.table.table-hover.table-condensed
|
||||
//- Contains a VM.
|
||||
tr(ng-repeat="VM in host.VMs | resolve | orderBy:natural('name_label') track by VM.UUID", xo-sref="VMs_view({id: VM.UUID})")
|
||||
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.UUID]", ng-change="updateVMSelection(VM.UUID)")
|
||||
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 {{values(VM.current_operations)[0]}}")
|
||||
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-md-2
|
||||
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
|
||||
td.vm-quick-buttons.col-md-2.hidden-xs
|
||||
.quick-buttons
|
||||
a(tooltip="Shutdown VM", xo-click="stopVM(VM.UUID)")
|
||||
a(tooltip="Shutdown VM", xo-click="stopVM(VM.id)")
|
||||
i.fa.fa-stop
|
||||
a(tooltip="Start VM", xo-click="startVM(VM.UUID)")
|
||||
a(tooltip="Start VM", xo-click="startVM(VM.id)")
|
||||
i.fa.fa-play
|
||||
a(tooltip="Reboot VM", xo-click="rebootVM(VM.UUID)")
|
||||
a(tooltip="Reboot VM", xo-click="rebootVM(VM.id)")
|
||||
i.fa.fa-refresh
|
||||
a(tooltip="VM Console", xo-sref="consoles_view({id: VM.UUID})")
|
||||
a(tooltip="VM Console", xo-sref="consoles_view({id: VM.id})")
|
||||
i.xo-icon-console
|
||||
//- Description.
|
||||
td.vm-description.col-md-4
|
||||
i(class="xo-icon-{{osType(VM.os_version.distro)}}",ng-if="VM.os_version.distro", tooltip="{{VM.os_version.name}}")
|
||||
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}}")
|
||||
|
|
||||
i.fa.fa-fw(ng-if="!VM.os_version.distro")
|
||||
| {{VM.name_description}}
|
||||
//- Metrics.
|
||||
//- Memory
|
||||
td.vm-memory-stat.col-md-2
|
||||
td.vm-memory-stat.col-md-2.hidden-xs
|
||||
.cpu
|
||||
| {{VM.memory.size | bytesToSize}}
|
||||
i.fa.fa-fw(ng-if="VM.PV_drivers")
|
||||
i.xo-icon-docker.fa-fw(ng-if="VM.docker", tooltip="Docker enabled")
|
||||
i.fa.fa-fw(ng-if="VM.PV_drivers && !VM.docker")
|
||||
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
|
||||
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="pool.VMs.length")
|
||||
.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.
|
||||
@@ -352,48 +363,49 @@ div(ng-if="pools.length")
|
||||
.table-responsive
|
||||
table.table.table-hover.table-condensed
|
||||
//- Contains a VM.
|
||||
tr(ng-repeat="VM in pool.VMs | resolve | orderBy:natural('name_label') track by VM.UUID", xo-sref="VMs_view({id: VM.UUID})")
|
||||
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.UUID]", ng-change="updateVMSelection(VM.UUID)")
|
||||
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 {{values(VM.current_operations)[0]}}")
|
||||
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-md-2
|
||||
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
|
||||
td.vm-quick-buttons.col-md-2.hidden-xs
|
||||
.quick-buttons
|
||||
a(tooltip="Shutdown VM", xo-click="stopVM(VM.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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.UUID)")
|
||||
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
|
||||
i(class="xo-icon-{{osType(VM.os_version.distro)}}",ng-if="VM.os_version.distro", tooltip="{{VM.os_version.name}}")
|
||||
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}}")
|
||||
|
|
||||
i.fa.fa-fw(ng-if="!VM.os_version.distro")
|
||||
| {{VM.name_description}}
|
||||
//- Metrics.
|
||||
//- Memory
|
||||
td.vm-memory-stat.col-md-2
|
||||
td.vm-memory-stat.col-md-2.hidden-xs
|
||||
.cpu
|
||||
| {{VM.memory.size | bytesToSize}}
|
||||
i.fa.fa-fw(ng-if="VM.PV_drivers")
|
||||
i.xo-icon-docker.fa-fw(ng-if="VM.docker", tooltip="Docker enabled")
|
||||
i.fa.fa-fw(ng-if="VM.PV_drivers && !VM.docker")
|
||||
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
|
||||
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.
|
||||
|
||||
343
app/modules/updater/index.js
Normal file
343
app/modules/updater/index.js
Normal file
@@ -0,0 +1,343 @@
|
||||
import * as format from '@julien-f/json-rpc/format'
|
||||
import angular from 'angular'
|
||||
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'
|
||||
|
||||
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', [
|
||||
// notify
|
||||
])
|
||||
.factory('updater', function ($interval, $timeout) {
|
||||
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)
|
||||
.return(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()
|
||||
}
|
||||
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.token = token
|
||||
return token
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(NotRegistered, () => this.registerState = 'unregistered')
|
||||
.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'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1,12 +1,18 @@
|
||||
angular = require 'angular'
|
||||
isEmpty = require 'isempty'
|
||||
_difference = require 'lodash.difference'
|
||||
_sortBy = require 'lodash.sortby'
|
||||
filter = require 'lodash.filter'
|
||||
forEach = require 'lodash.foreach'
|
||||
isEmpty = require 'lodash.isempty'
|
||||
sortBy = require 'lodash.sortby'
|
||||
|
||||
isoDevice = require('../iso-device')
|
||||
|
||||
#=====================================================================
|
||||
|
||||
module.exports = angular.module 'xoWebApp.vm', [
|
||||
require 'angular-ui-router'
|
||||
require 'angular-ui-router',
|
||||
require 'angular-ui-bootstrap'
|
||||
|
||||
isoDevice
|
||||
]
|
||||
.config ($stateProvider) ->
|
||||
$stateProvider.state 'VMs_view',
|
||||
@@ -16,75 +22,148 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
.controller 'VmCtrl', (
|
||||
$scope, $state, $stateParams, $location, $q
|
||||
xoApi, xo
|
||||
sizeToBytesFilter, bytesToSizeFilter
|
||||
sizeToBytesFilter, bytesToSizeFilter, xoHideUnauthorizedFilter
|
||||
modal
|
||||
$window
|
||||
$timeout
|
||||
dateFilter
|
||||
notify
|
||||
) ->
|
||||
$window.bytesToSize = bytesToSizeFilter # FIXME dirty workaround to custom a Chart.js tooltip template
|
||||
{get} = xoApi
|
||||
|
||||
merge = do ->
|
||||
push = Array::push.apply.bind Array::push
|
||||
(args...) ->
|
||||
result = []
|
||||
for arg in args
|
||||
push result, arg if arg?
|
||||
result
|
||||
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
|
||||
|
||||
{byTypes} = xoApi
|
||||
$scope.hosts = byTypes.host
|
||||
|
||||
$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)
|
||||
|
||||
# build VDI list of this VM
|
||||
mountedIso = ''
|
||||
VDIs = []
|
||||
for VBD in VM.$VBDs
|
||||
oVbd = get VBD
|
||||
oVdi = get oVbd?.VDI
|
||||
VDIs.push oVdi if oVdi? && not oVbd.is_cd_drive
|
||||
if oVbd.is_cd_drive && oVdi? # "Load" the cd drive
|
||||
mountedIso = oVdi.UUID
|
||||
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);
|
||||
$scope.VDIs = sortBy(VDIs, (value) -> (get resolveVBD(value))?.position);
|
||||
|
||||
container = get VM.$container
|
||||
|
||||
if container.type is 'host'
|
||||
host = container
|
||||
pool = (get container.poolRef) ? {}
|
||||
pool = (get container.$poolId) ? {}
|
||||
else
|
||||
host = {}
|
||||
pool = container
|
||||
|
||||
$scope.networks = get pool.networks
|
||||
|
||||
default_SR = get pool.default_SR
|
||||
default_SR = if default_SR
|
||||
default_SR.UUID
|
||||
if VM.power_state is 'Running' && !($scope.isVMWorking($scope.VM))
|
||||
refreshStatControl.start()
|
||||
else
|
||||
''
|
||||
|
||||
SRs = $scope.SRs = get (merge pool.SRs, host.SRs)
|
||||
# compute writable accessible SR from this VM
|
||||
$scope.writable_SRs = (SR for SR in SRs when SR.content_type isnt 'iso')
|
||||
|
||||
prepareDiskData mountedIso
|
||||
|
||||
refreshStatControl.stop()
|
||||
)
|
||||
|
||||
descriptor = (obj) ->
|
||||
return obj.name_label + (if obj.name_description.length then ' - ' + obj.name_description else '')
|
||||
|
||||
prepareDiskData = (mounted) ->
|
||||
prepareDiskData = () ->
|
||||
# For populating adding position choice
|
||||
unfreePositions = [];
|
||||
maxPos = 0;
|
||||
@@ -96,34 +175,83 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
unfreePositions.push parseInt oVbd.position
|
||||
maxPos = if (oVbd.position > maxPos) then parseInt oVbd.position else maxPos
|
||||
|
||||
# $scope.vdiFreePos = _difference([0..++maxPos], unfreePositions)
|
||||
$scope.maxPos = maxPos
|
||||
|
||||
$scope.VDIOpts = []
|
||||
ISOOpts = []
|
||||
for SR in $scope.SRs
|
||||
VDIOpts = []
|
||||
authSRs = xoHideUnauthorizedFilter($scope.SRs)
|
||||
for SR in authSRs
|
||||
if 'iso' isnt SR.SR_type
|
||||
for rVdi in SR.VDIs
|
||||
oVdi = get rVdi
|
||||
|
||||
$scope.VDIOpts.push({
|
||||
VDIOpts.push({
|
||||
sr: descriptor(SR),
|
||||
label: descriptor(oVdi),
|
||||
vdi: oVdi
|
||||
})
|
||||
else
|
||||
for rIso in SR.VDIs
|
||||
oIso = get rIso
|
||||
ISOOpts.push({
|
||||
sr: SR.name_label,
|
||||
label: descriptor(oIso),
|
||||
iso: oIso
|
||||
})
|
||||
$scope.VDIOpts = VDIOpts
|
||||
|
||||
$scope.isoDeviceData = {
|
||||
opts: ISOOpts
|
||||
mounted
|
||||
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
|
||||
@@ -161,7 +289,7 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
}
|
||||
|
||||
$scope.suspendVM = (id) ->
|
||||
xo.vm.suspend id, true
|
||||
xo.vm.suspend id
|
||||
notify.info {
|
||||
title: 'VM suspend...'
|
||||
message: 'Suspend the VM'
|
||||
@@ -209,7 +337,7 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
snapshot = get (id)
|
||||
|
||||
result = {
|
||||
id: snapshot.UUID
|
||||
id: snapshot.id
|
||||
name_label: $data
|
||||
}
|
||||
|
||||
@@ -220,10 +348,10 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
|
||||
$scope.saveVM = ($data) ->
|
||||
{VM} = $scope
|
||||
{CPUs, memory, name_label, name_description, high_availability} = $data
|
||||
{CPUs, memory, name_label, name_description, high_availability, auto_poweron} = $data
|
||||
|
||||
$data = {
|
||||
id: VM.UUID
|
||||
id: VM.id
|
||||
}
|
||||
if memory isnt $scope.memorySize and (memory = sizeToBytesFilter memory)
|
||||
$data.memory = memory
|
||||
@@ -236,6 +364,8 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
$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
|
||||
|
||||
xoApi.call 'vm.set', $data
|
||||
|
||||
@@ -243,7 +373,6 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
# Disks
|
||||
#-----------------------------------------------------------------
|
||||
|
||||
# TODO: implement in XO-Server.
|
||||
$scope.moveDisk = (index, direction) ->
|
||||
{VDIs} = $scope
|
||||
|
||||
@@ -267,7 +396,7 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
$scope.saveDisks = (data) ->
|
||||
# Group data by disk.
|
||||
disks = {}
|
||||
angular.forEach data, (value, key) ->
|
||||
forEach data, (value, key) ->
|
||||
i = key.indexOf '/'
|
||||
(disks[key.slice 0, i] ?= {})[key.slice i + 1] = value
|
||||
return
|
||||
@@ -275,17 +404,17 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
promises = []
|
||||
|
||||
# Handle SR change.
|
||||
angular.forEach disks, (attributes, id) ->
|
||||
forEach disks, (attributes, id) ->
|
||||
disk = get id
|
||||
if attributes.$SR isnt disk.$SR
|
||||
promises.push (migrateDisk id, attributes.$SR)
|
||||
|
||||
return
|
||||
|
||||
angular.forEach disks, (attributes, id) ->
|
||||
forEach disks, (attributes, id) ->
|
||||
# Keep only changed attributes.
|
||||
disk = get id
|
||||
angular.forEach attributes, (value, name) ->
|
||||
forEach attributes, (value, name) ->
|
||||
delete attributes[name] if value is disk[name]
|
||||
return
|
||||
|
||||
@@ -298,13 +427,30 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
return
|
||||
|
||||
# Handle Position changes
|
||||
mountedPos = (get resolveVBD(get $scope.isoDeviceData.mounted))?.position
|
||||
{VDIs} = $scope
|
||||
VDIs.forEach (vdi, index) ->
|
||||
oVbd = get resolveVBD(vdi)
|
||||
offset = if (mountedPos? && index >= mountedPos) then 1 else 0
|
||||
if oVbd? && index isnt oVbd.position
|
||||
promises.push xoApi.call 'vbd.set', {id: oVbd.id, position: String(index + offset)}
|
||||
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) ->
|
||||
@@ -314,23 +460,23 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
message: err
|
||||
}
|
||||
|
||||
$scope.deleteDisk = (UUID) ->
|
||||
$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: UUID}
|
||||
xoApi.call 'vdi.delete', {id: id}
|
||||
return
|
||||
return
|
||||
|
||||
#-----------------------------------------------------------------
|
||||
|
||||
# returns the ref of the VBD that links the VDI to the VM
|
||||
# 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.ref
|
||||
rVbd = vbd if (get vbd).VM is $scope.VM.id
|
||||
return rVbd || null
|
||||
|
||||
$scope.disconnectVBD = (vdi) ->
|
||||
@@ -419,14 +565,23 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
# 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 ->
|
||||
for log in $scope.VM.messages
|
||||
console.log "Remove log #{log}"
|
||||
xo.log.delete log
|
||||
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}"
|
||||
@@ -444,39 +599,37 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
}
|
||||
xo.vm.revert id
|
||||
|
||||
$scope.osType = (osName) ->
|
||||
switch osName
|
||||
when 'debian','ubuntu','centos','redhat','oracle','gentoo','suse','fedora','sles'
|
||||
'linux'
|
||||
when 'windows'
|
||||
'windows'
|
||||
else
|
||||
'other'
|
||||
|
||||
$scope.isVMWorking = (VM) ->
|
||||
return false unless VM
|
||||
return true for _ of VM.current_operations
|
||||
false
|
||||
|
||||
# extract a value in a object
|
||||
$scope.values = (object) ->
|
||||
value for _, value of object
|
||||
$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
|
||||
|
||||
params = {
|
||||
bootable
|
||||
mode : if (readonly || !isFreeForWriting(vdi)) then 'RO' else 'RW'
|
||||
position: String(position)
|
||||
vdi: vdi.UUID
|
||||
vm: $scope.VM.UUID
|
||||
}
|
||||
|
||||
console.log(params)
|
||||
return xoApi.call 'vm.attachDisk', params
|
||||
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
|
||||
|
||||
@@ -504,28 +657,13 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
$scope.createVdiWaiting = true # disables form fields
|
||||
position = $scope.maxPos + 1
|
||||
|
||||
params = {
|
||||
name
|
||||
size: String(size)
|
||||
sr
|
||||
}
|
||||
|
||||
# console.log(params)
|
||||
return xoApi.call 'disk.create', params
|
||||
return xo.disk.create name, String(size), sr
|
||||
|
||||
.then (diskUuid) ->
|
||||
params = {
|
||||
bootable,
|
||||
mode: if readonly then 'RO' else 'RW'
|
||||
position: String(position)
|
||||
vdi: diskUuid
|
||||
vm: $scope.VM.UUID
|
||||
}
|
||||
mode = if readonly then 'RO' else 'RW'
|
||||
return xo.vm.attachDisk $scope.VM.id, diskUuid, bootable, mode, String(position)
|
||||
|
||||
# console.log(params)
|
||||
return xoApi.call 'vm.attachDisk', params
|
||||
|
||||
.then -> $scope.creating = false # Closes form block
|
||||
.then -> $scope.creatingVdi = false # Closes form block
|
||||
|
||||
.catch (err) ->
|
||||
console.log(err);
|
||||
@@ -552,29 +690,17 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
$scope.createVifWaiting = true # disables form fields
|
||||
|
||||
position = 0
|
||||
$scope.VM.VIFs.forEach (vf) ->
|
||||
forEach $scope.VM.VIFs, (vf) ->
|
||||
int = get vf
|
||||
position = if int?.device > position then (get vf)?.device else position
|
||||
|
||||
position++
|
||||
|
||||
params = {
|
||||
vm: $scope.VM.UUID
|
||||
network: network.UUID
|
||||
position: String(position) # TODO
|
||||
mtu: String(mtu) || String(network.mtu)
|
||||
}
|
||||
|
||||
if !automac
|
||||
params.mac = mac
|
||||
|
||||
# console.log(params)
|
||||
|
||||
return xoApi.call 'vm.createInterface', params
|
||||
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
|
||||
# console.log(id)
|
||||
xoApi.call 'vif.connect', {id}
|
||||
.catch (err) ->
|
||||
console.log(err);
|
||||
notify.error {
|
||||
@@ -584,5 +710,12 @@ module.exports = angular.module 'xoWebApp.vm', [
|
||||
.finally ->
|
||||
$scope.createVifWaiting = false
|
||||
|
||||
$scope.statView = {
|
||||
cpuOnly: false,
|
||||
ramOnly: false,
|
||||
netOnly: false,
|
||||
diskOnly: false
|
||||
}
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.grid
|
||||
.panel.panel-default
|
||||
p.page-title
|
||||
i.xo-icon-vm(ng-if="isVMWorking(VM)", class="xo-color-pending", tooltip="{{VM.power_state}} and {{values(VM.current_operations)[0]}}")
|
||||
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
|
||||
@@ -22,35 +22,42 @@
|
||||
dd
|
||||
span(editable-text="VM.name_description", e-name="name_description", e-form="vmSettings")
|
||||
| {{VM.name_description}}
|
||||
dt(ng-if="VM.power_state == ('Running' || 'Paused')") Running on:
|
||||
dt(ng-if="VM.power_state == ('Halted')") Resident on:
|
||||
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"')
|
||||
a(xo-sref="hosts_view({id: container.UUID})")
|
||||
a(xo-sref="hosts_view({id: container.id})")
|
||||
| {{container.name_label}}
|
||||
small
|
||||
span(ng-if="(container.poolRef | resolve).name_label")
|
||||
span(ng-if="(container.$poolId | resolve).name_label")
|
||||
| (
|
||||
a(ui-sref="pools_view({id: (container.poolRef | resolve).UUID})") {{(container.poolRef | resolve).name_label}}
|
||||
a(ui-sref="pools_view({id: (container.$poolId | resolve).id})") {{(container.$poolId | resolve).name_label}}
|
||||
| )
|
||||
a(
|
||||
ng-if = 'container.type === "pool"'
|
||||
xo-sref="pools_view({id: container.UUID})"
|
||||
xo-sref="pools_view({id: container.id})"
|
||||
)
|
||||
| {{container.name_label}}
|
||||
dt(ng-if="VM.addresses") Address
|
||||
dt Addresses
|
||||
dd(ng-if="!VM.addresses") -
|
||||
dd(ng-repeat="IP in VM.addresses") {{IP}}
|
||||
span(ng-if="(VM.poolRef | resolve).HA_enabled")
|
||||
dt HA
|
||||
dd
|
||||
span(
|
||||
editable-checkbox="VM.high_availability"
|
||||
e-name="high_availability"
|
||||
e-form="vmSettings"
|
||||
)
|
||||
| {{VM.high_availability}}
|
||||
dt Tags
|
||||
dd
|
||||
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(
|
||||
@@ -69,6 +76,16 @@
|
||||
| {{memorySize}}
|
||||
dt UUID
|
||||
dd {{VM.UUID}}
|
||||
dt(ng-if="refreshStatControl.running && stats") Xen tools
|
||||
dd(ng-if="refreshStatControl.running && stats")
|
||||
span(ng-if="VM.PV_drivers", style="color:green;") Installed
|
||||
span(ng-if="!VM.PV_drivers") NOT installed
|
||||
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(
|
||||
@@ -86,31 +103,143 @@
|
||||
i.fa.fa-save
|
||||
| Save
|
||||
|
||||
.panel.panel-default
|
||||
.panel.panel-default.panel-height.center
|
||||
.panel-heading.panel-title
|
||||
i.xo-icon-stats(style="color: #e25440;")
|
||||
i.xo-icon-stats(style="color: #e25440;", xo-click="refreshStats(VM.id)")
|
||||
| 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
|
||||
.grid
|
||||
.grid-cell(ng-if="VM.os_version.distro")
|
||||
p.stat-name OS:
|
||||
p.center.big
|
||||
i(class="xo-icon-{{osType(VM.os_version.distro)}}",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
|
||||
.panel-body-stats(ng-if="refreshStatControl.running && stats")
|
||||
div(ng-if="statView.cpuOnly", ng-click="statView.cpuOnly = false")
|
||||
p.stat-name
|
||||
i.fa.fa-tachometer
|
||||
| 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.fa.fa-bar-chart
|
||||
i.fa.fa-tasks
|
||||
//- i.fa.fa-server
|
||||
| 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.fa.fa-sitemap
|
||||
| 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.fa.fa-hdd-o
|
||||
| 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.fa.fa-tachometer
|
||||
| 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.fa.fa-bar-chart
|
||||
i.fa.fa-tasks
|
||||
//- i.fa.fa-server
|
||||
| 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.fa.fa-sitemap
|
||||
| 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.fa.fa-hdd-o
|
||||
| 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.fa.fa-circle-o-notch.fa-spin.fa-2x
|
||||
| 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
|
||||
|
||||
//- Action panel
|
||||
.grid
|
||||
@@ -121,19 +250,19 @@
|
||||
.panel-body.text-center
|
||||
.grid
|
||||
.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.UUID)")
|
||||
button.btn(tooltip="Stop VM", type="button", style="width: 90%", xo-click="stopVM(VM.id)")
|
||||
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.UUID)")
|
||||
button.btn(tooltip="Suspend VM", type="button", style="width: 90%", xo-click="suspendVM(VM.id)")
|
||||
i.fa.fa-pause.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.UUID)")
|
||||
button.btn(tooltip="Resume VM", type="button", style="width: 90%", xo-click="resumeVM(VM.id)")
|
||||
i.fa.fa-play.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.UUID)")
|
||||
button.btn(tooltip="Start VM", type="button", style="width: 90%", xo-click="startVM(VM.id)")
|
||||
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.UUID)")
|
||||
button.btn(tooltip="Reboot VM", type="button", style="width: 90%", xo-click="rebootVM(VM.id)")
|
||||
i.fa.fa-refresh.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(
|
||||
ng-if="VM.power_state == ('Running' || 'Paused')"
|
||||
@@ -148,18 +277,18 @@
|
||||
i.fa.fa-share.fa-2x.fa-fw
|
||||
<span class="caret"></span>
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
li(ng-repeat="host in hosts | orderBy:natural('name_label') track by host.UUID")
|
||||
a(ng-click="migrateVM(VM.UUID, host.UUID)")
|
||||
li(ng-repeat="host in hosts.all | orderBy:natural('name_label') track by host.id")
|
||||
a(ng-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.UUID)")
|
||||
button.btn(tooltip="Force Reboot", type="button", style="width: 90%", xo-click="force_rebootVM(VM.id)")
|
||||
i.fa.fa-flash.fa-2x.fa-fw
|
||||
.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.UUID)")
|
||||
button.btn(tooltip="Force Shutdown", type="button", style="width: 90%", xo-click="force_stopVM(VM.id)")
|
||||
i.fa.fa-power-off.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.UUID)")
|
||||
button.btn(tooltip="Delete VM", type="button", style="width: 90%", xo-click="destroyVM(VM.id)")
|
||||
i.fa.fa-trash-o.fa-2x.fa-fw
|
||||
.grid-cell.btn-group.dropdown(
|
||||
ng-if="VM.power_state == ('Halted')"
|
||||
@@ -175,25 +304,77 @@
|
||||
<span class="caret"></span>
|
||||
ul.dropdown-menu.left(role="menu")
|
||||
li
|
||||
a(ng-click="cloneVM(VM.UUID,VM.name_label,false)")
|
||||
a(ng-click="cloneVM(VM.id,VM.name_label,false)")
|
||||
i.fa.fa-code-fork.fa-fw
|
||||
| Fast clone
|
||||
li
|
||||
a(ng-click="cloneVM(VM.UUID,VM.name_label,true)")
|
||||
a(ng-click="cloneVM(VM.id,VM.name_label,true)")
|
||||
i.xo-icon-sr.fa-fw
|
||||
| Full disk copy
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Convert to template", type="button", style="width: 90%", xo-click="convertVM(VM.UUID)")
|
||||
.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)")
|
||||
i.fa.fa-thumb-tack.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Create a snapshot", style="width: 90%", type="button", xo-click="snapshotVM(VM.UUID,VM.name_label)")
|
||||
button.btn(tooltip="Create a snapshot", style="width: 90%", type="button", xo-click="snapshotVM(VM.id,VM.name_label)")
|
||||
i.xo-icon-snapshot.fa-2x.fa-fw
|
||||
.grid-cell.btn-group
|
||||
button.btn(tooltip="Export the VM", style="width: 90%", type="button", xo-click="exportVM(VM.UUID)")
|
||||
button.btn(tooltip="Export the VM", style="width: 90%", type="button", xo-click="exportVM(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.UUID})")
|
||||
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(ng-if="VM.docker")
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-ship(style="color: #e25440;")
|
||||
| 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
|
||||
.panel.panel-default
|
||||
@@ -223,31 +404,31 @@
|
||||
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.UUID')
|
||||
td
|
||||
tr(ng-repeat = 'VDI in VDIs track by VDI.id')
|
||||
td.oneliner
|
||||
span(
|
||||
editable-text="VDI.name_label"
|
||||
e-name = '{{VDI.UUID}}/name_label'
|
||||
e-name = '{{VDI.id}}/name_label'
|
||||
)
|
||||
| {{VDI.name_label}}
|
||||
td
|
||||
td.oneliner
|
||||
span(
|
||||
editable-text="VDI.name_description"
|
||||
e-name = '{{VDI.UUID}}/name_description'
|
||||
e-name = '{{VDI.id}}/name_description'
|
||||
)
|
||||
| {{VDI.name_description}}
|
||||
td
|
||||
//- FIXME: should be editable, but the server needs first
|
||||
//- to accept a human readable string.
|
||||
| {{VDI.size | bytesToSize}}
|
||||
td
|
||||
td.oneliner
|
||||
span(
|
||||
editable-select="(VDI.$SR | resolve).ref"
|
||||
e-ng-options="SR.ref as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in writable_SRs"
|
||||
e-name = '{{VDI.UUID}}/$SR'
|
||||
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).UUID})")
|
||||
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
|
||||
@@ -273,7 +454,7 @@
|
||||
i.fa.fa-ban.fa-lg
|
||||
a(
|
||||
tooltip="Remove this disk"
|
||||
xo-click="deleteDisk(VDI.UUID)"
|
||||
xo-click="deleteDisk(VDI.id)"
|
||||
)
|
||||
i.fa.fa-trash-o.fa-lg
|
||||
td(ng-show="disksForm.$visible")
|
||||
@@ -311,18 +492,23 @@
|
||||
| Save
|
||||
.grid
|
||||
.col-md-4
|
||||
iso-device(ng-if = 'VM && isoDeviceData', vm = 'VM', isos = 'isoDeviceData')
|
||||
iso-device(ng-if = 'VM && SRs', vm = 'VM', srs = 'SRs')
|
||||
.col-md-8.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")
|
||||
button.btn(type="button", ng-class = '{"btn-success": adding, "btn-primary": !adding}', ng-disabled="disksForm.$waiting", ng-click="adding = !adding;creatingVdi = false;bootReordering = false")
|
||||
i.fa.fa-plus(ng-if = '!adding')
|
||||
i.fa.fa-minus(ng-if = 'adding')
|
||||
| Attach Disk
|
||||
|
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingVdi, "btn-primary": !creatingVdi}', ng-disabled="disksForm.$waiting", ng-click="creatingVdi = !creatingVdi;adding = false")
|
||||
button.btn(type="button", ng-class = '{"btn-success": creatingVdi, "btn-primary": !creatingVdi}', ng-disabled="disksForm.$waiting", ng-click="creatingVdi = !creatingVdi;adding = false;bootReordering = false")
|
||||
i.fa.fa-plus(ng-if = '!creatingVdi')
|
||||
i.fa.fa-minus(ng-if = 'creatingVdi')
|
||||
| New Disk
|
||||
|
|
||||
button.btn(type="button", ng-class = '{"btn-success": bootReordering, "btn-primary": !bootReordering}', ng-disabled="disksForm.$waiting", ng-click="bootReordering = !bootReordering;adding = false;creatingVdi = false")
|
||||
i.fa.fa-plus(ng-if = '!bootReordering')
|
||||
i.fa.fa-minus(ng-if = 'bootReordering')
|
||||
| Boot order
|
||||
br
|
||||
form.form-inline#addDiskForm(ng-if = 'adding', name = 'addForm', ng-submit = 'addVdi(vdiToAdd.vdi, vdiReadOnly, vdiBootable)')
|
||||
fieldset(ng-attr-disabled = '{{ addWaiting ? true : undefined }}')
|
||||
@@ -364,7 +550,7 @@
|
||||
|
|
||||
.form-group
|
||||
//- label(for = 'newDiskSR') SR
|
||||
select.form-control(ng-model = 'newDiskSR', required, ng-options="SR.ref as (SR.name_label + ' (' + (SR.size - SR.usage | bytesToSize) + ' free)') for SR in writable_SRs")
|
||||
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
|
||||
|
|
||||
br
|
||||
@@ -392,7 +578,20 @@
|
||||
|
|
||||
i.fa.fa-spin.fa-circle-o-notch
|
||||
br
|
||||
|
||||
form#bootOrderForm(ng-if = 'bootReordering', ng-submit = 'saveBootParams(VM.id, bootParams)')
|
||||
fieldset(ng-attr-disabled = '{{ savingBootOrder ? true : undefined }}')
|
||||
.form-group(ng-repeat = 'elem in bootParams')
|
||||
label
|
||||
span(ng-class = '{"text-muted": !elem.v}') {{ elem.t }}
|
||||
input(type = 'checkbox', ng-model = 'elem.v')
|
||||
|
|
||||
button.btn.btn-default(type = 'button', ng-click = 'bootMove($index, -1)', ng-class = '{disabled: $first}'): i.fa.fa-chevron-up
|
||||
|
|
||||
button.btn.btn-default(type = 'button', ng-click = 'bootMove($index, 1)', ng-class = '{disabled: $last}'): i.fa.fa-chevron-down
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit', ng-disabled = 'disksForm.$waiting')
|
||||
i.fa.fa-database
|
||||
| Save
|
||||
//- TODO: add interface in this panel
|
||||
.grid
|
||||
.panel.panel-default
|
||||
@@ -406,25 +605,25 @@
|
||||
th MTU
|
||||
th Network
|
||||
th Link status
|
||||
tr(ng-repeat="VIF in VM.VIFs | resolve | orderBy:natural('device') track by VIF.UUID")
|
||||
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
|
||||
td.oneliner
|
||||
| {{(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')", xo-click="disconnectVIF(VIF.UUID)")
|
||||
a(tooltip="Disconnect this interface", ng-if="VM.power_state == ('Running' || 'Paused')", 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.UUID)")
|
||||
a(tooltip="Connect this interface", xo-click="connectVIF(VIF.id)")
|
||||
i.fa.fa-link.fa-lg
|
||||
a(tooltip="Remove this interface", xo-click="deleteVIF(VIF.UUID)")
|
||||
a(tooltip="Remove this interface", xo-click="deleteVIF(VIF.id)")
|
||||
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")
|
||||
@@ -432,11 +631,20 @@
|
||||
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)')
|
||||
form.form-inline.text-right#createInterfaceForm(
|
||||
ng-if = 'creatingVif'
|
||||
name = 'createInterfaceForm'
|
||||
ng-submit = 'createInterface(newInterfaceNetwork, newInterfaceMTU, autoMac, newInterfaceMAC)'
|
||||
)
|
||||
fieldset(ng-attr-disabled = '{{ createVifWaiting ? true : undefined }}')
|
||||
.form-group
|
||||
label(for = 'newVifNetwork') Network
|
||||
select.form-control(ng-model = 'newInterfaceNetwork', ng-change = 'updateMTU(newInterfaceNetwork)', required, ng-options='network.name_label for network in networks')
|
||||
select.form-control(
|
||||
ng-options='network.name_label for network in networks'
|
||||
ng-model = 'newInterfaceNetwork'
|
||||
ng-change = 'updateMTU(newInterfaceNetwork)'
|
||||
required
|
||||
)
|
||||
option(value = '', disabled) --
|
||||
|
|
||||
.form-group
|
||||
@@ -456,7 +664,7 @@
|
||||
.form-group
|
||||
button.btn.btn-primary(type = 'submit')
|
||||
i.fa.fa-plus-square
|
||||
| Create
|
||||
| Create
|
||||
span(ng-if = 'createVifWaiting')
|
||||
|
|
||||
i.fa.fa-spin.fa-circle-o-notch
|
||||
@@ -474,18 +682,20 @@
|
||||
table.table.table-hover(ng-if="VM.snapshots.length")
|
||||
th Date
|
||||
th Name
|
||||
tr(ng-repeat="snapshot in VM.snapshots | resolve | orderBy:'-snapshot_time' track by snapshot.UUID")
|
||||
td {{snapshot.snapshot_time*1e3 | date:"medium"}}
|
||||
td
|
||||
span(editable-text="snapshot.name_label", e-name="name_label", e-form="vmSnap", onbeforesave="saveSnapshot(snapshot.UUID, $data)")
|
||||
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.UUID)")
|
||||
a(tooltip="Export this snapshot", type="button", xo-click="exportVM(snapshot.id)")
|
||||
i.fa.fa-upload.fa-lg
|
||||
a(tooltip="Revert VM to this snapshot", xo-click="revertSnapshot(snapshot.UUID)")
|
||||
a(tooltip="Revert VM to this snapshot", xo-click="revertSnapshot(snapshot.id)")
|
||||
i.fa.fa-undo.fa-lg
|
||||
a(tooltip="Remove this snapshot", xo-click="deleteSnapshot(snapshot.UUID)")
|
||||
a(tooltip="Remove this snapshot", xo-click="deleteSnapshot(snapshot.id)")
|
||||
i.fa.fa-trash-o.fa-lg
|
||||
.center(ng-if = '(VM.snapshots | count) > 5')
|
||||
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()")
|
||||
@@ -500,17 +710,62 @@
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-comments(style="color: #e25440;")
|
||||
| Logs
|
||||
span.quick-edit(ng-if="VM.messages.length", tooltip="Remove all logs", xo-click="deleteAllLog()")
|
||||
span.quick-edit(ng-if="VM.messages | isNotEmpty", tooltip="Remove all logs", xo-click="deleteAllLog()")
|
||||
i.fa.fa-trash-o.fa-fw
|
||||
.panel-body
|
||||
p.center(ng-if="!VM.messages.length") No recent logs
|
||||
table.table.table-hover(ng-if="VM.messages.length")
|
||||
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 | resolve | orderBy:'-time' track by message.UUID")
|
||||
td {{message.time*1e3 | date:"medium"}}
|
||||
td
|
||||
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
|
||||
a(xo-click="deleteLog(message.UUID)")
|
||||
a(xo-click="deleteLog(message.id)")
|
||||
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
|
||||
.center(ng-if = '(VM.messages | count) > 5')
|
||||
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
|
||||
.panel.panel-default
|
||||
.panel-heading.panel-title
|
||||
i.fa.fa-plug(style="color: #e25440;")
|
||||
| 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)")
|
||||
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)")
|
||||
i.fa.fa-link.fa-lg
|
||||
.center(ng-if = '((VM.$container | resolve).$PCIs | resolve).length > 5')
|
||||
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(style="color: #e25440;")
|
||||
| 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')
|
||||
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=">>")
|
||||
|
||||
83
app/node_modules/angular-no-vnc/index.js
generated
vendored
83
app/node_modules/angular-no-vnc/index.js
generated
vendored
@@ -1,91 +1,91 @@
|
||||
import angular from 'angular';
|
||||
import angular from 'angular'
|
||||
import {
|
||||
format as formatUrl,
|
||||
parse as parseUrl,
|
||||
resolve as resolveUrl
|
||||
} from 'url';
|
||||
import {RFB} from 'novnc-node';
|
||||
} from 'url'
|
||||
import {RFB} from 'novnc-node'
|
||||
|
||||
import view from './view';
|
||||
import view from './view'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
function parseRelativeUrl(url) {
|
||||
function parseRelativeUrl (url) {
|
||||
/* global window: false */
|
||||
return parseUrl(resolveUrl(String(window.location), url));
|
||||
return parseUrl(resolveUrl(String(window.location), url))
|
||||
}
|
||||
|
||||
const PROTOCOL_ALIASES = {
|
||||
'http:': 'ws:',
|
||||
'https:': 'wss:',
|
||||
};
|
||||
'https:': 'wss:'
|
||||
}
|
||||
|
||||
function fixProtocol(url) {
|
||||
let protocol = PROTOCOL_ALIASES[url.protocol];
|
||||
function fixProtocol (url) {
|
||||
let protocol = PROTOCOL_ALIASES[url.protocol]
|
||||
|
||||
if (protocol) {
|
||||
url.protocol = protocol;
|
||||
url.protocol = protocol
|
||||
}
|
||||
}
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('no-vnc', [])
|
||||
.controller('NoVncCtrl', function ($attrs, $element, $scope) {
|
||||
this.height = 480;
|
||||
this.height = 480
|
||||
$attrs.$observe('height', (height) => {
|
||||
this.height = height;
|
||||
});
|
||||
this.width = 640;
|
||||
this.height = height
|
||||
})
|
||||
this.width = 640
|
||||
$attrs.$observe('width', (width) => {
|
||||
this.width = width;
|
||||
});
|
||||
this.width = width
|
||||
})
|
||||
|
||||
let rfb;
|
||||
function clean() {
|
||||
let rfb
|
||||
function clean () {
|
||||
// If there was a previous connection.
|
||||
if (rfb) {
|
||||
rfb.disconnect();
|
||||
rfb = undefined;
|
||||
rfb.disconnect()
|
||||
rfb = undefined
|
||||
}
|
||||
}
|
||||
|
||||
this.remoteControl = {
|
||||
sendCtrlAltDel() {
|
||||
sendCtrlAltDel () {
|
||||
if (rfb) {
|
||||
rfb.sendCtrlAltDel();
|
||||
rfb.sendCtrlAltDel()
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let canvas = $element.find('canvas')[0];
|
||||
let canvas = $element.find('canvas')[0]
|
||||
|
||||
$attrs.$observe('url', (url) => {
|
||||
// Remove previous connection.
|
||||
clean();
|
||||
clean()
|
||||
|
||||
// If the URL is empty, stop now.
|
||||
if (!url) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the URL.
|
||||
url = parseRelativeUrl(url);
|
||||
fixProtocol(url);
|
||||
url = parseRelativeUrl(url)
|
||||
fixProtocol(url)
|
||||
|
||||
let isSecure = url.protocol === 'wss:';
|
||||
let isSecure = url.protocol === 'wss:'
|
||||
|
||||
rfb = new RFB({
|
||||
encrypt: isSecure,
|
||||
target: canvas,
|
||||
wsProtocols: ['chat'],
|
||||
});
|
||||
wsProtocols: ['chat']
|
||||
})
|
||||
|
||||
// Connect.
|
||||
rfb.connect(formatUrl(url));
|
||||
});
|
||||
rfb.connect(formatUrl(url))
|
||||
})
|
||||
|
||||
$scope.$on('$destroy', clean);
|
||||
$scope.$on('$destroy', clean)
|
||||
})
|
||||
.directive('noVnc', function () {
|
||||
return {
|
||||
@@ -93,10 +93,9 @@ export default angular.module('no-vnc', [])
|
||||
controller: 'NoVncCtrl as noVnc',
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
remoteControl: '=',
|
||||
remoteControl: '='
|
||||
},
|
||||
template: view,
|
||||
};
|
||||
template: view
|
||||
}
|
||||
})
|
||||
.name
|
||||
;
|
||||
|
||||
36
app/node_modules/master-select/demo.js
generated
vendored
Normal file
36
app/node_modules/master-select/demo.js
generated
vendored
Normal 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
87
app/node_modules/master-select/index.js
generated
vendored
Normal 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
10
app/node_modules/master-select/package.json
generated
vendored
Normal 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
31
app/node_modules/master-select/view.jade
generated
vendored
Normal 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}}
|
||||
444
app/node_modules/xo-api/index.js
generated
vendored
444
app/node_modules/xo-api/index.js
generated
vendored
@@ -1,139 +1,413 @@
|
||||
import angular from 'angular';
|
||||
import indexOf from 'lodash.indexof';
|
||||
let isArray = angular.isArray;
|
||||
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'
|
||||
|
||||
import 'angular-cookies';
|
||||
const {defineProperty} = Object
|
||||
const {isArray, isString} = angular
|
||||
|
||||
import xoLib from 'xo-lib';
|
||||
// ===================================================================
|
||||
|
||||
// Low level XO API for Angular.
|
||||
export default angular.module('xo-api', [
|
||||
'ngCookies',
|
||||
angularCookies
|
||||
])
|
||||
.run(function ($rootScope) {
|
||||
// Ensure correct integration with Angular.
|
||||
xoLib.setScheduler(function (fn) {
|
||||
$rootScope.$evalAsync(fn);
|
||||
});
|
||||
$rootScope.$evalAsync(fn)
|
||||
})
|
||||
})
|
||||
.service('xoApi', function ($cookieStore) {
|
||||
var xo = new xoLib.Xo();
|
||||
.service('xoApi', function (
|
||||
$cookieStore,
|
||||
$rootScope,
|
||||
$timeout
|
||||
) {
|
||||
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()
|
||||
})
|
||||
|
||||
try {
|
||||
let token = $cookieStore.get('token');
|
||||
const token = $cookieStore.get('token')
|
||||
|
||||
// If there is a token, sign in with it.
|
||||
if (token) {
|
||||
xo.signIn({ token });
|
||||
xo.signIn({ token })
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof SyntaxError) {
|
||||
$cookieStore.remove('token');
|
||||
$cookieStore.remove('token')
|
||||
} else {
|
||||
throw e;
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
let getObject;
|
||||
{
|
||||
let {
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
const getObject = (function (objects) {
|
||||
const {
|
||||
all: byIds,
|
||||
indexes: {
|
||||
ref: byRefs,
|
||||
UUID: byUuids
|
||||
ref: byRefs
|
||||
}
|
||||
} = xo.objects;
|
||||
} = objects
|
||||
|
||||
// Look up an object by id, UUID or ref and optionally check its
|
||||
// type.
|
||||
getObject = (id, type) => {
|
||||
let object = byIds[id];
|
||||
|
||||
// If not found by id, try by UUID and ref.
|
||||
if (!object) {
|
||||
object = byUuids[id] || byRefs[id];
|
||||
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
|
||||
// In indexes, the object is wrapped in an array.
|
||||
object = object[0];
|
||||
}
|
||||
return function getObject (id, type) {
|
||||
const object = byIds[id] || byRefs[id]
|
||||
|
||||
if (
|
||||
// No type specifier.
|
||||
!type ||
|
||||
// The object has been found and …
|
||||
object && (
|
||||
// … no type specified.
|
||||
!type ||
|
||||
|
||||
// A single type.
|
||||
(type === object.type) ||
|
||||
// … is of the expected type.
|
||||
(type === object.type) ||
|
||||
|
||||
// An array of possible types.
|
||||
isArray(type) && (indexOf(type, object.type) === -1)
|
||||
// … is of one of the allowed types.
|
||||
isArray(type) && (indexOf(type, object.type) === -1)
|
||||
)
|
||||
) {
|
||||
return object;
|
||||
return object
|
||||
}
|
||||
};
|
||||
}
|
||||
})(xo.objects)
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
// TODO: should probably be merged in the main collection in xo-lib.
|
||||
let currentAcls = 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
|
||||
}
|
||||
})
|
||||
|
||||
$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)
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
//------------------
|
||||
// Session
|
||||
//------------------
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
logIn(email, password, persist) {
|
||||
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
|
||||
// -----------------
|
||||
|
||||
logIn (email, password, persist) {
|
||||
return xo.signIn({ email, password }).then(() => {
|
||||
if (persist) {
|
||||
xo.call('token.create').then(function (token) {
|
||||
$cookieStore.put('token', token);
|
||||
});
|
||||
$cookieStore.put('token', token)
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
logOut() {
|
||||
$cookieStore.remove('token');
|
||||
logOut () {
|
||||
$cookieStore.remove('token')
|
||||
|
||||
return xo.signOut();
|
||||
return xo.signOut()
|
||||
},
|
||||
get status() {
|
||||
return xo.status;
|
||||
get status () {
|
||||
return xo.status
|
||||
},
|
||||
get user() {
|
||||
return xo.user;
|
||||
get user () {
|
||||
return xo.user
|
||||
},
|
||||
|
||||
//------------------
|
||||
// -----------------
|
||||
// RPC
|
||||
//------------------
|
||||
// -----------------
|
||||
|
||||
call(method, params) {
|
||||
return xo.call(method, params);
|
||||
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
|
||||
//------------------
|
||||
|
||||
get(id, types) {
|
||||
if (isArray(id)) {
|
||||
let objects = [];
|
||||
|
||||
angular.forEach(id, id => {
|
||||
let object = getObject(id, types);
|
||||
if (object) {
|
||||
objects.push(object);
|
||||
}
|
||||
});
|
||||
|
||||
return objects;
|
||||
}
|
||||
|
||||
return getObject(id, types);
|
||||
},
|
||||
// -----------------
|
||||
|
||||
// Collection of all objects.
|
||||
all: xo.objects.all,
|
||||
|
||||
byTypes: xo.objects.indexes.type,
|
||||
};
|
||||
// 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,
|
||||
|
||||
// -----------------
|
||||
// Various
|
||||
// -----------------
|
||||
|
||||
// Global stats.
|
||||
stats
|
||||
}
|
||||
})
|
||||
.name
|
||||
;
|
||||
|
||||
2
app/node_modules/xo-directives/index.coffee
generated
vendored
2
app/node_modules/xo-directives/index.coffee
generated
vendored
@@ -48,7 +48,7 @@ module.exports = angular.module 'xoWebApp.directives', []
|
||||
event.stopPropagation()
|
||||
|
||||
# Apply the `xo-click` attribute.
|
||||
$scope.$apply ->
|
||||
$scope.$applyAsync ->
|
||||
fn $scope, {$event: event}
|
||||
true
|
||||
)
|
||||
|
||||
110
app/node_modules/xo-filters/index.coffee
generated
vendored
110
app/node_modules/xo-filters/index.coffee
generated
vendored
@@ -1,110 +0,0 @@
|
||||
angular = require 'angular'
|
||||
|
||||
# TODO: split into multiple modules.
|
||||
module.exports = angular.module 'xoWebApp.filters', [
|
||||
require 'xo-api'
|
||||
]
|
||||
|
||||
# The bytes filters takes a number and formats it using adapted
|
||||
# units (KB, MB, etc.).
|
||||
.filter 'bytesToSize', ->
|
||||
(bytes, unit, base) ->
|
||||
unit ?= 'B'
|
||||
base ?= 1024
|
||||
powers = ['', 'K', 'M', 'G', 'T', 'P']
|
||||
|
||||
i = 0
|
||||
while bytes >= base
|
||||
bytes /= base
|
||||
++i
|
||||
|
||||
if bytes is -1
|
||||
"-"
|
||||
else
|
||||
# Maximum 1 decimals.
|
||||
bytes = ((bytes * 10)|0) / 10
|
||||
"#{bytes}#{powers[i]}B"
|
||||
|
||||
.filter 'sizeToBytes', ->
|
||||
regex = ///^
|
||||
(\d+(?:\.\d+)?) # digits ('.' digits)?
|
||||
\s* # Optional spaces beetween the digits and the unit.
|
||||
([kmgtp])? # Optional unit modifier K/M/G/T/P.
|
||||
b? # Optional unit (“b”), not meaningful.
|
||||
$///i
|
||||
factors = {
|
||||
k: 1024
|
||||
m: 1048576
|
||||
g: 1073741824
|
||||
t: 1099511627776
|
||||
p: 1125899906842624
|
||||
}
|
||||
(size) ->
|
||||
matches = regex.exec size
|
||||
|
||||
# If the input is invalid, just returns null.
|
||||
return null unless matches
|
||||
|
||||
modifier = matches[2]
|
||||
Math.round if modifier and (factor = factors[modifier.toLowerCase()])
|
||||
factor * matches[1]
|
||||
else
|
||||
matches[1]
|
||||
|
||||
# Simply returns the number of elements in the collection.
|
||||
.filter 'count', ->
|
||||
(collection) ->
|
||||
# Array.
|
||||
if angular.isArray collection
|
||||
return collection.length
|
||||
|
||||
# Object.
|
||||
count = 0
|
||||
for key of collection
|
||||
++count if collection.hasOwnProperty key
|
||||
|
||||
count
|
||||
|
||||
# Resolves links between objects.
|
||||
.filter('resolve', (xoApi) -> xoApi.get)
|
||||
|
||||
# Applies a function to a list of items.
|
||||
#
|
||||
# If a string is used instead of a function, it will be used as a
|
||||
# property name to extract from each item.
|
||||
#
|
||||
# Note: This filter behaves nicely if the first argument is not an
|
||||
# array.
|
||||
.filter 'map', ->
|
||||
(items, fn) ->
|
||||
unless angular.isArray items
|
||||
return []
|
||||
|
||||
if angular.isString fn
|
||||
property = fn
|
||||
fn = (item) -> item[property]
|
||||
|
||||
fn item for item in items
|
||||
|
||||
.filter '%', ->
|
||||
(value) ->
|
||||
# If `value` is an array of two values, divide the first by the
|
||||
# second and mutiply by 100.
|
||||
if value.length is 2
|
||||
|
||||
# Special case, if the divider is 0, simply returns "N/A".
|
||||
return 'N/A' if value[1] is 0
|
||||
|
||||
result = 100 * value[0] / value[1]
|
||||
if isNaN result
|
||||
return 'N/A'
|
||||
|
||||
value = result
|
||||
|
||||
# No decimals at most.
|
||||
value = (Math.round value * 1e0) / 1e0
|
||||
|
||||
"#{value}%"
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
174
app/node_modules/xo-filters/index.js
generated
vendored
Normal file
174
app/node_modules/xo-filters/index.js
generated
vendored
Normal file
@@ -0,0 +1,174 @@
|
||||
import angular from 'angular'
|
||||
import forEach from 'lodash.foreach'
|
||||
import isEmpty from 'lodash.isempty'
|
||||
import map from 'lodash.map'
|
||||
import slice from 'lodash.slice'
|
||||
import xoApi from 'xo-api'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default angular.module('xoWebApp.filters', [
|
||||
xoApi
|
||||
])
|
||||
|
||||
// The bytes filters takes a number and formats it using adapted
|
||||
// units (KB, MB, etc.).
|
||||
.filter('bytesToSize', () => {
|
||||
const powers = ['', 'K', 'M', 'G', 'T', 'P']
|
||||
|
||||
return function bytesToSize (bytes, unit = 'B', base = 1024) {
|
||||
let i = 0
|
||||
while (bytes >= base) {
|
||||
bytes /= base
|
||||
++i
|
||||
}
|
||||
|
||||
if (bytes === -1) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
// Maximum 1 decimals.
|
||||
bytes = ((bytes * 10) | 0) / 10
|
||||
|
||||
return `${bytes}${powers[i]}${unit}`
|
||||
}
|
||||
})
|
||||
|
||||
.filter('sizeToBytes', () => {
|
||||
/* eslint no-multi-spaces: 0 */
|
||||
|
||||
const RE = new RegExp('^' +
|
||||
'(\\d+(?:\\.\\d+)?)' + // digits ('.' digits)?
|
||||
'\\s*' + // Optional spaces between the digits and the unit.
|
||||
'([kmgtp])?' + // Optional unit modifier K/M/G/T/P.
|
||||
'b?' + // Optional unit (“b”), not meaningful.
|
||||
'$', 'i')
|
||||
|
||||
const factors = {
|
||||
k: 1024,
|
||||
m: 1048576,
|
||||
g: 1073741824,
|
||||
t: 1099511627776,
|
||||
p: 1125899906842624
|
||||
}
|
||||
|
||||
return function sizeToBytes (size) {
|
||||
const matches = RE.exec(size)
|
||||
|
||||
// If the input is invalid, just returns null.
|
||||
if (!matches) {
|
||||
return null
|
||||
}
|
||||
|
||||
let value = +matches[1]
|
||||
|
||||
const modifier = matches[2]
|
||||
if (modifier) {
|
||||
const factor = factors[modifier.toLowerCase()]
|
||||
if (factor) {
|
||||
value *= factor
|
||||
}
|
||||
}
|
||||
|
||||
return Math.round(value)
|
||||
}
|
||||
})
|
||||
|
||||
// Simply returns the number of elements in the collection.
|
||||
.filter('count', () => {
|
||||
const {hasOwnProperty} = Object.prototype
|
||||
|
||||
return function count (collection) {
|
||||
if (typeof collection !== 'object') {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Array.
|
||||
if (angular.isArray(collection)) {
|
||||
return collection.length
|
||||
}
|
||||
|
||||
// Object.
|
||||
let n = 0
|
||||
for (let key in collection) {
|
||||
if (hasOwnProperty.call(collection, key)) {
|
||||
++n
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
})
|
||||
|
||||
// Resolves links between objects.
|
||||
.filter('resolve', (xoApi) => xoApi.get)
|
||||
|
||||
.filter('isEmpty', () => isEmpty)
|
||||
.filter('isNotEmpty', () => (collection) => !isEmpty(collection))
|
||||
|
||||
.filter('slice', () => slice)
|
||||
|
||||
// Applies a function to a list of items.
|
||||
//
|
||||
// If a string is used instead of a function, it will be used as a
|
||||
// property name to extract from each item.
|
||||
.filter('map', () => map)
|
||||
|
||||
.filter('percentage', () => {
|
||||
return function percentage (value) {
|
||||
// If `value` is an array of two values, divide the first by the
|
||||
// second and multiply by 100.
|
||||
if (value.length === 2) {
|
||||
// Special case, if the divider is 0, simply returns "N/A".
|
||||
if (value[1] === 0) {
|
||||
return 'N/A'
|
||||
}
|
||||
|
||||
const result = 100 * value[0] / value[1]
|
||||
if (isNaN(result)) {
|
||||
return 'N/A'
|
||||
}
|
||||
|
||||
value = result
|
||||
}
|
||||
|
||||
// No decimals at most.
|
||||
value = Math.round(value * 1e0) / 1e0
|
||||
|
||||
return `${value}%`
|
||||
}
|
||||
})
|
||||
|
||||
.filter('osFamily', () => {
|
||||
const osToFamily = (function (osByFamily) {
|
||||
const osToFamily = Object.create(null)
|
||||
|
||||
forEach(osByFamily, (list, family) => {
|
||||
forEach(list, os => {
|
||||
osToFamily[os] = family
|
||||
})
|
||||
})
|
||||
|
||||
return osToFamily
|
||||
})({
|
||||
linux: [
|
||||
'centos',
|
||||
'CoreOS',
|
||||
'debian',
|
||||
'fedora',
|
||||
'gentoo',
|
||||
'oracle',
|
||||
'redhat',
|
||||
'sles',
|
||||
'suse',
|
||||
'ubuntu'
|
||||
],
|
||||
windows: [
|
||||
'windows'
|
||||
]
|
||||
})
|
||||
|
||||
return (osName) => osToFamily[osName] || 'other'
|
||||
})
|
||||
|
||||
// A module exports its name.
|
||||
.name
|
||||
2
app/node_modules/xo-filters/package.json
generated
vendored
2
app/node_modules/xo-filters/package.json
generated
vendored
@@ -2,7 +2,7 @@
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"coffeeify"
|
||||
"babelify"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
40
app/node_modules/xo-notify/index.js
generated
vendored
Normal file
40
app/node_modules/xo-notify/index.js
generated
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
import angular from 'angular'
|
||||
import angularAnimate from 'angular-animate'
|
||||
import angularNotifyToaster from 'angular-notify-toaster'
|
||||
|
||||
const {isString} = angular
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// High level integration of XO API in xo-web.
|
||||
export default angular.module('xo-notify', [
|
||||
angularAnimate,
|
||||
angularNotifyToaster
|
||||
])
|
||||
.service('xoNotify', (toaster) => {
|
||||
function makeNotifier (level) {
|
||||
return function notifier (options) {
|
||||
if (isString(options)) {
|
||||
options = { message: options }
|
||||
} else if (!options.message) {
|
||||
throw new Error('missing message')
|
||||
}
|
||||
|
||||
toaster.pop(
|
||||
level,
|
||||
options.title || 'Xen-Orchestra',
|
||||
options.message
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
warning: makeNotifier('warning'),
|
||||
error: makeNotifier('error'),
|
||||
info: makeNotifier('info')
|
||||
// TODO: It is probably a bad design to have notification for
|
||||
// successful operations.
|
||||
// success: makeNotifier 'success'
|
||||
}
|
||||
})
|
||||
.name
|
||||
8
app/node_modules/xo-notify/package.json
generated
vendored
Normal file
8
app/node_modules/xo-notify/package.json
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"babelify"
|
||||
]
|
||||
}
|
||||
}
|
||||
252
app/node_modules/xo-services/index.coffee
generated
vendored
252
app/node_modules/xo-services/index.coffee
generated
vendored
@@ -1,252 +0,0 @@
|
||||
angular = require 'angular'
|
||||
|
||||
filter = require 'lodash.filter'
|
||||
|
||||
#=====================================================================
|
||||
|
||||
# TODO: split into multiple modules.
|
||||
module.exports = angular.module 'xoWebApp.services', [
|
||||
require 'angular-animate'
|
||||
|
||||
require 'angular-notify-toaster'
|
||||
|
||||
require 'xo-api'
|
||||
]
|
||||
|
||||
.service 'notify', (toaster) ->
|
||||
notifier = (level) ->
|
||||
(options) ->
|
||||
if angular.isString options
|
||||
options = { message: options }
|
||||
else
|
||||
throw new Error 'missing message' unless options.message
|
||||
|
||||
toaster.pop(
|
||||
level
|
||||
options.title ? 'Xen-Orchestra'
|
||||
options.message
|
||||
)
|
||||
|
||||
{
|
||||
warning: notifier 'warning'
|
||||
error: notifier 'error'
|
||||
info: notifier 'info'
|
||||
# TODO: It is probably a bad design to have notification for
|
||||
# successful operations.
|
||||
# success: notifier 'success'
|
||||
}
|
||||
|
||||
.service 'xo', ($timeout, xoApi, notify) ->
|
||||
# FIXME: default mapper should be identity.
|
||||
defaultArgsMapper = (id) -> if id? then {id} else {}
|
||||
|
||||
action = (name, method, options) ->
|
||||
unless method
|
||||
return ->
|
||||
notify.info {
|
||||
title: name
|
||||
message: 'This feature has not been implemented yet.'
|
||||
}
|
||||
|
||||
# TODO: A (broken) promise should be returned for
|
||||
# consistency.
|
||||
|
||||
{argsMapper, notification} = options ? {}
|
||||
argsMapper ?= defaultArgsMapper
|
||||
|
||||
(args...) ->
|
||||
xoApi.call(
|
||||
method
|
||||
argsMapper args...
|
||||
).catch (error) ->
|
||||
unless notification is false
|
||||
code = error?.code
|
||||
message = if code is 2
|
||||
'You don\'t have the permission.'
|
||||
else
|
||||
'The action failed for unknown reason.'
|
||||
|
||||
notify.warning {
|
||||
title: name
|
||||
message
|
||||
}
|
||||
|
||||
console.error error
|
||||
|
||||
# Re-throws the error to make it available in the promise
|
||||
# chain.
|
||||
throw error
|
||||
|
||||
# The interface.
|
||||
xo = {
|
||||
acl:
|
||||
add: action('Adding an ACL entry', 'acl.add', {
|
||||
argsMapper: (subject, object) => {subject, object},
|
||||
})
|
||||
get: action('Getting ACLs', 'acl.get')
|
||||
remove: action('Remove an ACL entry', 'acl.remove', {
|
||||
argsMapper: (subject, object) => {subject, object},
|
||||
})
|
||||
|
||||
pool:
|
||||
disconnect: action 'Disconnect pool'
|
||||
new_sr: action 'New SR' #temp fix before creating SR
|
||||
patch: action 'Upload patch', 'pool.patch', argsMapper: (pool) -> {pool}
|
||||
|
||||
host:
|
||||
attach: action 'Atach host'#, 'host.attach'
|
||||
detach: action 'Detach host', 'host.detach'
|
||||
restart: action 'Restart host', 'host.restart'
|
||||
restartToolStack: action 'Restart tool stack', 'host.restart_agent'
|
||||
start: action 'Start host', 'host.start'
|
||||
enable: action 'Enable host', 'host.enable'
|
||||
stop: action 'Stop host', 'host.stop'
|
||||
disable: action 'Disable host', 'host.disable'
|
||||
new_sr: action 'New SR' #temp fix before creating SR
|
||||
# TODO: attach/set
|
||||
|
||||
log:
|
||||
delete: action 'Delete Log', 'message.delete'
|
||||
|
||||
message:
|
||||
delete: action 'Delete message'
|
||||
|
||||
pbd:
|
||||
delete: action 'Delete PBD'
|
||||
disconnect: action 'Disconnect PBD'
|
||||
|
||||
server:
|
||||
add: action 'Add server', 'server.add', {
|
||||
argsMapper: (params) -> angular.copy(params)
|
||||
}
|
||||
remove: action 'Remove server', 'server.remove', argsMapper: (id) -> {id}
|
||||
getAll: action 'Getting server', 'server.getAll'
|
||||
set: action 'Save server', 'server.set', {
|
||||
argsMapper: (params) -> angular.copy(params)
|
||||
}
|
||||
|
||||
task:
|
||||
cancel: action 'Cancel task', 'task.cancel', argsMapper: (id) -> {id}
|
||||
destroy: action 'Destroy task', 'task.destroy', argsMapper: (id) -> {id}
|
||||
|
||||
user:
|
||||
create: action 'Create user', 'user.create', {
|
||||
argsMapper: (params) -> angular.copy(params)
|
||||
}
|
||||
delete: action 'Delete user', 'user.delete', argsMapper: (id) -> {id}
|
||||
getAll: action 'Getting users', 'user.getAll'
|
||||
set: action 'Save user', 'user.set', {
|
||||
argsMapper: (params) -> angular.copy(params)
|
||||
}
|
||||
|
||||
vm:
|
||||
convert: action 'Convert VM', 'vm.convert', {
|
||||
argsMapper: (id) -> {id}
|
||||
}
|
||||
clone: action 'Copy VM', 'vm.clone', {
|
||||
argsMapper: (id, name, full_copy) -> {id, name, full_copy} #todo : sr ref to choose target SR
|
||||
}
|
||||
createSnapshot: action 'Create VM snapshot', 'vm.snapshot', {
|
||||
argsMapper: (id, name) -> {id, name}
|
||||
}
|
||||
export: action 'Export VM', 'vm.export', {
|
||||
argsMapper: (vm, compress = true) -> {vm, compress}
|
||||
}
|
||||
delete: action 'Delete VM', 'vm.delete', {
|
||||
argsMapper: (id, delete_disks) -> { id, delete_disks }
|
||||
}
|
||||
ejectCd: action 'Eject disc', 'vm.ejectCd'
|
||||
insertCd: action 'Insert disc', 'vm.insertCd', {
|
||||
argsMapper: (id, cd_id, force = false) -> { id, cd_id, force }
|
||||
}
|
||||
import: action 'Import VM', 'vm.import', {
|
||||
argsMapper: (host) -> { host }
|
||||
}
|
||||
migrate: action 'Migrate VM', 'vm.migrate', {
|
||||
argsMapper: (id, host_id) -> { id, host_id }
|
||||
}
|
||||
migratePool: action 'Migrate VM to another pool', 'vm.migrate_pool', {
|
||||
argsMapper: (params) -> angular.copy(params)
|
||||
}
|
||||
restart: action 'Restart VM', 'vm.restart', {
|
||||
argsMapper: (id, force = false) -> { id, force }
|
||||
}
|
||||
start: action 'Start VM', 'vm.start'
|
||||
stop: action 'Stop VM', 'vm.stop', {
|
||||
argsMapper: (id, force = false) -> { id, force }
|
||||
}
|
||||
revert: action 'Revert snapshot', 'vm.revert'
|
||||
suspend: action 'Suspend VM', 'vm.suspend'
|
||||
resume: action 'Resume VM', 'vm.resume', {
|
||||
argsMapper: (id, force = true) -> { id, force }
|
||||
}
|
||||
# TODO: create/set/pause
|
||||
|
||||
vdi:
|
||||
delete: action 'Delete VDI', 'vdi.delete'
|
||||
migrate: action 'Migrate VDI', 'vdi.migrate', {
|
||||
argsMapper: (id, sr_id) -> { id, sr_id }
|
||||
}
|
||||
|
||||
vif:
|
||||
delete: action 'Delete VIF', 'vif.delete'
|
||||
disconnect: action 'Disconnect VIF', 'vif.disconnect'
|
||||
connect: action 'Connect VIF', 'vif.connect'
|
||||
|
||||
vbd:
|
||||
delete: action 'Delete VBD', 'vbd.delete'
|
||||
disconnect: action 'Disconnect VBD', 'vbd.disconnect'
|
||||
connect: action 'Connect VBD', 'vbd.connect'
|
||||
}
|
||||
|
||||
# TODO: should probably be merged in the main collection in xo-lib.
|
||||
currentAcls = Object.create(null)
|
||||
updateCurrentAcls = () ->
|
||||
xoApi.call('acl.getCurrent').then((acls) ->
|
||||
currentAcls = Object.create(null)
|
||||
for acl in acls
|
||||
object = xoApi.get(acl.object)
|
||||
if object
|
||||
currentAcls[object.id] = true
|
||||
|
||||
$timeout(updateCurrentAcls, 1e4)
|
||||
return
|
||||
)
|
||||
updateCurrentAcls()
|
||||
|
||||
# Adds the dynamic properties.
|
||||
Object.defineProperties(xo, {
|
||||
byTypes: { get: ->
|
||||
throw new Error('use xoApi.byTypes instead');
|
||||
},
|
||||
get: { get: ->
|
||||
throw new Error('use xoApi.get() instead');
|
||||
},
|
||||
|
||||
currentAcls: { get: -> currentAcls },
|
||||
})
|
||||
|
||||
xo.canAccess = (id) ->
|
||||
{id} = id if id.id
|
||||
|
||||
return (
|
||||
# Administrators can access everything.
|
||||
xoApi.user and (xoApi.user.permission is 'admin') or
|
||||
|
||||
# Check if the id is in the ACLs table.
|
||||
(id of currentAcls) or
|
||||
|
||||
# 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.
|
||||
(id = xoApi.get(id)?.id) and (id of currentAcls)
|
||||
)
|
||||
|
||||
# Returns the interface.
|
||||
xo
|
||||
|
||||
.filter 'xoHideUnauthorized', (xo) ->
|
||||
{canAccess} = xo
|
||||
return (objects) -> filter objects, xo.canAccess
|
||||
|
||||
# A module exports its name.
|
||||
.name
|
||||
14
app/node_modules/xo-services/index.js
generated
vendored
Normal file
14
app/node_modules/xo-services/index.js
generated
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import angular from 'angular'
|
||||
import xo from 'xo'
|
||||
import xoNotify from 'xo-notify'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// TODO: remove when no longer used.
|
||||
export default angular.module('xoWebApp.services', [
|
||||
xo,
|
||||
xoNotify
|
||||
])
|
||||
// Alias the service for compatibility.
|
||||
.service('notify', (xoNotify) => xoNotify)
|
||||
.name
|
||||
2
app/node_modules/xo-services/package.json
generated
vendored
2
app/node_modules/xo-services/package.json
generated
vendored
@@ -2,7 +2,7 @@
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"coffeeify"
|
||||
"babelify"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
354
app/node_modules/xo/index.js
generated
vendored
Normal file
354
app/node_modules/xo/index.js
generated
vendored
Normal file
@@ -0,0 +1,354 @@
|
||||
import angular from 'angular'
|
||||
import filter from 'lodash.filter'
|
||||
import xoApi from 'xo-api'
|
||||
import xoNotify from 'xo-notify'
|
||||
|
||||
const {isString} = angular
|
||||
|
||||
const isPlainObject = (function (toS) {
|
||||
const ref = toS.call({})
|
||||
return (value) => value != null && toS.call(value) === ref
|
||||
})(Object.prototype.toString)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// High level integration of XO API in xo-web.
|
||||
export default angular.module('xo', [
|
||||
xoApi,
|
||||
xoNotify
|
||||
])
|
||||
.service('xo', function (xoApi, xoNotify) {
|
||||
// FIXME: default mapper should be identity.
|
||||
const defaultArgsMapper = (...args) => {
|
||||
const {length} = args
|
||||
|
||||
// No arguments.
|
||||
if (!length) {
|
||||
return {}
|
||||
}
|
||||
|
||||
if (length !== 1) {
|
||||
throw new Error('no obvious mapping: more than one argument')
|
||||
}
|
||||
|
||||
const arg = args[0]
|
||||
|
||||
// The only argument is an object, it is probably the arguments,
|
||||
// therefore it should be forwarded.
|
||||
if (isPlainObject(arg)) {
|
||||
return arg
|
||||
}
|
||||
|
||||
// The only argument is a string, attempts to match it as `id`.
|
||||
if (isString(arg)) {
|
||||
return { id: arg }
|
||||
}
|
||||
|
||||
throw new Error('no obvious mapping: the only argument is neither an object or a string')
|
||||
}
|
||||
|
||||
const action = (name, method, options) => {
|
||||
if (!method) {
|
||||
return () => {
|
||||
xoNotify.info({
|
||||
title: name,
|
||||
message: 'This feature has not been implemented yet.'
|
||||
})
|
||||
|
||||
// TODO: A (broken) promise should be returned for
|
||||
// consistency.
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
argsMapper = defaultArgsMapper,
|
||||
notification
|
||||
} = options || {}
|
||||
|
||||
return (...args) => {
|
||||
const promise = xoApi.call(method, argsMapper(...args))
|
||||
|
||||
promise.catch(error => {
|
||||
console.error('Error for %s:', method, error)
|
||||
|
||||
if (notification !== false) {
|
||||
const message = (error && error.code === 2) ?
|
||||
'You don\'t have the permission.' :
|
||||
'The action failed for unknown reason.'
|
||||
|
||||
xoNotify.warning({
|
||||
title: name,
|
||||
message
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return promise
|
||||
}
|
||||
}
|
||||
|
||||
/* eslint key-spacing: 0 */
|
||||
|
||||
// The interface.
|
||||
const xo = {
|
||||
acl: {
|
||||
add: action('Adding an ACL entry', 'acl.add', {
|
||||
argsMapper: (subject, object, action) => ({subject, object, action})
|
||||
}),
|
||||
get: action('Getting ACLs', 'acl.get'),
|
||||
remove: action('Remove an ACL entry', 'acl.remove', {
|
||||
argsMapper: (subject, object, action) => ({subject, object, action})
|
||||
})
|
||||
},
|
||||
|
||||
disk: {
|
||||
create: action('Create disk', 'disk.create', {
|
||||
argsMapper: (name, size, sr) => ({name, size, sr})
|
||||
})
|
||||
},
|
||||
|
||||
pool: {
|
||||
disconnect: action('Disconnect pool'),
|
||||
new_sr: action('New SR'), // temp fix before creating SR
|
||||
patch: action('Upload patch', 'pool.patch', {
|
||||
argsMapper: (pool) => ({pool})
|
||||
})
|
||||
},
|
||||
|
||||
host: {
|
||||
attach: action('Attach host'),
|
||||
detach: action('Detach host', 'host.detach'),
|
||||
restart: action('Restart host', 'host.restart'),
|
||||
restartToolStack: action('Restart tool stack', 'host.restart_agent'),
|
||||
start: action('Start host', 'host.start'),
|
||||
enable: action('Enable host', 'host.enable'),
|
||||
stop: action('Stop host', 'host.stop'),
|
||||
disable: action('Disable host', 'host.disable'),
|
||||
new_sr: action('New SR'), // temp fix before creating SR
|
||||
listMissingPatches: action('Check available patches', 'host.listMissingPatches', {
|
||||
argsMapper: (host) => ({host})
|
||||
}),
|
||||
installPatch: action('Install a patch from a patch id', 'host.installPatch', {
|
||||
argsMapper: (host, patch) => ({host, patch})
|
||||
}),
|
||||
refreshStats: action('Get Stats', 'host.stats', {
|
||||
notification: false,
|
||||
argsMapper: (host) => ({host})
|
||||
})
|
||||
// TODO: attach/set
|
||||
},
|
||||
|
||||
log: {
|
||||
delete: action('Delete Log', 'message.delete')
|
||||
},
|
||||
|
||||
message: {
|
||||
delete: action('Delete message')
|
||||
},
|
||||
|
||||
pbd: {
|
||||
delete: action('Delete PBD'),
|
||||
disconnect: action('Disconnect PBD')
|
||||
},
|
||||
|
||||
server: {
|
||||
add: action('Add server', 'server.add'),
|
||||
remove: action('Remove server', 'server.remove'),
|
||||
getAll: action('Getting server', 'server.getAll'),
|
||||
set: action('Save server', 'server.set'),
|
||||
connect: action('Connect to a server', 'server.connect', {
|
||||
notification: false
|
||||
}),
|
||||
disconnect: action('Disconnect from a server', 'server.disconnect')
|
||||
},
|
||||
|
||||
task: {
|
||||
cancel: action('Cancel task', 'task.cancel'),
|
||||
destroy: action('Destroy task', 'task.destroy')
|
||||
},
|
||||
|
||||
user: {
|
||||
create: action('Create user', 'user.create'),
|
||||
delete: action('Delete user', 'user.delete', {
|
||||
argsMapper: (id) => ({ id: String(id) })
|
||||
}),
|
||||
getAll: action('Getting users', 'user.getAll'),
|
||||
set: action('Save user', 'user.set')
|
||||
},
|
||||
|
||||
group: {
|
||||
create: action('Create group', 'group.create'),
|
||||
delete: action('Delete group', 'group.delete', {
|
||||
argsMapper: (id) => ({ id: String(id) })
|
||||
}),
|
||||
getAll: action('Getting groups', 'group.getAll'),
|
||||
set: action('Save group', 'group.set'),
|
||||
setUsers: action('Set group users', 'group.setUsers', {
|
||||
argsMapper: (id, userIds) => ({ id: String(id), userIds})
|
||||
}),
|
||||
addUser: action('Add group user', 'group.addUser', {
|
||||
argsMapper: (id, userId) => ({ id: String(id), userId: String(userId)})
|
||||
}),
|
||||
removeUser: action('Remove group users', 'group.removeUser', {
|
||||
argsMapper: (id, userId) => ({ id: String(id), userId: String(userId)})
|
||||
})
|
||||
},
|
||||
|
||||
role: {
|
||||
getAll: action('Getting roles', 'role.getAll')
|
||||
},
|
||||
|
||||
docker: {
|
||||
start: action('Start a Docker Container', 'docker.start', {
|
||||
argsMapper: (vm, container) => ({vm, container})
|
||||
}),
|
||||
stop: action('Stop a Docker Container', 'docker.stop', {
|
||||
argsMapper: (vm, container) => ({vm, container})
|
||||
}),
|
||||
pause: action('Pause a Docker Container', 'docker.pause', {
|
||||
argsMapper: (vm, container) => ({vm, container})
|
||||
}),
|
||||
unpause: action('Resume a Docker Container', 'docker.unpause', {
|
||||
argsMapper: (vm, container) => ({vm, container})
|
||||
}),
|
||||
restart: action('Restart a Docker Container', 'docker.restart', {
|
||||
argsMapper: (vm, container) => ({vm, container})
|
||||
})
|
||||
},
|
||||
|
||||
vm: {
|
||||
attachDisk: action('Attach disk to VM', 'vm.attachDisk', {
|
||||
argsMapper: (vm, vdi, bootable, mode, position) => ({vm, vdi, bootable, mode, position})
|
||||
}),
|
||||
convert: action('Convert VM', 'vm.convert'),
|
||||
clone: action('Copy VM', 'vm.clone', {
|
||||
// todo : sr ref to choose target SR
|
||||
argsMapper: (id, name, full_copy) => ({id, name, full_copy})
|
||||
}),
|
||||
createInterface: action('Create network interface', 'vm.createInterface', {
|
||||
argsMapper: (vm, network, position, mtu, mac) => ({vm, network, position, mtu, mac})
|
||||
}),
|
||||
createSnapshot: action('Create VM snapshot', 'vm.snapshot', {
|
||||
argsMapper: (id, name) => ({id, name})
|
||||
}),
|
||||
export: action('Export VM', 'vm.export', {
|
||||
argsMapper: (vm, compress = true) => ({vm, compress})
|
||||
}),
|
||||
delete: action('Delete VM', 'vm.delete', {
|
||||
argsMapper: (id, delete_disks) => ({ id, delete_disks })
|
||||
}),
|
||||
ejectCd: action('Eject disc', 'vm.ejectCd'),
|
||||
insertCd: action('Insert disc', 'vm.insertCd', {
|
||||
argsMapper: (id, cd_id, force = false) => ({ id, cd_id, force })
|
||||
}),
|
||||
import: action('Import VM', 'vm.import', {
|
||||
argsMapper: (host) => ({ host })
|
||||
}),
|
||||
migrate: action('Migrate VM', 'vm.migrate', {
|
||||
argsMapper: (id, host_id) => ({ id, host_id })
|
||||
}),
|
||||
migratePool: action('Migrate VM to another pool', 'vm.migrate_pool'),
|
||||
restart: action('Restart VM', 'vm.restart', {
|
||||
argsMapper: (id, force = false) => ({ id, force })
|
||||
}),
|
||||
start: action('Start VM', 'vm.start'),
|
||||
stop: action('Stop VM', 'vm.stop', {
|
||||
argsMapper: (id, force = false) => ({ id, force })
|
||||
}),
|
||||
revert: action('Revert snapshot', 'vm.revert'),
|
||||
suspend: action('Suspend VM', 'vm.suspend'),
|
||||
resume: action('Resume VM', 'vm.resume', {
|
||||
argsMapper: (id, force = true) => ({ id, force })
|
||||
}),
|
||||
refreshStats: action('Get Stats', 'vm.stats', {
|
||||
notification: false
|
||||
}),
|
||||
// TODO: create/set/pause
|
||||
connectPci: action('Connect PCI device', 'vm.attachPci', {
|
||||
argsMapper: (vm, pciId) => ({vm, pciId})
|
||||
}),
|
||||
disconnectPci: action('Disconnect PCI device', 'vm.detachPci', {
|
||||
argsMapper: (vm) => ({vm})
|
||||
})
|
||||
},
|
||||
|
||||
vdi: {
|
||||
delete: action('Delete VDI', 'vdi.delete'),
|
||||
migrate: action('Migrate VDI', 'vdi.migrate', {
|
||||
argsMapper: (id, sr_id) => ({ id, sr_id })
|
||||
})
|
||||
},
|
||||
|
||||
vif: {
|
||||
delete: action('Delete VIF', 'vif.delete'),
|
||||
disconnect: action('Disconnect VIF', 'vif.disconnect'),
|
||||
connect: action('Connect VIF', 'vif.connect')
|
||||
},
|
||||
|
||||
vbd: {
|
||||
delete: action('Delete VBD', 'vbd.delete'),
|
||||
disconnect: action('Disconnect VBD', 'vbd.disconnect'),
|
||||
connect: action('Connect VBD', 'vbd.connect')
|
||||
},
|
||||
|
||||
job: {
|
||||
getAll: action('Get All jobs', 'job.getAll'),
|
||||
create: action('Create a job', 'job.create', {
|
||||
argsMapper: (job) => ({job})
|
||||
}),
|
||||
set: action('Modify a job', 'job.set', {
|
||||
argsMapper: (job) => ({job})
|
||||
}),
|
||||
delete: action('Delete a job', 'job.delete', {
|
||||
argsMapper: (id) => ({id})
|
||||
})
|
||||
},
|
||||
|
||||
schedule: {
|
||||
getAll: action('Get all schedules', 'schedule.getAll'),
|
||||
create: action('Create a schedule', 'schedule.create', {
|
||||
argsMapper: (jobId, cron, enabled) => ({jobId, cron, enabled})
|
||||
}),
|
||||
set: action('Modify a schedule', 'schedule.set', {
|
||||
argsMapper: (id, jobId = undefined, cron = undefined, enabled = undefined) => {
|
||||
const args = {id}
|
||||
jobId !== undefined && (args.jobId = jobId)
|
||||
cron !== undefined && (args.cron = cron)
|
||||
enabled !== undefined && (args.enabled = enabled)
|
||||
return args
|
||||
}
|
||||
}),
|
||||
delete: action('Delete a schedule', 'schedule.delete', {
|
||||
argsMapper: (id) => ({id})
|
||||
})
|
||||
},
|
||||
|
||||
scheduler: {
|
||||
getScheduleTable: action('Get schedule state map', 'scheduler.getScheduleTable'),
|
||||
enable: action('Enable a schedule', 'scheduler.enable', {
|
||||
argsMapper: (id) => ({id})
|
||||
}),
|
||||
disable: action('Disable a schedule', 'scheduler.disable', {
|
||||
argsMapper: (id) => ({id})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Adds the dynamic properties.
|
||||
Object.defineProperties(xo, {
|
||||
get: {
|
||||
get () {
|
||||
throw new Error('use xoApi.get() instead')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Returns the interface.
|
||||
return xo
|
||||
})
|
||||
.filter('xoHideUnauthorized', (xoApi) => {
|
||||
const {canAccess} = xoApi
|
||||
|
||||
return (objects) => filter(objects, canAccess)
|
||||
})
|
||||
.name
|
||||
8
app/node_modules/xo/package.json
generated
vendored
Normal file
8
app/node_modules/xo/package.json
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"private": true,
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"babelify"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
// Font-Awesome 4.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
$fa-font-path: "";
|
||||
$fa-font-path: "../";
|
||||
|
||||
@import "../../node_modules/font-awesome/scss/font-awesome";
|
||||
|
||||
@@ -17,37 +17,29 @@ $fa-font-path: "";
|
||||
@extend .fa;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Angular Charts.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
@import "../../dist/bower_components/angular-chart.js/dist/angular-chart";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Angular xEditable.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// TODO: do not use CSS import but includes
|
||||
//
|
||||
// This syntax is not yet supported for .css files.
|
||||
//
|
||||
// See https://github.com/sass/node-sass/issues/618
|
||||
//@import "../../dist/bower_components/angular-xeditable/dist/css/xeditable";
|
||||
|
||||
@import "/bower_components/angular-xeditable/dist/css/xeditable.css";
|
||||
@import "../../dist/bower_components/angular-xeditable/dist/css/xeditable";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Angular Notify Toaster.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// TODO: do not use CSS import but includes
|
||||
//
|
||||
// This syntax is not yet supported for .css files.
|
||||
//
|
||||
// See https://github.com/sass/node-sass/issues/618
|
||||
//@import "../../dist/bower_components/angular-notify-toaster/toaster";
|
||||
|
||||
@import "/bower_components/angular-notify-toaster/toaster.css";
|
||||
@import "../../dist/bower_components/angular-notify-toaster/toaster";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// UI Select.
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
@import "/bower_components/angular-ui-select/dist/select.css";
|
||||
@import "../../dist/bower_components/angular-ui-select/dist/select";
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -76,6 +68,10 @@ a, [ng-click], [xo-click], [xo-sref] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
html, body, .view-main {
|
||||
height: 100%
|
||||
}
|
||||
|
||||
// Force our content to be under the fixed navbar.
|
||||
.view-main {
|
||||
padding-top: 50px;
|
||||
@@ -117,6 +113,28 @@ a, [ng-click], [xo-click], [xo-sref] {
|
||||
@extend .text-warning;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Drag and drop
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
.xo-dragged td{
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.xo-drop-target td, .xo-drop-target .grid-cell, .xo-drop-target.grid-cell {
|
||||
// @extend .bg-success;
|
||||
}
|
||||
|
||||
.xo-drop-legit td, .xo-drop-legit .grid-cell, .xo-drop-legit.grid-cell {
|
||||
// @extend .bg-warning;
|
||||
}
|
||||
|
||||
.xo-drop-legit.vm-cell, .xo-drop-target.vm-cell {
|
||||
outline-style: dashed;
|
||||
outline-width: medium;
|
||||
outline-color: #aaa;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// XO icons
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
@@ -131,6 +149,12 @@ a, [ng-click], [xo-click], [xo-sref] {
|
||||
@extend .text-muted;
|
||||
}
|
||||
|
||||
.xo-icon-docker {
|
||||
@extend .fa;
|
||||
@extend .fa-ship;
|
||||
@extend .text-info;
|
||||
}
|
||||
|
||||
.xo-icon-warning {
|
||||
@extend .fa;
|
||||
@extend .fa-exclamation-circle;
|
||||
@@ -487,6 +511,8 @@ div.host-cell:hover .substats {
|
||||
.vm-cell td {
|
||||
border-bottom: 1px solid #edece4 !important;
|
||||
border-top: 0px !important;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
@@ -526,9 +552,14 @@ div.host-cell:hover .substats {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.stat-simple {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// General object view
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
.panel {
|
||||
flex: 1;
|
||||
margin: 0.4em;
|
||||
@@ -541,6 +572,24 @@ div.host-cell:hover .substats {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.panel-body-stats {
|
||||
@extend .panel-body;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.chart-stat-preview {
|
||||
max-height: 8em;
|
||||
text-align: center;
|
||||
border-left: 1px solid #ddd;
|
||||
border-right: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.chart-stat-full {
|
||||
max-height: 16em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
text-align: center;
|
||||
font-size: 2em;
|
||||
@@ -553,6 +602,12 @@ div.host-cell:hover .substats {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
// not so big stats
|
||||
.mid-stat {
|
||||
font-size: 2.5em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Flat view
|
||||
@@ -590,3 +645,57 @@ img.navbar-logo {
|
||||
font-variant: small-caps;
|
||||
padding-top: 0.8em;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Settings
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
.settings-menu {
|
||||
background-color: #242628;
|
||||
border-right: 1px solid #eee;
|
||||
width: 212px;
|
||||
}
|
||||
|
||||
.settings-menu li a {
|
||||
font-size: 1em;
|
||||
padding-left: 2em;
|
||||
display: block;
|
||||
color: #f8f8f8;
|
||||
&:hover, &:focus {
|
||||
background-color: #2e3133;
|
||||
}
|
||||
}
|
||||
|
||||
// smaller settings menu on smaller screen
|
||||
@media (max-width: 970px) {
|
||||
.settings-menu {
|
||||
// 2em of padding + 2.29em of fa-fw
|
||||
width: 3.29em;
|
||||
}
|
||||
.settings-menu li a {
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
}
|
||||
.menu-entry {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.settings-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fa-menu {
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// Object view
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
|
||||
// For table with only one line, e.g Docker list table etc
|
||||
.oneliner {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ a.btn.navbar-btn.btn-default.dropdown-toggle.inversed {background-color:#444; bo
|
||||
}
|
||||
|
||||
/* stats name in a grid cell */
|
||||
.stat-name {margin-top: 1em; text-align: center; font-variant: small-caps;}
|
||||
.stat-name {text-align: center; font-variant: small-caps; margin-bottom: -0.3em;}
|
||||
|
||||
.grid-button {
|
||||
margin-left: 1em;
|
||||
|
||||
356
gulpfile.js
356
gulpfile.js
@@ -2,64 +2,76 @@
|
||||
//
|
||||
// https://gist.github.com/julien-f/4af9f3865513efeff6ab
|
||||
|
||||
'use strict';
|
||||
'use strict'
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
var gulp = require('gulp');
|
||||
var gulp = require('gulp')
|
||||
|
||||
// All plugins are loaded (on demand) by gulp-load-plugins.
|
||||
var $ = require('gulp-load-plugins')();
|
||||
var $ = require('gulp-load-plugins')()
|
||||
|
||||
//====================================================================
|
||||
var pipe = require('nice-pipe')
|
||||
|
||||
var DIST_DIR = __dirname +'/dist';
|
||||
var SRC_DIR = __dirname +'/app';
|
||||
// ===================================================================
|
||||
|
||||
var DIST_DIR = __dirname + '/dist'
|
||||
var SRC_DIR = __dirname + '/app'
|
||||
|
||||
// Bower directory is read from its configuration.
|
||||
var BOWER_DIR = (function () {
|
||||
var cfg;
|
||||
var cfg
|
||||
|
||||
try
|
||||
{
|
||||
cfg = JSON.parse(require('fs').readFileSync(__dirname +'/.bowerrc'));
|
||||
}
|
||||
catch (error)
|
||||
{
|
||||
cfg = {};
|
||||
try {
|
||||
cfg = JSON.parse(require('fs').readFileSync(__dirname + '/.bowerrc'))
|
||||
} catch (error) {
|
||||
cfg = {}
|
||||
}
|
||||
|
||||
cfg.cwd || (cfg.cwd = __dirname);
|
||||
cfg.directory || (cfg.directory = 'bower_components');
|
||||
cfg.cwd || (cfg.cwd = __dirname)
|
||||
cfg.directory || (cfg.directory = 'bower_components')
|
||||
|
||||
return cfg.cwd +'/'+ cfg.directory;
|
||||
})();
|
||||
return cfg.cwd + '/' + cfg.directory
|
||||
})()
|
||||
|
||||
var PRODUCTION = process.argv.indexOf('--production') !== -1;
|
||||
var PRODUCTION = process.argv.indexOf('--production') !== -1
|
||||
|
||||
// Port to use for the livereload server.
|
||||
//
|
||||
// It must be available and if possible unique to not conflict with other projects.
|
||||
// http://www.random.org/integers/?num=1&min=1024&max=65535&col=1&base=10&format=plain&rnd=new
|
||||
var LIVERELOAD_PORT = 46417;
|
||||
var LIVERELOAD_PORT = 46417
|
||||
|
||||
// Port to use for the embedded web server.
|
||||
//
|
||||
// Set to 0 to choose a random port at each run.
|
||||
var SERVER_PORT = LIVERELOAD_PORT + 1;
|
||||
var SERVER_PORT = LIVERELOAD_PORT + 1
|
||||
|
||||
// Address the server should bind to.
|
||||
//
|
||||
// - `'localhost'` to make it accessible from this host only
|
||||
// - `null` to make it accessible for the whole network
|
||||
var SERVER_ADDR = 'localhost';
|
||||
var SERVER_ADDR = 'localhost'
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Create a noop duplex stream.
|
||||
var noop = function () {
|
||||
var PassThrough = require('stream').PassThrough
|
||||
|
||||
noop = function () {
|
||||
return new PassThrough({
|
||||
objectMode: true
|
||||
})
|
||||
}
|
||||
|
||||
return noop.apply(this, arguments)
|
||||
}
|
||||
|
||||
// Browserify plugin for gulp.js which uses watchify in development
|
||||
// mode.
|
||||
function browserify(path, opts) {
|
||||
opts || (opts = {});
|
||||
function browserify (path, opts) {
|
||||
opts || (opts = {})
|
||||
|
||||
var bundler = require('browserify')({
|
||||
basedir: SRC_DIR,
|
||||
@@ -71,50 +83,49 @@ function browserify(path, opts) {
|
||||
// Required by Watchify.
|
||||
cache: {},
|
||||
packageCache: {},
|
||||
fullPaths: !PRODUCTION,
|
||||
});
|
||||
fullPaths: !PRODUCTION
|
||||
})
|
||||
|
||||
if (!PRODUCTION) {
|
||||
bundler = require('watchify')(bundler);
|
||||
bundler.plugin('bundle-collapser/plugin');
|
||||
bundler = require('watchify')(bundler)
|
||||
bundler.plugin('bundle-collapser/plugin')
|
||||
}
|
||||
|
||||
// Append the extension if necessary.
|
||||
if (!/\.js$/.test(path)) {
|
||||
path += '.js';
|
||||
path += '.js'
|
||||
}
|
||||
|
||||
// Absolute path.
|
||||
path = require('path').resolve(SRC_DIR, path);
|
||||
path = require('path').resolve(SRC_DIR, path)
|
||||
|
||||
var proxy = $.plumber().pipe(new (require('stream').PassThrough)({
|
||||
objectMode: true,
|
||||
}));
|
||||
var proxy = noop()
|
||||
|
||||
var write;
|
||||
function bundle() {
|
||||
bundler.bundle(function onBundleComplete(err, buf) {
|
||||
var write
|
||||
function bundle () {
|
||||
bundler.bundle(function onBundleComplete (err, buf) {
|
||||
if (err) {
|
||||
proxy.emit('error', err);
|
||||
return;
|
||||
proxy.emit('error', err)
|
||||
return
|
||||
}
|
||||
|
||||
write(new (require('vinyl'))({
|
||||
base: SRC_DIR,
|
||||
path: path,
|
||||
contents: buf,
|
||||
}));
|
||||
});
|
||||
contents: buf
|
||||
}))
|
||||
})
|
||||
}
|
||||
if (PRODUCTION) {
|
||||
write = proxy.end.bind(proxy);
|
||||
write = proxy.end.bind(proxy)
|
||||
} else {
|
||||
write = proxy.write.bind(proxy);
|
||||
bundler.on('update', bundle);
|
||||
proxy = $.plumber().pipe(proxy)
|
||||
write = proxy.write.bind(proxy)
|
||||
bundler.on('update', bundle)
|
||||
}
|
||||
bundle();
|
||||
bundle()
|
||||
|
||||
return proxy;
|
||||
return proxy
|
||||
}
|
||||
|
||||
// Combine multiple streams together and can be handled as a single
|
||||
@@ -122,224 +133,211 @@ function browserify(path, opts) {
|
||||
var combine = function () {
|
||||
// `event-stream` is required only when necessary to maximize
|
||||
// performance.
|
||||
combine = require('event-stream').pipe;
|
||||
return combine.apply(this, arguments);
|
||||
};
|
||||
combine = require('event-stream').pipe
|
||||
return combine.apply(this, arguments)
|
||||
}
|
||||
|
||||
// Merge multiple readable streams into a single one.
|
||||
var merge = function () {
|
||||
// `event-stream` is required only when necessary to maximize
|
||||
// performance.
|
||||
merge = require('event-stream').merge;
|
||||
return merge.apply(this, arguments);
|
||||
};
|
||||
|
||||
// Create a noop duplex stream.
|
||||
var noop = function () {
|
||||
var PassThrough = require('stream').PassThrough;
|
||||
|
||||
noop = function () {
|
||||
return new PassThrough({
|
||||
objectMode: true
|
||||
});
|
||||
};
|
||||
|
||||
return noop.apply(this, arguments);
|
||||
};
|
||||
merge = require('event-stream').merge
|
||||
return merge.apply(this, arguments)
|
||||
}
|
||||
|
||||
// Similar to `gulp.src()` but the pattern is relative to `SRC_DIR`
|
||||
// and files are automatically watched when not in production mode.
|
||||
var src = (function () {
|
||||
var resolvePath = require('path').resolve;
|
||||
function resolve(path) {
|
||||
var resolvePath = require('path').resolve
|
||||
function resolve (path) {
|
||||
if (path) {
|
||||
return resolvePath(SRC_DIR, path);
|
||||
return resolvePath(SRC_DIR, path)
|
||||
}
|
||||
return SRC_DIR;
|
||||
return SRC_DIR
|
||||
}
|
||||
|
||||
if (PRODUCTION)
|
||||
{
|
||||
return function src(pattern, base) {
|
||||
base = resolve(base);
|
||||
if (PRODUCTION) {
|
||||
return function src (pattern, base) {
|
||||
base = resolve(base)
|
||||
|
||||
return gulp.src(pattern, {
|
||||
base: base,
|
||||
cwd: base,
|
||||
});
|
||||
};
|
||||
cwd: base
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// gulp-plumber prevents streams from disconnecting when errors.
|
||||
// See: https://gist.github.com/floatdrop/8269868#file-thoughts-md
|
||||
return function src(pattern, base) {
|
||||
base = resolve(base);
|
||||
return function src (pattern, base) {
|
||||
base = resolve(base)
|
||||
|
||||
return gulp.src(pattern, {
|
||||
base: base,
|
||||
cwd: base,
|
||||
cwd: base
|
||||
})
|
||||
.pipe($.watch(pattern, {
|
||||
base: base,
|
||||
cwd: base,
|
||||
cwd: base
|
||||
}))
|
||||
.pipe($.plumber())
|
||||
;
|
||||
};
|
||||
})();
|
||||
}
|
||||
})()
|
||||
|
||||
// Similar to `gulp.dest()` but the output directory is relative to
|
||||
// `DIST_DIR` and default to `./`, and files are automatically live-
|
||||
// reloaded when not in production mode.
|
||||
var dest = (function () {
|
||||
var resolvePath = require('path').resolve;
|
||||
function resolve(path) {
|
||||
var resolvePath = require('path').resolve
|
||||
function resolve (path) {
|
||||
if (path) {
|
||||
return resolvePath(DIST_DIR, path);
|
||||
return resolvePath(DIST_DIR, path)
|
||||
}
|
||||
return DIST_DIR;
|
||||
return DIST_DIR
|
||||
}
|
||||
|
||||
if (PRODUCTION)
|
||||
{
|
||||
return function dest(path) {
|
||||
return gulp.dest(resolve(path));
|
||||
};
|
||||
if (PRODUCTION) {
|
||||
return function dest (path) {
|
||||
return gulp.dest(resolve(path))
|
||||
}
|
||||
}
|
||||
|
||||
var livereload = function () {
|
||||
$.livereload.listen(LIVERELOAD_PORT);
|
||||
$.livereload.listen(LIVERELOAD_PORT)
|
||||
|
||||
livereload = $.livereload;
|
||||
return livereload();
|
||||
};
|
||||
livereload = $.livereload
|
||||
return livereload()
|
||||
}
|
||||
|
||||
return function dest(path) {
|
||||
return function dest (path) {
|
||||
return combine(
|
||||
gulp.dest(resolve(path)),
|
||||
livereload()
|
||||
);
|
||||
};
|
||||
})();
|
||||
)
|
||||
}
|
||||
})()
|
||||
|
||||
//====================================================================
|
||||
// ===================================================================
|
||||
|
||||
gulp.task('buildPages', function buildPages() {
|
||||
return src('[i]ndex.jade')
|
||||
.pipe($.jade())
|
||||
.pipe(PRODUCTION ? noop() : $.embedlr({ port: LIVERELOAD_PORT }))
|
||||
.pipe(dest())
|
||||
;
|
||||
});
|
||||
gulp.task('buildPages', function buildPages () {
|
||||
return pipe([
|
||||
src('[i]ndex.jade'),
|
||||
$.jade(),
|
||||
!PRODUCTION && $.embedlr({ port: LIVERELOAD_PORT }),
|
||||
dest()
|
||||
])
|
||||
})
|
||||
|
||||
gulp.task('buildScripts', [
|
||||
'installBowerComponents',
|
||||
], function buildScripts() {
|
||||
return browserify('./app', {
|
||||
extensions: '.coffee .jade'.split(' '),
|
||||
})
|
||||
// Annotate the code before minification (for Angular.js)
|
||||
.pipe($.ngAnnotate({
|
||||
add: true,
|
||||
'single_quotes': true,
|
||||
}))
|
||||
.pipe(PRODUCTION ? $.uglify() : noop())
|
||||
.pipe(dest())
|
||||
;
|
||||
});
|
||||
'installBowerComponents'
|
||||
], function buildScripts () {
|
||||
return pipe([
|
||||
browserify('./app.js', {
|
||||
extensions: '.coffee .jade'.split(' ')
|
||||
}),
|
||||
PRODUCTION && $.uglify({ mangle: false }),
|
||||
dest()
|
||||
])
|
||||
})
|
||||
|
||||
gulp.task('buildStyles', [
|
||||
'installBowerComponents',
|
||||
], function buildStyles() {
|
||||
return src('styles/[m]ain.scss')
|
||||
.pipe($.sass())
|
||||
.pipe($.autoprefixer([
|
||||
'installBowerComponents'
|
||||
], function buildStyles () {
|
||||
return pipe([
|
||||
src('styles/[m]ain.scss'),
|
||||
!PRODUCTION && $.sourcemaps.init({
|
||||
loadMaps: true
|
||||
}),
|
||||
$.sass(),
|
||||
$.autoprefixer([
|
||||
'last 1 version',
|
||||
'> 1%',
|
||||
]))
|
||||
.pipe(PRODUCTION ? $.csso() : noop())
|
||||
.pipe(dest())
|
||||
;
|
||||
});
|
||||
'> 1%'
|
||||
]),
|
||||
PRODUCTION && $.minifyCss(),
|
||||
!PRODUCTION && $.sourcemaps.write(),
|
||||
dest()
|
||||
])
|
||||
})
|
||||
|
||||
gulp.task('copyAssets', [
|
||||
'installBowerComponents',
|
||||
], function copyAssets() {
|
||||
var imgStream;
|
||||
'installBowerComponents'
|
||||
], function copyAssets () {
|
||||
var imgStream
|
||||
if (PRODUCTION) {
|
||||
var imgFilter = $.filter('**/*.{gif,jpg,jpeg,png,svg}');
|
||||
var imgFilter = $.filter('**/*.{gif,jpg,jpeg,png,svg}')
|
||||
|
||||
imgStream = combine(
|
||||
imgFilter,
|
||||
$.imagemin({
|
||||
progressive: true,
|
||||
progressive: true
|
||||
}),
|
||||
imgFilter.restore()
|
||||
);
|
||||
)
|
||||
} else {
|
||||
imgStream = noop();
|
||||
imgStream = noop()
|
||||
}
|
||||
|
||||
return merge(
|
||||
src([
|
||||
'[f]avicon.ico',
|
||||
'images/**/*',
|
||||
]).pipe(imgStream),
|
||||
src(
|
||||
'fontawesome-webfont.*',
|
||||
__dirname + '/node_modules/font-awesome/fonts/'
|
||||
)
|
||||
).pipe(dest());
|
||||
});
|
||||
return pipe([
|
||||
merge(
|
||||
src([
|
||||
'[f]avicon.ico',
|
||||
'images/**/*'
|
||||
]).pipe(imgStream),
|
||||
src(
|
||||
'fontawesome-webfont.*',
|
||||
__dirname + '/node_modules/font-awesome/fonts/'
|
||||
)
|
||||
),
|
||||
dest()
|
||||
])
|
||||
})
|
||||
|
||||
gulp.task('installBowerComponents', function installBowerComponents(done) {
|
||||
gulp.task('installBowerComponents', function installBowerComponents (done) {
|
||||
require('bower').commands.install()
|
||||
.on('error', done)
|
||||
.on('end', function () {
|
||||
done();
|
||||
done()
|
||||
})
|
||||
;
|
||||
});
|
||||
})
|
||||
|
||||
//--------------------------------------------------------------------
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
gulp.task('build', [
|
||||
'buildPages',
|
||||
'buildScripts',
|
||||
'buildStyles',
|
||||
'copyAssets',
|
||||
]);
|
||||
'copyAssets'
|
||||
])
|
||||
|
||||
gulp.task('clean', function clean(done) {
|
||||
require('rimraf')(DIST_DIR, done);
|
||||
});
|
||||
gulp.task('clean', function clear (done) {
|
||||
require('rimraf')(DIST_DIR, done)
|
||||
})
|
||||
|
||||
gulp.task('distclean', ['clean'], function distclean(done) {
|
||||
require('rimraf')(BOWER_DIR, done);
|
||||
});
|
||||
gulp.task('distclean', ['clean'], function distclean (done) {
|
||||
require('rimraf')(BOWER_DIR, done)
|
||||
})
|
||||
|
||||
gulp.task('server', function server(done) {
|
||||
gulp.task('server', function server (done) {
|
||||
require('connect')()
|
||||
.use(require('serve-static')(DIST_DIR))
|
||||
.listen(SERVER_PORT, SERVER_ADDR, function serverOnListen() {
|
||||
var address = this.address();
|
||||
.listen(SERVER_PORT, SERVER_ADDR, function serverOnListen () {
|
||||
var address = this.address()
|
||||
|
||||
var port = address.port;
|
||||
address = address.address;
|
||||
var port = address.port
|
||||
address = address.address
|
||||
|
||||
// Correctly handle IPv6 addresses.
|
||||
if (address.indexOf(':') !== -1) {
|
||||
address = '['+ address +']';
|
||||
address = '[' + address + ']'
|
||||
}
|
||||
|
||||
console.log('Listening on http://'+ address +':'+ port);
|
||||
console.log('Listening on http://' + address + ':' + port)
|
||||
})
|
||||
.on('close', function serverOnClose() {
|
||||
done();
|
||||
.on('close', function serverOnClose () {
|
||||
done()
|
||||
})
|
||||
;
|
||||
});
|
||||
})
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
gulp.task('default', ['build']);
|
||||
gulp.task('default', ['build'])
|
||||
|
||||
4
node_modules/angular-ui-event.js
generated
vendored
Normal file
4
node_modules/angular-ui-event.js
generated
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
'use strict';
|
||||
|
||||
require('./angular-ui-utils/modules/event/event');
|
||||
module.exports = 'ui.event';
|
||||
16
node_modules/has.js
generated
vendored
16
node_modules/has.js
generated
vendored
@@ -1,16 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
//====================================================================
|
||||
|
||||
module.exports = (function (hasOwnProperty) {
|
||||
/* jshint eqnull: true */
|
||||
|
||||
return hasOwnProperty ?
|
||||
function has(obj, prop) {
|
||||
return (obj != null) && hasOwnProperty.call(obj, prop);
|
||||
} :
|
||||
function has(obj, prop) {
|
||||
return (obj != null) && obj[prop] !== undefined;
|
||||
}
|
||||
;
|
||||
})(Object.prototype.hasOwnProperty);
|
||||
21
node_modules/isempty.js
generated
vendored
21
node_modules/isempty.js
generated
vendored
@@ -1,21 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
//====================================================================
|
||||
|
||||
var has = require('has');
|
||||
|
||||
//====================================================================
|
||||
|
||||
module.exports = function isEmpty(obj) {
|
||||
if (has(obj, 'length')) {
|
||||
return obj.length === 0;
|
||||
}
|
||||
|
||||
var prop;
|
||||
for (prop in obj) {
|
||||
if (has(obj, prop)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
62
package.json
62
package.json
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "xo-web",
|
||||
"version": "3.8.0",
|
||||
"version": "4.2.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
"xen",
|
||||
@@ -9,17 +10,21 @@
|
||||
"web"
|
||||
],
|
||||
"devDependencies": {
|
||||
"angular": "^1.3.15",
|
||||
"angular-animate": "^1.3.15",
|
||||
"@julien-f/json-rpc": "^0.4.3",
|
||||
"angular": "~1.4.0",
|
||||
"angular-animate": "~1.4.0",
|
||||
"angular-bootstrap": "^0.12.0",
|
||||
"angular-cookies": "^1.3.15",
|
||||
"angular-cookies": "~1.4.0",
|
||||
"angular-ui-router": "^0.2.13",
|
||||
"angular-ui-utils": "^0.1.1",
|
||||
"babelify": "^5.0.4",
|
||||
"ansi_up": "^1.1.3",
|
||||
"babel-eslint": "^3.1.9",
|
||||
"babel-runtime": "^5.5.6",
|
||||
"babelify": "^6.0.2",
|
||||
"bluebird": "^2.9.14",
|
||||
"bootstrap-sass": "^3.3.4",
|
||||
"bower": "^1.3.12",
|
||||
"browserify": "^9.0.3",
|
||||
"browserify": "^10.2.1",
|
||||
"browserify-plain-jade": "^0.2.2",
|
||||
"bundle-collapser": "^1.1.4",
|
||||
"coffeeify": "^1.0.0",
|
||||
@@ -27,31 +32,51 @@
|
||||
"font-awesome": "^4.3.0",
|
||||
"gulp": "^3.8.11",
|
||||
"gulp-autoprefixer": "^2.1.0",
|
||||
"gulp-coffee": "^2.3.1",
|
||||
"gulp-csso": "^1.0.0",
|
||||
"gulp-embedlr": "^0.5.2",
|
||||
"gulp-filter": "^2.0.2",
|
||||
"gulp-imagemin": "^2.2.1",
|
||||
"gulp-jade": "^1.0.0",
|
||||
"gulp-livereload": "^3.8.0",
|
||||
"gulp-load-plugins": "^0.8.1",
|
||||
"gulp-ng-annotate": "^0.5.2",
|
||||
"gulp-load-plugins": "^0.10.0",
|
||||
"gulp-minify-css": "^1.1.1",
|
||||
"gulp-plumber": "^1.0.0",
|
||||
"gulp-sass": "^1.3.3",
|
||||
"gulp-sass": "^2.0.1",
|
||||
"gulp-sourcemaps": "^1.5.2",
|
||||
"gulp-uglify": "^1.1.0",
|
||||
"gulp-watch": "^4.2.0",
|
||||
"in-publish": "^1.1.1",
|
||||
"jquery": "^2.1.3",
|
||||
"lodash.contains": "^2.4.1",
|
||||
"later": "^1.1.6",
|
||||
"lodash.assign": "^3.1.0",
|
||||
"lodash.clonedeep": "^3.0.1",
|
||||
"lodash.difference": "^3.0.1",
|
||||
"lodash.filter": "^3.0.0",
|
||||
"lodash.find": "^3.2.1",
|
||||
"lodash.foreach": "^3.0.3",
|
||||
"lodash.includes": "^3.1.1",
|
||||
"lodash.indexof": "^3.0.2",
|
||||
"lodash.intersection": "^3.1.0",
|
||||
"lodash.isempty": "^3.0.3",
|
||||
"lodash.map": "^3.1.2",
|
||||
"lodash.omit": "^3.1.0",
|
||||
"lodash.remove": "^3.0.0",
|
||||
"lodash.slice": "^3.0.0",
|
||||
"lodash.sortby": "^3.1.0",
|
||||
"lodash.sum": "^3.6.1",
|
||||
"lodash.throttle": "^3.0.1",
|
||||
"make-error": "^1.0.2",
|
||||
"moment": "^2.10.3",
|
||||
"nice-pipe": "^0.2.2",
|
||||
"novnc-node": "^0.5.1",
|
||||
"prettycron": "^0.10.0",
|
||||
"rimraf": "^2.3.2",
|
||||
"vinyl": "^0.4.6",
|
||||
"watchify": "^2.4.0",
|
||||
"xo-lib": "^0.6.3"
|
||||
"socket.io-client": "^1.3.5",
|
||||
"standard": "^4.0.0",
|
||||
"vinyl": "^0.5.0",
|
||||
"watchify": "^3.1.1",
|
||||
"ws": "^0.7.2",
|
||||
"xo-collection": "^0.3.2",
|
||||
"xo-lib": "^0.7.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -67,6 +92,7 @@
|
||||
"scripts": {
|
||||
"build": "gulp distclean && gulp build --production",
|
||||
"dev": "gulp build",
|
||||
"lint": "standard",
|
||||
"prepublish": "in-publish && npm run build || in-install"
|
||||
},
|
||||
"files": [
|
||||
@@ -78,5 +104,11 @@
|
||||
"browserify-plain-jade",
|
||||
"coffeeify"
|
||||
]
|
||||
},
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
"parser": "babel-eslint"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user