Compare commits

...

1043 Commits

Author SHA1 Message Date
Olivier Lambert
5dbeccf92f 5.0.8 2016-07-08 19:29:54 +02:00
Olivier Lambert
56bba1d84b fix(home): typo on host memoryTotal 2016-07-08 16:22:11 +02:00
ABHAMON Ronan
af05d362b4 fix(stats): avoid calculations on null stats objects
Fix #969
2016-07-08 13:53:25 +02:00
ABHAMON Ronan
268ccf9a36 feat(settings/plugins): presets are supported
Fix #1222
2016-07-08 12:50:45 +02:00
ABHAMON Ronan
e77d4fafaa feat(patches): fix all patches related issues
Fixes #1244, #1245 and #1246
2016-07-08 12:30:32 +02:00
Pierre Donias
b88b99e342 fix(new-vm): display fast clone option only if there are template disks
Fix #1172
2016-07-08 12:20:28 +02:00
Olivier Lambert
f862d0df5b fix(host,vm): use stacked value text in a tooltip for stats 2016-07-08 10:40:15 +02:00
Fabrice Marsaud
dac954155c feat(menu): better contrast and size for update icons 2016-07-07 17:23:12 +02:00
Fabrice Marsaud
cf9deceb15 fix(logs): displays job log message if available 2016-07-07 15:22:43 +02:00
Fabrice Marsaud
72aed98088 Filter errors and successes in job logs (#1242)
* Meta data for job log state

* Preset filters for sorted tables

* Work around a babel issue

Fixes #1232
2016-07-07 14:28:09 +02:00
Fabrice Marsaud
ec92eddde8 display log errors properly (#1248)
Fixes #1134
2016-07-07 14:25:14 +02:00
ABHAMON Ronan
e30b5ab6c3 fix(xo-app): use correct sortOrder on sortedTable instances (#1243) 2016-07-07 14:20:54 +02:00
Olivier Lambert
0a5d26b001 5.0.7 2016-07-07 12:14:37 +02:00
ABHAMON Ronan
7e4b881041 feat(srs/uuid): UI improvements (fix #1142) (#1230) 2016-07-07 11:23:37 +02:00
Fabrice Marsaud
27a6af414f fix(host): broken patch filter
Fixed #1236
2016-07-07 11:20:43 +02:00
Olivier Lambert
ba6204f811 feat(user): add Spanish in the language selector 2016-07-06 17:09:16 +02:00
Olivier Lambert
d17b1050ad feat(es): add i18n file for Spanish 2016-07-06 17:05:45 +02:00
Olivier Lambert
b70bc86f71 feat(plugins): compact plugin view. Fixes #1130 2016-07-06 15:14:13 +02:00
Olivier Lambert
42b08633e9 fix(host): correctly display XS version. Fixes #1225 2016-07-06 12:45:20 +02:00
ABHAMON Ronan
bc898e1afd fix(pools): use sorted table & links to srs/hosts (fix #1141) (#1219)
* fix(pools): use sorted table & links to srs/hosts (fix #1141)
2016-07-06 10:45:48 +02:00
Pierre Donias
48d5f34ae6 fix(new-vm): wrong conditions for creation disabling (#1224) 2016-07-06 10:07:07 +02:00
Olivier Lambert
67b8b15cd8 5.0.6 2016-07-05 19:14:36 +02:00
Pierre Donias
09d80afa69 fix(form/SizeInput): controlled & uncontrolled modes (#1220) 2016-07-05 19:06:28 +02:00
ABHAMON Ronan
c0d95304f6 fix(home): actions depend on the selected type (#1218)
Fix #1153
2016-07-05 18:14:37 +02:00
Olivier Lambert
5a0d67a9f6 feat(vm): allow halted VM migration (#1221)
Fixes #1216
2016-07-05 17:55:49 +02:00
Julien Fontanet
08305b4b93 feat(SortedTable): support rowLink prop 2016-07-05 17:04:55 +02:00
Olivier Lambert
04d5612946 chore(xo): more resolveId (#1217) 2016-07-05 14:41:10 +02:00
Olivier Lambert
3dcb6f1f61 fix(host patch): wrong version check for patching hosts 2016-07-05 13:26:59 +02:00
Olivier Lambert
4e7684e38b feat(actions): add confirms for critical actions (#1215)
Fixes #1211
2016-07-05 13:24:04 +02:00
Fabrice Marsaud
a692b7571f feat(logs): sortable (#1214) 2016-07-05 12:30:03 +02:00
ABHAMON Ronan
a098618efa feat(dashboard/overview): add links to related pages (#1212)
Fixes #1139
2016-07-05 11:10:58 +02:00
Pierre Donias
71381e75f1 fix(migration): intra-pool migration should not send maps (#1213) 2016-07-05 11:10:15 +02:00
Pierre Donias
05b345db4a feat(home): bulk VM copy (#1205)
Fixes #1154.
2016-07-05 10:33:30 +02:00
Julien Fontanet
f85f6eab9e 5.0.5 2016-07-04 19:04:36 +02:00
Julien Fontanet
b6dc8b507d fix(SortedTable): total number of items when collection is a map 2016-07-04 19:03:31 +02:00
ABHAMON Ronan
831308ee05 feat(pool/patches): can patches hosts (#1203)
Fixes #1149
2016-07-04 18:52:39 +02:00
Fabrice Marsaud
eb5bcb759f feat(home): can display pools (#1202)
Fixes #1140
2016-07-04 16:28:41 +02:00
ABHAMON Ronan
8286570811 feat(xo-line-charts): supports sum of stats series (#1197)
Fixes #1158
2016-07-04 16:27:26 +02:00
Pierre Donias
10b511f0ed fix(form/SizeInput): props.defaultValue instead of props.value (#1206) 2016-07-04 15:56:57 +02:00
Julien Fontanet
751e335bc0 5.0.4 2016-07-01 17:53:47 +02:00
Pierre Donias
cb107521f2 feat(migration): smart VIFs-networks mapping (#1195) 2016-07-01 17:10:57 +02:00
Olivier Lambert
e56af57b74 fix(card): minor style change 2016-07-01 17:09:10 +02:00
ABHAMON Ronan
a2a1cbab6e feat(sorted-table): display selected items number (#1200)
Fixes #1133
2016-07-01 17:07:57 +02:00
Pierre Donias
306a021a8d fix(new-vm): make radio buttons controlled (#1199)
Fixes #1198
2016-07-01 16:11:19 +02:00
Julien Fontanet
d8c414af2f fix(getEventValue): returns value prop for radios 2016-07-01 15:47:46 +02:00
Julien Fontanet
ec4c76b2e0 feat(selectors): filter() can be chained after pick() 2016-07-01 15:47:46 +02:00
ABHAMON Ronan
e23b8a6891 feat(dashboard/overview): display missing patches (#1191)
Fixes #1148
2016-07-01 15:28:07 +02:00
Olivier Lambert
34006bcbf6 fix(health): forget to check edition 2016-07-01 14:47:43 +02:00
ABHAMON Ronan
ed9aeabf6a chore(dashboard/health): use Card (#1194) 2016-07-01 11:20:32 +02:00
Fabrice Marsaud
799fc5089f feat(settings/remotes): add button to test a remote (#1192)
Fixes #1075.
2016-07-01 09:25:57 +02:00
Julien Fontanet
683d510aa6 5.0.3 2016-06-30 15:12:26 +02:00
Pierre Donias
ebd7e58f61 feat(home): bulk VM migration (#1187)
Fixes #1146
2016-06-30 15:04:21 +02:00
Fabrice Marsaud
9a498b54ac fix(menu): only display one icon for updates when collapsed (#1190)
Fixes #1188
2016-06-30 15:03:26 +02:00
ABHAMON Ronan
2687f45e6e fix(settings/plugins): set config value to undefined if value is null (#1189) 2016-06-30 14:31:43 +02:00
ABHAMON Ronan
f79a17fcec feat(json-schema-input): generate uiSchema JSON schema (#1182) 2016-06-30 13:52:20 +02:00
ABHAMON Ronan
8fd377d1e2 feat(dashboard/dataviz): parallel coordinates graph (#1174)
Fixes #1157
2016-06-30 11:36:29 +02:00
Olivier Lambert
fda06fbd29 feat(VM/network): VIFs management (#1186)
Fixes #1176
2016-06-30 11:28:25 +02:00
Fabrice Marsaud
cee4378e6d feat(xoa-updates): reload after upgrading (#1183)
Fixes #1131.
2016-06-30 11:26:03 +02:00
Fabrice Marsaud
ab6d342886 fix(VM/network): fix broken propTypes import (#1184) 2016-06-29 17:59:39 +02:00
Fabrice Marsaud
9954c08993 fix(xoa-updates): fix env test (#1181) 2016-06-29 12:16:25 +02:00
Julien Fontanet
3ae80aeab3 feat(link): expose Link and BlockLink components 2016-06-29 11:57:42 +02:00
Julien Fontanet
2a3534f659 chore(utils): do not re-export propTypes 2016-06-29 11:57:42 +02:00
Julien Fontanet
fc39de0d5a chore(sign-in): remove because unused 2016-06-29 11:57:41 +02:00
Julien Fontanet
64e4b79d41 chore(utils/createSimpleMatcher): remove because not used 2016-06-29 11:57:41 +02:00
Fabrice Marsaud
53887da3da feat(VM/network): VIF creation (#1173)
Fixes #1138.
2016-06-28 17:47:44 +02:00
ABHAMON Ronan
7c60d68f56 fix(xo-line-chart): set precision on LoadLineChart (#1175)
Fixes #1167
2016-06-28 17:17:46 +02:00
Julien Fontanet
2ac1b991b1 feat(BaseComponent#_linkedState): only allocate when necessary 2016-06-28 15:56:53 +02:00
Julien Fontanet
8257714cdb feat(get-event-value): works with checkbox/radio/select 2016-06-28 15:56:53 +02:00
Julien Fontanet
1b8bacbf5a chore(utils/autobind): remove in favor of ES7 class properties syntax 2016-06-28 15:56:52 +02:00
Julien Fontanet
1d5b84389d chore(utils): do not re-export invoke 2016-06-28 15:56:51 +02:00
Julien Fontanet
f7dcf52977 chore(utils/If): remove because does not work 2016-06-28 15:03:34 +02:00
Julien Fontanet
e26dd5147a feat(BaseComponent#linkState): creates a callback associated to a state entry 2016-06-28 14:56:51 +02:00
Julien Fontanet
bb8f96c2e2 5.0.2 2016-06-28 14:38:13 +02:00
Julien Fontanet
95d4cc9055 chore(README): master → stable 2016-06-28 14:37:14 +02:00
Julien Fontanet
cb84a85f8b chore(package): make package publishable 2016-06-28 14:36:32 +02:00
Pierre Donias
0a8aa2ecf5 feat(user): better UI and password edition (#1165)
Fixes #1127
2016-06-28 14:32:02 +02:00
Julien Fontanet
5941321e84 fix(intl/locales): Spanish is es, not sp 2016-06-28 14:15:11 +02:00
Julien Fontanet
8cf62280f4 feat(intl/locales/sp): initial file 2016-06-28 14:13:21 +02:00
Olivier Lambert
4cea142b57 fix(tasks): improve the task view (#1166)
Fixes #1147
2016-06-28 12:31:25 +02:00
Fabrice Marsaud
64d9245bc4 fix(settings/users): correctly set default permission value (#1170)
Fixes #1159
2016-06-28 11:49:04 +02:00
Fabrice Marsaud
2d78c0c4c3 fix(backup/restore): ignore incorrectly formatted files (#1163)
Fixes #1164
2016-06-28 11:19:54 +02:00
Pierre Donias
aa585e2d25 fix(home): always use advanced migration modal (#1137) 2016-06-27 16:15:52 +02:00
Julien Fontanet
325ab17dcc chore(xo): prefix local function call with _ 2016-06-27 16:04:54 +02:00
Pierre Donias
443ea44bcd fix(new/vm): default custom cloud config (#1125) 2016-06-27 15:21:20 +02:00
Pierre Donias
07958d8efa fix(vms/new): gracefully handle missing objects (#1124) 2016-06-27 15:20:05 +02:00
Olivier Lambert
f19affe599 fix: better SR predicate (#1122) 2016-06-27 15:15:46 +02:00
Julien Fontanet
f7b7c27b6c fix(host/storage): use long clicks for SR name edition 2016-06-25 09:10:22 +02:00
Julien Fontanet
c7af5b384c fix(xo/editSr): use camel case param
Fixes #1116
2016-06-25 09:09:12 +02:00
Olivier Lambert
436a9dfc14 5.0.1 2016-06-25 07:09:43 +02:00
Olivier Lambert
1d6d8ccb28 fix(ACLs): are available from Enterprise (#1117) 2016-06-25 01:09:06 +02:00
Julien Fontanet
7d0862ecfd fix(disclaimer): only from sources (#1119) 2016-06-25 01:08:18 +02:00
Julien Fontanet
7de059919b Merge branch 'v5.x' into next-release 2016-06-24 18:22:54 +02:00
Julien Fontanet
dfd1fb86cb chore(tab-button): inline props 2016-06-24 14:05:15 +02:00
ABHAMON Ronan
847a92433f fix(host/storage): use TabButtonLink instead of Link (#1113) 2016-06-24 14:04:02 +02:00
Pierre Donias
53af4df47b feat(vms/new): fixes and multiple VMs creation (#1098) 2016-06-24 14:02:24 +02:00
Olivier Lambert
09db7c999e fix(he i18n): fix missing space 2016-06-24 14:01:50 +02:00
Denis Kalitviansky
1b4c958aba feat(i18n): Hebrew (#1112) 2016-06-24 13:44:05 +02:00
Fabrice Marsaud
9368d5df01 fix(backup/edit) (#1110) 2016-06-24 13:42:46 +02:00
Olivier Lambert
f3b5026190 fix(ACLs): update views to behave with missing objects (#1111) 2016-06-24 12:53:18 +02:00
ricardovilarinho
19dcd81639 feat(i18n): more Portugese (#1106) 2016-06-24 12:46:37 +02:00
Julien Fontanet
d38c171151 fix(home): add missing key prop 2016-06-24 11:53:32 +02:00
Julien Fontanet
af3049925f fix(SingleLineRow): behave with falsy children 2016-06-24 11:46:39 +02:00
Julien Fontanet
a79825d18c feat(menu): add some entries to non-admins 2016-06-24 11:20:22 +02:00
Olivier Lambert
c4b456b470 fix(home): broken OpenSource modal due to bad intl import 2016-06-24 11:15:09 +02:00
Julien Fontanet
ccdf28767a fix(build): various issues
- missing assets with npm run build
- some files had source maps even though it did not make sense
2016-06-24 11:05:14 +02:00
ABHAMON Ronan
2561f7d793 fix(home/storage): "add a storage" link style & label (#1109) 2016-06-24 10:52:36 +02:00
Fabrice Marsaud
57bd8c1a49 fix(settings/users): permission when creating user (#1108) 2016-06-24 09:58:38 +02:00
Julien Fontanet
8387e4ae04 chore(intl): remove unused messages 2016-06-23 20:03:41 +02:00
Julien Fontanet
5c02935017 fix(user): correctly select current lang 2016-06-23 20:01:56 +02:00
Julien Fontanet
726ffb9b1b feat(store): save lang in cookie 2016-06-23 19:59:18 +02:00
Olivier Lambert
5dcc3f4076 fix(i18n): various mistakes 2016-06-23 19:27:35 +02:00
Fabrice Marsaud
4639d7872f feat(backup/restore): translation (#1105) 2016-06-23 17:52:26 +02:00
Pierre Donias
71cb6af8c4 fix(intl): confirmOK, confirmCancel and alertOk (#1104) 2016-06-23 17:46:28 +02:00
Fabrice Marsaud
52060301bd feat(logs): use SortedTable (#1099) 2016-06-23 17:43:14 +02:00
Olivier Lambert
dfa3e6d8e4 feat(changelog): adding changelog file and changes for 5.0.0 2016-06-23 17:41:42 +02:00
ABHAMON Ronan
f38f3fe5c9 fix(self): message if no resource sets (#1100) 2016-06-23 17:13:47 +02:00
Fabrice Marsaud
24bf031270 feat(menu): add updater status icon (#1103) 2016-06-23 17:11:25 +02:00
ABHAMON Ronan
eeadd72e1f feat(host/storage): add button redirect to new/sr page (#1097) 2016-06-23 16:45:36 +02:00
ricardovilarinho
e4139bab04 Initial work on portuguese translation (#1102) 2016-06-23 16:34:50 +02:00
Fabrice Marsaud
7c7205849b feat(backup/restore): display number of backups per VM (#1101) 2016-06-23 16:24:36 +02:00
ABHAMON Ronan
03b2b13f14 feat(dashboard/stats): add weekly charts (#1093) 2016-06-23 15:53:44 +02:00
Olivier Lambert
8caf9f7fde feat(locales): add i18n PT and HE files 2016-06-23 15:50:19 +02:00
Julien Fontanet
5b8a5ac6b6 fix(create-locale): remove trailing comma 2016-06-23 15:43:34 +02:00
Julien Fontanet
4429bed1cf fix(intl/messages): xenToolsStatusValue description 2016-06-23 15:25:12 +02:00
Julien Fontanet
b9beda3484 fix(intl): export messages 2016-06-23 15:21:31 +02:00
Julien Fontanet
354c9bc927 feat(create-locale): tool to scaffold a new locale 2016-06-23 15:06:28 +02:00
Julien Fontanet
a2d88f7fbf chore(intl): split messages data 2016-06-23 15:05:53 +02:00
Olivier Lambert
83cad000e7 feat(job new): i18n 2016-06-23 12:30:29 +02:00
Fabrice Marsaud
1b78791aa9 feat(groups,users): UI improvements (#1094) 2016-06-23 12:20:17 +02:00
Olivier Lambert
2b05fbf6a0 feat(pool network): i18n 2016-06-23 12:17:59 +02:00
Olivier Lambert
7b677cddaf feat(jobs): xoa plans for jobs feature 2016-06-23 11:03:03 +02:00
Olivier Lambert
18a8fcaa70 feat(newSr,pool,updates): i18n 2016-06-23 10:46:27 +02:00
ABHAMON Ronan
dd1bc757d5 fix(SortedTable): better pagination/filter alignment (#1095) 2016-06-23 10:44:35 +02:00
Olivier Lambert
27ca0fdfcc fix(home): broken link for new VM button 2016-06-22 17:46:30 +02:00
Olivier Lambert
578de05a40 feat(logs): i18n 2016-06-22 17:38:31 +02:00
Olivier Lambert
cd3e1d6bd4 feat(job): i18n 2016-06-22 17:23:37 +02:00
Julien Fontanet
de160bb51b feat(xo): make all calls wait for authentication 2016-06-22 17:12:56 +02:00
ABHAMON Ronan
14417e14c0 fix(vm/snapshots): use ButtonGroup with action buttons (#1096) 2016-06-22 17:02:32 +02:00
Olivier Lambert
f2d8b4e444 feat(dashboard): i18n 2016-06-22 16:55:47 +02:00
Olivier Lambert
b7c41fee28 feat(restore backup): i18n 2016-06-22 16:35:15 +02:00
Olivier Lambert
4f0678d6a2 fix(new backup): forgotten i18n for reset word 2016-06-22 15:41:25 +02:00
Olivier Lambert
880c624935 feat(xoa-upgrade): i18n 2016-06-22 15:22:17 +02:00
Olivier Lambert
0fa0902262 feat(about): i18n 2016-06-22 15:12:07 +02:00
Fabrice Marsaud
a2ab3ccaee feat(jobs/scheduling) (#1032) 2016-06-22 14:51:37 +02:00
Fabrice Marsaud
77a0d1c2ff feat(settings/acls): use SortedTable (#1090) 2016-06-22 14:26:30 +02:00
Olivier Lambert
7fdb022819 feat(backup restore): more explicit restore operation 2016-06-22 12:02:04 +02:00
Fabrice Marsaud
878a630b69 feat(backup/restore): better UI (#1081) 2016-06-22 11:41:50 +02:00
Pierre Donias
fbcfc69983 feat(host,pool): network management (#1084) 2016-06-22 10:01:51 +02:00
Olivier Lambert
a1bd327524 feat(about): hide xo version numbers for non admin users. Fix #877 2016-06-21 17:42:38 +02:00
Olivier Lambert
e62829debd fix(new-vm): using 'state' and not 'this' for SSH cloudConfig 2016-06-21 16:51:00 +02:00
Julien Fontanet
d9d669964f fix(xo/attachDiskToVm): camel casing and params order 2016-06-21 15:36:02 +02:00
Julien Fontanet
ced17b632a fix(editable/Text): do not add all props to the input element 2016-06-21 14:55:16 +02:00
Olivier Lambert
0aada62a5a feat(host,vm): use SortedTable for VM and host logs 2016-06-21 14:01:49 +02:00
Julien Fontanet
2fece7a8fe perf(selectors/createGetObjectMessages): better input dependency 2016-06-21 13:52:07 +02:00
Julien Fontanet
6680373c76 fix(VM/Logs): use correct selector creator 2016-06-21 13:52:07 +02:00
Olivier Lambert
68ae43fd72 feat(vm): add ISO disk drive selector in VM disk view 2016-06-21 13:40:47 +02:00
Julien Fontanet
6b6f452d06 feat(VM/Advanced): can edit number of current CPUs 2016-06-21 12:13:20 +02:00
Greenkeeper
7153ff17e8 chore(package): update modular-css to version 0.22.1 (#1087)
https://greenkeeper.io/
2016-06-21 09:16:12 +02:00
Pierre Donias
0fde5a1b3d fix(new-vm): network instead of $network (#1085) 2016-06-20 17:17:08 +02:00
Julien Fontanet
b17fbdd19b fix(SortedTable): correctly pass a selector to createFilter. 2016-06-20 17:05:23 +02:00
Julien Fontanet
61ae522486 chore(xo): remove test function plop 2016-06-20 17:05:23 +02:00
Olivier Lambert
bd414ae9f2 fix(new-vm): send VDI id for ISO, stringify device for VDI 2016-06-20 15:36:59 +02:00
Olivier Lambert
7579db5876 feat(host): use SortedTable for patches (#1083) 2016-06-20 14:21:20 +02:00
Julien Fontanet
994ce8dab2 fix(log-error): add missing file 2016-06-20 13:12:46 +02:00
Julien Fontanet
e8a84dce7d chore(xo/purgePluginConfiguration): remove unused try/catch. 2016-06-20 13:11:24 +02:00
Julien Fontanet
fdca9eda90 fix(xo): display next connection attempt as warning 2016-06-20 13:11:24 +02:00
Julien Fontanet
e007009a00 feat(xo): logs all call errors 2016-06-20 13:11:24 +02:00
Julien Fontanet
1d5cc209dd feat(log-error): properly display an error in the console 2016-06-20 13:11:24 +02:00
Pierre Donias
09b18e1563 fix(vms/new): Display VDIs instead of SRs in ISO/DVD selector (#1082) 2016-06-20 09:53:00 +02:00
Fabrice Marsaud
363db0edea Create or attach disk and a VM, and change boot order with Drag&Drop (#1067)
* feat(vm): add & attach disks, boot order

* chore(vm): fix issues and use correct selectors predicates for SR/disks
2016-06-17 18:53:06 +02:00
Olivier Lambert
e500240a35 feat(visualizations): add a coming soon message 2016-06-17 17:48:46 +02:00
Pierre Donias
6694977b87 fix(form/Select): set a minimum width (#1080) 2016-06-17 15:23:45 +02:00
Pierre Donias
b173dc1f28 fix(Page): fix height (#1079) 2016-06-17 14:56:05 +02:00
Pierre Donias
aa91f5649f fix(vms/new): text field to set the cloud configuration (#1077) 2016-06-17 11:59:06 +02:00
ABHAMON Ronan
74efd563ab feat(xo-week-heatmap) (#1064) 2016-06-17 11:27:15 +02:00
Pierre Donias
b0b389fb4d fix(dashboard/overview): lint (#1078) 2016-06-17 11:26:32 +02:00
Pierre Donias
e2d9131a07 feat(react-novnc): support clipboard (#1076) 2016-06-16 17:43:23 +02:00
Olivier Lambert
3b1c8216b9 feat(home): more info density on large screens 2016-06-16 17:00:03 +02:00
Julien Fontanet
7f259a43cf fix(home): handle disappearing filter during life 2016-06-16 15:28:32 +02:00
Julien Fontanet
874a504df3 feat(home): can display hosts 2016-06-16 15:09:08 +02:00
Julien Fontanet
6a4c6318e3 fix(form/Toggle): controlled/uncontrolled issues 2016-06-16 11:06:14 +02:00
Julien Fontanet
09bf2b87dc feat(form/Toggle): move into its own module 2016-06-16 11:05:36 +02:00
Olivier Lambert
9c5c9838ae chose(vm,host): use value for toggle 2016-06-16 10:47:15 +02:00
Olivier Lambert
3924033d9a feat(settings): read-only for server connection (#1074) 2016-06-15 18:00:31 +02:00
ABHAMON Ronan
d81e45e456 feat(IsoDevice) (#1071) 2016-06-15 17:42:15 +02:00
Pierre Donias
631a762b56 feat(host/tab-storage): each row of the table is a link to the storage (#1073) 2016-06-15 17:10:35 +02:00
Julien Fontanet
a9cf79942f feat(form/Toggle): can be used as a controlled component 2016-06-15 17:03:06 +02:00
Julien Fontanet
bd31476933 fix(Tooltip): fix import 2016-06-15 17:00:51 +02:00
Julien Fontanet
9b9e4c2ffa fix(select-objects): remove unused import 2016-06-15 16:44:48 +02:00
Julien Fontanet
ec93daac7e feat(Tooltip): accept a className prop 2016-06-15 16:08:03 +02:00
Julien Fontanet
3431b2dfb1 fix(Tooltip): stricter propTypes for children prop 2016-06-15 16:05:45 +02:00
Julien Fontanet
4270abaf1c feat(Tooltip): children prop is optional 2016-06-15 16:05:45 +02:00
Julien Fontanet
0bd288afbd feat(Tooltip): support a tagName prop 2016-06-15 16:05:45 +02:00
Julien Fontanet
b72d5d50a1 feat(select-objects/SelectTag): accept an objects prop 2016-06-15 16:05:45 +02:00
Pierre Donias
d51889c233 feat(utils/BlockLink): Ctrl-click or middle mouse click to open in new tab (#1070) 2016-06-15 14:29:29 +02:00
Olivier Lambert
332b093ee9 feat(copyVM): include Edition check 2016-06-15 13:10:53 +02:00
Pierre Donias
8be332208f feat(xo): copyVm() (#1069) 2016-06-15 11:51:35 +02:00
ABHAMON Ronan
85d1188628 feat(react-novnc): can send Ctrl+Alt+Del (#1068) 2016-06-15 11:28:44 +02:00
Julien Fontanet
56896996c3 feat(selectors/createGetObjectsOfType): type can be a selector 2016-06-15 10:32:21 +02:00
Julien Fontanet
896374e069 feat(selectors/createSortForType): type can be a selector 2016-06-15 10:32:21 +02:00
Julien Fontanet
ac36505fb2 fix(home): remove extranous space in filter 2016-06-15 10:32:21 +02:00
Julien Fontanet
d36df1a8ae chore(home): move VmItem in its own module 2016-06-15 10:32:21 +02:00
ABHAMON Ronan
edd939c069 feat(react-novnc): auto reconnect (#1065) 2016-06-15 10:04:41 +02:00
Olivier Lambert
f80225ba54 feat(updates): current version in updater view 2016-06-14 18:48:39 +02:00
Olivier Lambert
3ccd87b369 fix(self): use formatSize for human readable disk and RAM available 2016-06-14 18:08:32 +02:00
ABHAMON Ronan
59d3dd9255 feat(vm): VM export (#1066) 2016-06-14 17:27:57 +02:00
Pierre Donias
392f08059d feat(vms/new): VM creation page (#1058) 2016-06-14 17:05:10 +02:00
Julien Fontanet
0d5c9a2bba feat(home): connect pools/hosts/tags selects 2016-06-14 16:39:10 +02:00
Julien Fontanet
f27de8015b feat(complex-matcher): -addPropertyClause, +getPropertyClausesStrings, +setPropertyClause 2016-06-14 16:39:10 +02:00
Julien Fontanet
3b7bdee814 fix(complex-matcher/parse): do not fail on empty input 2016-06-14 16:39:10 +02:00
Julien Fontanet
397ed9d581 feat(complex-matcher): $ char is allowed in raw strings 2016-06-14 16:39:10 +02:00
Julien Fontanet
a098880669 fix(selectors/createGetTags): make it work for real :) 2016-06-14 16:39:10 +02:00
Julien Fontanet
047d4cb650 fix(select-objects/SelectTags): fix label 2016-06-14 16:39:10 +02:00
Julien Fontanet
736904c579 feat(select-objects): can be used as controlled inputs 2016-06-14 16:39:10 +02:00
Olivier Lambert
e883c668b5 feat(XOA): VM import / backups (#1063) 2016-06-14 15:29:47 +02:00
Julien Fontanet
14181aa8a7 fix(selectors): fix pool ordering 2016-06-14 11:53:08 +02:00
Olivier Lambert
0b1ba99afa feat(XOA): Free edition(#1062) 2016-06-14 10:44:44 +02:00
Olivier Lambert
88a6215939 feat(health): use SortedTable (#1057) 2016-06-13 12:08:58 +02:00
Greenkeeper
d5ebd33038 chore(package): update globby to version 5.0.0 (#1056)
https://greenkeeper.io/
2016-06-13 09:47:34 +02:00
ABHAMON Ronan
b2ac214c0f feat(form/Select): advanced virtualized select implementation (#1052) 2016-06-13 09:44:38 +02:00
Olivier Lambert
3dee41a511 feat(tasks): better task view. (#1055)
* feat(tasks): better task view
2016-06-10 17:26:30 +02:00
ABHAMON Ronan
934818c07d feat(backup/new): disaster recovery accept an SR (#1054)
Fixes #955
2016-06-10 15:49:21 +02:00
Pierre Donias
4dc614a58e fix(ActionButton): oneOfType expects an array (#1053) 2016-06-10 15:42:02 +02:00
Greenkeeper
35ea095b75 chore(package): update chartist-plugin-legend to version 0.3.1 (#1051)
https://greenkeeper.io/
2016-06-10 12:59:01 +02:00
Olivier Lambert
ae1a4c73b3 fix(issue template): use the word "current" instead of "actual" 2016-06-09 22:21:50 +02:00
ABHAMON Ronan
b58dbe89be feat(vms/import) (#1045) 2016-06-09 17:58:12 +02:00
ABHAMON Ronan
1b4551b622 fix(SortedTable): select first page when changing filter (#1050) 2016-06-09 16:57:30 +02:00
Fabrice Marsaud
72a8f819d3 fix(backup/overview): fix display pending jobs (#1049) 2016-06-09 15:01:27 +02:00
ABHAMON Ronan
df8e16379c fix(select-objects): remove option margin when no containers (#1047) 2016-06-09 14:15:06 +02:00
Julien Fontanet
7dbbc7e25c fix(invoke): fix optim when called without args 2016-06-09 13:36:07 +02:00
Julien Fontanet
1eaae70adb feat(editable/Text): support validation related props 2016-06-09 13:36:07 +02:00
Julien Fontanet
d4a61782c4 chore(menu): menu prefix for icons is optional 2016-06-09 13:35:12 +02:00
Fabrice Marsaud
0e39c6f895 fix(SelectRemote) (#1046) 2016-06-09 12:26:31 +02:00
Fabrice Marsaud
fcc3ede485 fix(settings/remotes): use file type instead of local (#1044) 2016-06-09 10:45:18 +02:00
Fabrice Marsaud
f001e7e713 xo-remote-parser 0.2 2016-06-09 10:28:13 +02:00
Olivier Lambert
3fb8fae821 feat(selectHost): display hostname directly. Continue to group per pools 2016-06-09 10:13:37 +02:00
Fabrice Marsaud
349f3185c5 feat(settings/remotes): edition (#1040) 2016-06-08 17:24:41 +02:00
Olivier Lambert
b457b8409f feat: most of the views now have a header (#1042) 2016-06-08 17:07:43 +02:00
Olivier Lambert
c61e5e1ac8 feat(health): use sorted Table for alarms (#1041) 2016-06-08 15:42:39 +02:00
ABHAMON Ronan
9e60f9d9fd feat(SortedTable): add filter (#1039) 2016-06-08 14:48:34 +02:00
ABHAMON Ronan
a95d40078f feat(SortedTable): pagination can be injected in a container (#1038) 2016-06-08 10:11:18 +02:00
Julien Fontanet
515798bd9f feat(selectors): filter objects by permissions 2016-06-07 16:45:32 +02:00
Julien Fontanet
20b28135a3 chore(store): do not connect to XOA updaters in Sources plan 2016-06-07 15:57:46 +02:00
Julien Fontanet
33d2b8bbeb chore(dev-tools): move into store 2016-06-07 15:57:46 +02:00
Julien Fontanet
7c2059af2b fix(xoa-updater/blockXoaAccess): typo 2016-06-07 15:57:46 +02:00
ABHAMON Ronan
1e0f57bd1a feat(SortedTable): support pagination (#1036) 2016-06-07 14:07:29 +02:00
Olivier Lambert
9484f1dbe6 fix(new sr): wrong URL for srs view 2016-06-07 10:29:06 +02:00
Julien Fontanet
658766c9e4 fix(home): fix variable name 2016-06-07 10:22:25 +02:00
Julien Fontanet
9e47d9acf1 chore(migrate-vm-modal): move into xo 2016-06-06 15:37:04 +02:00
Julien Fontanet
5a1247c021 chore(selectors): getAreObjectsFecthed() → areObjectsFetched() 2016-06-06 15:30:50 +02:00
Julien Fontanet
a8b7972f3c chore: use Container instead of .container-fluid 2016-06-06 14:31:26 +02:00
Fabrice Marsaud
836c2127f7 chore(backup/restore): refactor using render-xo-item (#1023) 2016-06-06 14:28:24 +02:00
Olivier Lambert
6cef200aed feat(menu,about): set content depending on XOA plan (#1033) 2016-06-06 14:21:02 +02:00
Julien Fontanet
c9c80b1d62 fix(xoa-updater): fix import lodash/forEach 2016-06-06 13:45:55 +02:00
Julien Fontanet
04328bc2d1 fix(new/sr): fix import lodash/trim 2016-06-06 13:45:39 +02:00
Julien Fontanet
d909d0eeeb fix(package): update xo-remote-parser to 0.2.1 2016-06-06 13:39:22 +02:00
Julien Fontanet
80f5e913ec chore(users) 2016-06-06 11:28:35 +02:00
Julien Fontanet
57eca31a9c feat(utils/addSubscriptions): decorator to inject subscriptions 2016-06-06 11:28:35 +02:00
Julien Fontanet
c645fc7ad1 feat(utils/connectStore): can handle an object of selectors 2016-06-06 11:28:35 +02:00
Julien Fontanet
78b524b2e8 chore: always user selector to access state 2016-06-06 11:28:35 +02:00
Fabrice Marsaud
1ff1b6931b feat(xoa-updater): initial integration (#952) 2016-06-06 11:23:57 +02:00
Olivier Lambert
29ac883616 feat(xo-app,home): add nice loading icon 2016-06-03 22:22:56 +02:00
Julien Fontanet
467a147603 fix(grid/Row): remove flex which broke columns collapse 2016-06-03 20:36:13 +02:00
Julien Fontanet
7b49b6304c fix(xo-app): fix body scrolling 2016-06-03 20:35:00 +02:00
Olivier Lambert
25991027b9 fix(home): links to import and restore 2016-06-03 19:48:45 +02:00
Olivier Lambert
fce83dfa66 chore(home): text outside links 2016-06-03 19:27:36 +02:00
Olivier Lambert
8603d5d468 feat(home): using card component 2016-06-03 19:15:52 +02:00
Julien Fontanet
bd17f85140 chore(home): do not use btn class on links 2016-06-03 17:56:00 +02:00
Julien Fontanet
037dddb945 fix(messages): fix a typo 2016-06-03 17:52:15 +02:00
Julien Fontanet
dd2151e611 fix(grid/Col): should always have a class 2016-06-03 17:51:56 +02:00
Julien Fontanet
a1e0cdadd6 fix(xo-app): do not set flex on the body container 2016-06-03 17:51:34 +02:00
Julien Fontanet
8d36efa66c chore(Col): do not set the size when full width 2016-06-03 17:35:41 +02:00
Julien Fontanet
b9a12a6dcc feat(Debug): can display promises 2016-06-03 17:24:56 +02:00
Olivier Lambert
08e4fe9990 feat(about): add about info (#1031) 2016-06-03 17:24:07 +02:00
ABHAMON Ronan
2333fec181 feat(SortedTable) (#1030) 2016-06-03 17:22:22 +02:00
Olivier Lambert
05676a78e3 feat(home): handle loading, no servers or no VMs (#1028) 2016-06-03 16:35:55 +02:00
Julien Fontanet
02aaae240c feat(CenterPanel): to use when no data for instance 2016-06-03 11:30:48 +02:00
Julien Fontanet
158924fe3c chore(xo-app): remove unnecessary style 2016-06-03 10:17:36 +02:00
Julien Fontanet
0341b926b9 chore(xo-app/page): move styles to CSS module 2016-06-03 10:12:03 +02:00
Julien Fontanet
69d1f93ea4 feat(xo-app): remove body padding 2016-06-03 10:11:03 +02:00
Julien Fontanet
423fb56ae0 fix(reducers): add missing change 2016-06-02 18:25:48 +02:00
Julien Fontanet
c5fc8d437f fix(selectors/getAreObjectsFetched): previous test was not good enough 2016-06-02 18:24:44 +02:00
Julien Fontanet
0811addf9c fix(selectors/getAreObjectsFetched): use the correct test 2016-06-02 18:18:31 +02:00
Julien Fontanet
e6f8108dc0 feat(selectors/getAreObjectsFetched) 2016-06-02 18:06:20 +02:00
Julien Fontanet
4aa4a8c75d feat(Servers): add spaces in creation form 2016-06-02 18:03:27 +02:00
Julien Fontanet
bbe0467d16 feat(Server): use editable/Password 2016-06-02 18:03:27 +02:00
Julien Fontanet
88ca69138b feat(Server): use ActionRowButton 2016-06-02 18:03:27 +02:00
Julien Fontanet
6a0d9c8805 feat(Users): use ActionButton as submit 2016-06-02 18:01:01 +02:00
Julien Fontanet
1a57f9f134 feat(Users): use ActionRowButton 2016-06-02 18:01:01 +02:00
Julien Fontanet
109aedd3ae feat(ActionButton): redirectOnSuccess can be a function 2016-06-02 18:01:01 +02:00
ABHAMON Ronan
bd9f9344e5 fix(backup jobs): edition (#1026) 2016-06-02 15:23:26 +02:00
ABHAMON Ronan
5190873e99 fix(json-schema-input): correctly handle optional array/object (#1027)
Fixes #1000.
2016-06-02 15:17:51 +02:00
Julien Fontanet
c5fe7eb0dd chore(Groups): remove unused import 2016-06-02 13:51:05 +02:00
Fabrice Marsaud
fef1b14d69 fix a xo fn 2016-06-02 13:48:05 +02:00
Julien Fontanet
472fc02533 feat(Groups): use editable/Text for name 2016-06-02 13:47:39 +02:00
Julien Fontanet
ed29524cf3 fix(xo): resolveIds handles non objects 2016-06-02 13:46:50 +02:00
Julien Fontanet
69f35436c2 feat(form/Password): enableGenerator defaults to false 2016-06-02 13:46:19 +02:00
Fabrice Marsaud
a0ca1cddb5 feat(ACLs) (#1011) 2016-06-02 13:19:35 +02:00
Greenkeeper
be4ffd8308 chore(package): update notifyjs to version 2.0.1 (#1025)
https://greenkeeper.io/
2016-06-02 12:04:35 +02:00
Julien Fontanet
8e246f08ee fix(xo subscriptions): fix running condition 2016-06-02 11:45:24 +02:00
Julien Fontanet
73eda65300 fix(xo subscriptions): wait for previous call to finish 2016-06-02 10:32:50 +02:00
Julien Fontanet
be4df02844 fix(README): fix XOA_PLAN example 2016-06-02 10:27:47 +02:00
Julien Fontanet
7de461319f feat(XOA_PLAN): environment var for different builds 2016-06-02 10:25:58 +02:00
Olivier Lambert
970fc16aab feat(vm): working VDI live migration 2016-06-01 18:35:09 +02:00
Olivier Lambert
5db2c5804d fix(backup): typo for button size 2016-06-01 17:41:05 +02:00
Olivier Lambert
6c2924a08a fix(backup): typo in button style 2016-06-01 17:41:05 +02:00
Pierre Donias
32511fe6a0 feat(editable/XoSelect) (#1020) 2016-06-01 17:22:39 +02:00
Olivier Lambert
94d5b0f083 chore(backup): use the appropriate components 2016-06-01 17:22:25 +02:00
Julien Fontanet
0e957b9566 fix(renderXoItemFromId): handle missing object 2016-06-01 17:01:53 +02:00
Julien Fontanet
ec93f21f0a fix(renderXoItemFromId): fix incorrect var name 2016-06-01 17:01:41 +02:00
Julien Fontanet
bbc4f3beb4 chore(xo): move subscription refreshes in related methods 2016-06-01 16:54:21 +02:00
Julien Fontanet
c271a25a51 feat(selectors/createFilter): if predicate is false, empty collection is returned 2016-06-01 16:35:09 +02:00
Julien Fontanet
c986bf0c46 chore(common): group multiple-files modules in dirs 2016-06-01 16:35:09 +02:00
ABHAMON Ronan
6f994b75e5 feat(backups): deletion & redirect after creation (#1019) 2016-06-01 16:30:40 +02:00
Olivier Lambert
a227039260 feat(vm): allow VDI remove, forget and disconnect 2016-06-01 14:37:00 +02:00
Pierre Donias
ee38c07a3f feat(form/SizeInput): new component for size input (#1017) 2016-06-01 11:15:05 +02:00
ABHAMON Ronan
9678ebd71e chore(select-objects): major refactoring (#1001) 2016-06-01 10:43:38 +02:00
Pierre Donias
82ce0d3461 feat(vm): redirect to home page when VM no longer exists (#1018)
feat(vm): redirect to home page when VM no longer exists
2016-05-31 18:39:03 +02:00
Olivier Lambert
8315c79ef7 feat(host): display the date for license expiry value 2016-05-31 17:25:35 +02:00
Olivier Lambert
69cb6d30b5 feat(VM): VDIs edition (#1015) 2016-05-31 12:27:16 +02:00
Julien Fontanet
f4beef514e fix(xo subscriptions): do not notify if no results yet 2016-05-31 09:16:52 +02:00
Olivier Lambert
f002677134 feat(vm,host): use copiable 2016-05-30 18:48:54 +02:00
Olivier Lambert
6270d2d3af feat(sr,host): add actions on PBDs (#1010) 2016-05-30 18:35:14 +02:00
Julien Fontanet
83625e4ba7 fix(editable/Number): ensure onChange gets a number 2016-05-30 18:21:42 +02:00
Julien Fontanet
d039112b5b feat(xo subscription): notify ASAP when data available 2016-05-30 17:14:24 +02:00
Julien Fontanet
d8481af288 feat(xo subscription): only notify on changes 2016-05-30 17:14:24 +02:00
Julien Fontanet
ea902c1073 fix(xo subscriptions): do not refresh if no subscribers 2016-05-30 17:14:24 +02:00
Julien Fontanet
db62c18a39 fix(xo subscriptions): avoid potential race condition
The subscription could still be active if all the subscriber
unsubscribed at the event reception.
2016-05-30 17:14:24 +02:00
Julien Fontanet
d004e2f759 feat(Copiable) 2016-05-30 17:14:24 +02:00
Julien Fontanet
1f7e457c64 fix(editable/Number): fix size when starting edition 2016-05-30 15:04:52 +02:00
Julien Fontanet
4eae9398d8 fix(editable): fix undo button 2016-05-30 14:58:11 +02:00
Olivier Lambert
4766121570 chore(migrate modal): remove useless message and i18n 2016-05-30 14:57:02 +02:00
Olivier Lambert
3180641e33 feat(sr): add usage and free space 2016-05-30 14:22:53 +02:00
Julien Fontanet
9273002905 ùchore(Tasks): remove unused import 2016-05-30 12:47:59 +02:00
Julien Fontanet
3fc9c5ec90 feat(Tasks): basic tasks list 2016-05-30 12:42:22 +02:00
Julien Fontanet
3266cea1d6 chore(package): update react-router to version 3.0.0-alpha.1 2016-05-30 12:41:52 +02:00
Julien Fontanet
97839c06dc chore(Menu): remove incorrect log 2016-05-30 11:58:32 +02:00
Olivier Lambert
304f290e42 feat(Menu): add tasks link (#1005) 2016-05-30 11:40:35 +02:00
Julien Fontanet
52a241f300 chore(ActionToggle): use btnStyle prop 2016-05-30 10:41:32 +02:00
Julien Fontanet
1c1ea0dcc4 chore(Menu): clean up resize handler code a bit 2016-05-30 10:41:32 +02:00
Julien Fontanet
d998b384e8 chore(ActionToggle): rewritten as stateless component 2016-05-30 10:41:32 +02:00
Olivier Lambert
9184afa6de feat(sr): add various actions (#1002)
* feat(sr,xojs): add SR actions

* naming fix

* typo

* remove SR

* working

* fix

* fix

* noop fixes
2016-05-27 19:07:44 +02:00
Pierre Donias
3e1b4d724f feat(xo/migrateVm): advanced dialog to select host (#993) 2016-05-27 18:21:42 +02:00
Fabrice Marsaud
5b6f50b25b feat(backups): restoration (#996) 2016-05-27 17:56:30 +02:00
Fabrice Marsaud
b757025359 feat(sr addition) (#971) 2016-05-27 17:06:08 +02:00
Olivier Lambert
52e97edbd5 feat(vm,home): add color for OS icons 2016-05-27 15:15:16 +02:00
Olivier Lambert
def88db128 feat(backup overview): i18n 2016-05-27 15:06:58 +02:00
Fabrice Marsaud
d04702e5d4 feat(backups/overview): add logs (#995) 2016-05-27 14:18:23 +02:00
Julien Fontanet
f6407771b5 feat(Icon): accepts className prop 2016-05-27 13:28:58 +02:00
Julien Fontanet
f6a6e125b6 fix(editable/Select): correctly set default value 2016-05-27 13:24:09 +02:00
Julien Fontanet
2303b8a89f feat(loading): center message 2016-05-27 13:16:10 +02:00
Julien Fontanet
93f286b6ac chore(package): remove unused react-router-redux 2016-05-27 13:16:10 +02:00
Julien Fontanet
75e5f931eb chore(store): clarify Xo connection 2016-05-27 13:16:09 +02:00
Olivier Lambert
b215e89572 fix(vm): use Number component instead of Text for vCPU max number edition 2016-05-27 10:18:18 +02:00
Julien Fontanet
07a7e8cf0a feat(selectors/createPager): n can be a selector as well 2016-05-27 10:04:17 +02:00
Olivier Lambert
52000edd7d feat(vm): handle correctly suspended VMs 2016-05-27 09:58:53 +02:00
ABHAMON Ronan
3e4c07c86f feat(self-service): dashboard and management (#992) 2016-05-26 18:20:09 +02:00
Olivier Lambert
92ce69c603 feat(meter): style (#994) 2016-05-26 17:06:01 +02:00
Olivier Lambert
a338e0a3f1 fix(tags): clashing component names tags/xo-tags/label 2016-05-26 13:50:56 +02:00
Fabrice Marsaud
143e09b65f feat(settings): remote management (#975) 2016-05-26 11:46:22 +02:00
Olivier Lambert
e5cc5abdc9 feat(theme) (#978) 2016-05-26 10:31:49 +02:00
Fabrice Marsaud
dfc96ebb99 feat(xo-app): Open Source disclaimer (#972) 2016-05-26 10:24:28 +02:00
Julien Fontanet
9397d0121d perf(select-objects/SelectVm): improve connectStore 2016-05-25 18:33:11 +02:00
Julien Fontanet
d8a1f3c73a feat(home): auto open pools/hosts selects 2016-05-25 13:07:15 +02:00
Julien Fontanet
a07cb425a4 fix(form/Range): uncontrolled therefore value → defaultValue 2016-05-25 13:07:15 +02:00
Julien Fontanet
a89b33dfdf chore(host): _isRunning is a property 2016-05-25 13:07:15 +02:00
Olivier Lambert
486d33448b feat(host): toggle and other improvements in advanced tab 2016-05-25 12:06:33 +02:00
Julien Fontanet
2299d397cb feat(messages): possibility to add props to the FormattedMessage 2016-05-25 11:04:53 +02:00
Julien Fontanet
0173c4709f fix(settings/server): password edition 2016-05-25 11:04:53 +02:00
Olivier Lambert
42fdf8b61f feat(form): boolean toggle (#985) 2016-05-25 11:04:21 +02:00
Greenkeeper
0253723652 chore(package): update ava to version 0.15.0 (#987)
https://greenkeeper.io/
2016-05-25 09:58:59 +02:00
Olivier Lambert
5ca51d3510 feat(vm): edition of number of CPUs (#984) 2016-05-24 18:00:06 +02:00
Julien Fontanet
466dc0127d fix(xo): subscriptions wait for sign in 2016-05-24 17:52:04 +02:00
Julien Fontanet
32f610485c fix(editable): anything can be used as children 2016-05-24 17:42:20 +02:00
Olivier Lambert
429e1b54ee feat(vm): edition in advanced tab (#983) 2016-05-24 16:51:50 +02:00
ABHAMON Ronan
268c037487 feat(select-objects): connected to store and accept an optional predicate (#981) 2016-05-24 15:45:28 +02:00
Julien Fontanet
c146f3105e fix(host/stats): remove unused import 2016-05-24 15:09:21 +02:00
Julien Fontanet
81e0c04722 feat(messages): a render function can be passed 2016-05-24 14:44:57 +02:00
Olivier Lambert
5d156695d2 feat(vm): memory limits (#980) 2016-05-24 12:55:35 +02:00
Julien Fontanet
f71438347c feat(selectors/createGetObjectsOfType): add groupBy() method 2016-05-24 12:41:17 +02:00
Pierre Donias
a3081d607f fix(Editable/Text): use value instead of children since children is not required (#982) 2016-05-24 12:06:38 +02:00
Julien Fontanet
ca81f445b9 fix(selectors/createTags): more complete stub 2016-05-24 11:56:24 +02:00
Olivier Lambert
1c22ce6d76 feat(home): add bold for select sort option (#979) 2016-05-24 11:07:05 +02:00
Pierre Donias
a0d482ba88 feat(Editable): Size component (#966) 2016-05-24 10:13:51 +02:00
Pierre Donias
0c050cc053 feat(SingleLineRow): columns are centered vertically (#973) 2016-05-24 10:12:17 +02:00
Olivier Lambert
9645d624f2 fix(messages): typo on number 2016-05-23 19:14:56 +02:00
Julien Fontanet
f29cb94d9f fix(selectors/createPicker): typo 2016-05-23 17:37:21 +02:00
Julien Fontanet
e239206626 feat(selectors): hide all objects to non admins 2016-05-23 17:25:28 +02:00
Julien Fontanet
35d1065eaf feat(menu): Sign out 2016-05-23 17:25:08 +02:00
Julien Fontanet
8384d6f9d7 fix(messages): vars must be explicitely marked as number 2016-05-23 17:20:25 +02:00
Julien Fontanet
5b3282ba51 perf: major rework of reducers and selectors (#976) 2016-05-23 16:29:23 +02:00
Olivier Lambert
bd1043f034 feat(home): add more bulk actions (#974) 2016-05-23 13:47:12 +02:00
Julien Fontanet
c847dcec15 fix(vm/general): remove test icon 2016-05-21 13:53:15 +02:00
Julien Fontanet
f66994f0b5 fix({host,vm}/console): better display when no stats available 2016-05-21 13:45:01 +02:00
Julien Fontanet
eba27f1823 fix(backup/new): use standard icon on save button 2016-05-21 13:45:01 +02:00
Julien Fontanet
ad1bbb2a00 fix(utils/osFamily): behaves if osName is undefined 2016-05-21 13:45:01 +02:00
Julien Fontanet
42506ab37d chore: rationalize whitespace usage 2016-05-21 13:45:01 +02:00
Julien Fontanet
6bae33826d chore(icons): always use Icon 2016-05-21 13:44:56 +02:00
Julien Fontanet
914c2b89c5 feat(icons): keep using Linux icon for CoreOS 2016-05-21 13:42:47 +02:00
Olivier Lambert
e79926cf29 fix(vm): check if vm.addresses exists 2016-05-21 13:09:40 +02:00
Olivier Lambert
1f15d2c736 fix(vm): i18n shorter button names 2016-05-21 13:03:37 +02:00
Olivier Lambert
fadd27fd23 fix(vm): better display when tools not present (#968) 2016-05-21 13:02:44 +02:00
Julien Fontanet
d5aeb8db55 feat(icons): add CoreOS 2016-05-21 12:02:23 +02:00
Pierre Donias
d5dbdd9986 fix(home): fix false icon attribute in Icon (#967) 2016-05-20 18:19:49 +02:00
Pierre Donias
352c977dc7 feat(home): add tick next to current sort criteria in dropdown (#965) 2016-05-20 16:55:04 +02:00
Julien Fontanet
bf008eba99 style(home): remove double line breaks 2016-05-20 16:29:54 +02:00
Pierre Donias
76b7777fff feat(Menu): collapses when window width is small (#963) 2016-05-20 16:28:12 +02:00
Julien Fontanet
9292d990da fix(home): behaves if filter is null 2016-05-20 15:15:47 +02:00
Julien Fontanet
87fe715823 fix(icons): add generic Linux icon 2016-05-20 12:31:11 +02:00
Julien Fontanet
25e32e0600 chore(icons): remove fixed width 2016-05-20 12:31:10 +02:00
Julien Fontanet
41c901a05c fix(complex-matcher/addPropertyClause): fix when enclause in a and 2016-05-20 11:44:04 +02:00
Julien Fontanet
fdaba2faf4 chore(home): pagination handling more standard 2016-05-20 11:44:04 +02:00
Julien Fontanet
0c73ad4f46 fix(complex-matcher): execute() requires node as context 2016-05-20 09:30:02 +02:00
Julien Fontanet
36c44bc3d4 feat(complex-matcher): addPropertyClause() 2016-05-19 18:13:51 +02:00
Julien Fontanet
d612598bd0 feat(complex-matcher): execute() and toString() expects current node as context 2016-05-19 18:13:51 +02:00
Julien Fontanet
2d75b6086f chore(complex-matcher/parse): refactor 2016-05-19 18:13:51 +02:00
Julien Fontanet
3345674604 chore(complex-matcher): expose node creators 2016-05-19 18:13:51 +02:00
Julien Fontanet
1eeaeeeca5 feat(benchmarks): complex matcher parsing 2016-05-19 18:13:51 +02:00
Olivier Lambert
b0bea8b3ba feat(vm/host): add links to host and pools 2016-05-19 18:10:00 +02:00
Olivier Lambert
0e3e5edd17 fix(host): wrong function name for log deletion 2016-05-19 16:47:57 +02:00
Olivier Lambert
ec1287a2f4 fix(multiple views): missing rows 2016-05-19 16:47:27 +02:00
Pierre Donias
9d2c857c59 feat(Text): placeholder prop (#961) 2016-05-19 14:34:25 +02:00
Pierre Donias
077f4f201c fix(grid): vertically center Col inside Row (#962) 2016-05-19 14:33:59 +02:00
Pierre Donias
9a2154a2ce feat(editable): editable Select (#959) 2016-05-19 12:58:43 +02:00
Olivier Lambert
f4c111c1c2 fix(home): a bit better responsive things 2016-05-18 17:18:53 +02:00
Pierre Donias
9483a06e8a feat(Text): Long click to edit with prop useLongClick (#957) 2016-05-18 13:31:15 +02:00
Olivier Lambert
df2a90dc1d fix(xo): wrong parameter name 2016-05-18 12:44:43 +02:00
Pierre Donias
246c190ccd fix(home): collapse-all button should not be hidden by action buttons. (#956) 2016-05-18 11:25:07 +02:00
Pierre Donias
4640817a14 feat(home): VMs migration (#953) 2016-05-18 10:59:24 +02:00
Pierre Donias
9c7690d39b fix(home): remove useless ref on VmItem (#954) 2016-05-17 17:46:29 +02:00
Pierre Donias
160805af05 feat(home): bulk actions (#948) 2016-05-17 16:16:03 +02:00
ABHAMON Ronan
39e85730f0 feat(plugins): new page to configure plugins (#946) 2016-05-17 15:42:10 +02:00
Olivier Lambert
da692e1a92 feat(home): sort by (#950) 2016-05-17 15:33:03 +02:00
Julien Fontanet
23bc60f1ac feat(selectors/createSort): any input can be a selector 2016-05-17 12:21:32 +02:00
Julien Fontanet
ce0f759509 chore(store/actions/createAction): do not add payload if undefined 2016-05-17 10:53:27 +02:00
Julien Fontanet
1793e5943a chore(store/actions/createAction): remove unused promises handling 2016-05-17 10:52:59 +02:00
Julien Fontanet
201b5db155 chore(BaseComponent): inline env test because it was not prune from the build 2016-05-17 10:12:45 +02:00
Olivier Lambert
c588ac6777 fix(backup): unknown schedule typo in translation 2016-05-16 18:23:35 +02:00
Julien Fontanet
28c01fd4e1 feat(messages): throw an error when a message is undefined 2016-05-16 18:19:27 +02:00
Julien Fontanet
0715e7a31f feat(ISSUE_TEMPLATE): copied from next-release 2016-05-16 16:33:20 +02:00
Julien Fontanet
b497c38e34 feat(ActionButton): prints handler errors 2016-05-16 15:55:55 +02:00
Julien Fontanet
8a08dce405 chore(sr/TabAdvanced): remove unused imports 2016-05-16 15:55:15 +02:00
Julien Fontanet
a620c348bf fix(Menu): always update to avoid issues with router and intl 2016-05-16 15:55:15 +02:00
Julien Fontanet
fe750b7270 chore(*/TabLogs): avoid creating function in render() 2016-05-16 15:55:15 +02:00
Julien Fontanet
4aa9d56dfc fix(home/TabGeneral): use key prop on correct component 2016-05-16 15:55:15 +02:00
Julien Fontanet
4b2ebf2a3a chore: use handlerParam prop everywhere 2016-05-16 15:55:15 +02:00
Julien Fontanet
6290446ea5 chore(dashboard/health): remove unused forEach import 2016-05-16 15:55:15 +02:00
Julien Fontanet
42f5d06960 chore(*/ActionBar): use param prop 2016-05-16 15:55:14 +02:00
Julien Fontanet
85e8006137 chore(dashboard/health): use ActionRowButton handlerParam prop 2016-05-16 15:55:14 +02:00
Julien Fontanet
7c29d4c644 fix(dashboard/health): adapt to modal/confirm changes 2016-05-16 15:55:14 +02:00
Julien Fontanet
7378bc852d feat(xo): deleteSr() 2016-05-16 15:55:14 +02:00
Julien Fontanet
67ed137cfa feat(xo): add confirm to convertVmToTemplate and deleteVm 2016-05-16 15:55:14 +02:00
Julien Fontanet
02e08e54a2 feat(TabButton): forward all props to ActionButton 2016-05-16 15:55:14 +02:00
Julien Fontanet
6b2dd24334 feat(modal/confirm): now expects an object param 2016-05-16 15:55:14 +02:00
Julien Fontanet
131d5becad feat(ActionBar): add param prop to inject as handlerParam 2016-05-16 15:55:14 +02:00
Julien Fontanet
157e0a83b1 feat(ActionButton): accept an handlerParam prop
It makes it easier to use without having to create new functions
everywhere (which can cause perf issues).
2016-05-16 15:55:14 +02:00
Julien Fontanet
d0d3abce3e chore(*/ActionBar): remove incorrect handlers prop 2016-05-16 15:55:14 +02:00
Julien Fontanet
b965c41a45 fix(ActionBar): do not keep recreating style prop 2016-05-16 15:55:14 +02:00
Julien Fontanet
42f824e034 fix(vm): do not show 0 snapshots 2016-05-16 15:55:14 +02:00
Olivier Lambert
2768d9d49d fix(pool patch): incorrect key for translation 2016-05-16 15:52:12 +02:00
Olivier Lambert
c9a86dcae3 feat(host): link to SRs 2016-05-16 15:40:57 +02:00
ABHAMON Ronan
e1d307ea2c feat(home): implement objects selection for filtering (#943) 2016-05-16 12:59:34 +02:00
Olivier Lambert
98ece12ae8 fix(home): number of VMs per page to 20 2016-05-16 10:40:34 +02:00
Olivier Lambert
4cf3db7c2a fix(vm disks): check if VBD has VDIs 2016-05-16 10:37:55 +02:00
Julien Fontanet
7c2f79d980 chore(Vm): move some logic in the tabs 2016-05-16 01:18:54 +02:00
Julien Fontanet
fe064f8b6a feat(utils/@checkPropsState): create an optimized shouldComponentUpdate() 2016-05-16 00:44:15 +02:00
Julien Fontanet
2bad2f6b80 perf(BaseComponent): do not use splat and spread params in constructor 2016-05-16 00:42:20 +02:00
Julien Fontanet
2ba9c5193f chore(Home): use VMS_PER_PAGE constant 2016-05-16 00:16:33 +02:00
Julien Fontanet
331695c10a fix(Tooltip): put tooltip above other components 2016-05-16 00:04:31 +02:00
Julien Fontanet
f299193f05 chore(Menu): cleanup and minor optimization 2016-05-16 00:03:55 +02:00
Julien Fontanet
de8130abc2 feat(selectors/_id): use id prop when no routeParams 2016-05-15 23:55:50 +02:00
Julien Fontanet
1cbde7f2e1 chore(XoApp): inherits from BaseComponent 2016-05-15 23:51:15 +02:00
Julien Fontanet
d1c796d9a7 chore(XoApp): unnecessary to validate children prop 2016-05-15 23:50:33 +02:00
Julien Fontanet
af43061353 chore(XoApp): remove unused connectStore() 2016-05-15 23:50:07 +02:00
Olivier Lambert
1b2ca8e69e 4.16.1 2016-05-14 11:18:16 +02:00
Olivier Lambert
a9e6679b08 fix(vm view): filter perms on all SR. Fix #945 2016-05-14 11:17:43 +02:00
Olivier Lambert
9408760122 chore(host,sr): style fixes 2016-05-14 11:02:26 +02:00
Olivier Lambert
c25e804d61 fix(home): use link for quick actions 2016-05-14 10:34:59 +02:00
Olivier Lambert
b18b2262eb vdi map 2016-05-14 00:12:56 +02:00
Olivier Lambert
570440dc7d host memory bar 2016-05-13 23:12:04 +02:00
Olivier Lambert
aef660fb2f feat(pool): sr view (#944) 2016-05-13 19:50:02 +02:00
Julien Fontanet
17671c7282 feat(selectors/createGetObjects): ignore missing objects 2016-05-13 17:38:38 +02:00
Julien Fontanet
8216ab44b4 feat(selectors): expose createSelector as its usually the name we want 2016-05-13 17:38:34 +02:00
Julien Fontanet
5902d43a94 fix(xo): fix internal createSubscription() 2016-05-13 17:20:12 +02:00
Pierre Donias
a6eb04d3f9 feat(home): VMs pagination (#940) 2016-05-13 17:15:45 +02:00
Julien Fontanet
a71780e860 chore(xo/subscribe): split into independant functions 2016-05-13 17:14:30 +02:00
Julien Fontanet
c2d815ef66 perf(selectors/objects): avoid creating empty objects 2016-05-13 17:11:34 +02:00
Julien Fontanet
bf4679aa9b perf(modal): do not create functions in render 2016-05-13 17:10:16 +02:00
Julien Fontanet
4dd74bbb16 perf(react-novnc): do not create functions in render 2016-05-13 17:10:13 +02:00
Julien Fontanet
10530146ca chore(selectors): remove unused vmContainers 2016-05-13 16:59:30 +02:00
Julien Fontanet
75d49da3d4 fix(home): correctly set filter in input field 2016-05-13 16:43:32 +02:00
Julien Fontanet
af026b0c52 perf(home): let VmItem fetch its container 2016-05-13 16:43:32 +02:00
Julien Fontanet
bcd4f70d0e perf(selectors/createGetObject): simplify 2016-05-13 16:43:32 +02:00
Olivier Lambert
54cc31d1a0 feat(pool): pool view (#933) 2016-05-13 15:26:26 +02:00
ABHAMON Ronan
3f0553861a feat(select-objects): split out XO select logic in low level components (#939) 2016-05-13 14:33:22 +02:00
Julien Fontanet
e1b3c51d2c fix(home): fix expand all button 2016-05-13 14:27:13 +02:00
Julien Fontanet
58a0e3fad6 fix(home): correctly set default filter 2016-05-13 14:26:54 +02:00
Julien Fontanet
6bb235650a chore(home): extract _saveFilter() 2016-05-13 12:07:50 +02:00
Julien Fontanet
0bf0bc4c33 chore(scheduling): remove an incorrect FIXME 2016-05-13 09:43:12 +02:00
Julien Fontanet
3085749e92 chore(Notification): minor optimizations 2016-05-12 21:12:03 +02:00
Julien Fontanet
de3abbf6b8 chore(host): minor optimizations 2016-05-12 21:08:12 +02:00
Julien Fontanet
925469689f chore(scheduling): implement components on top of BaseComponent 2016-05-12 20:50:06 +02:00
Julien Fontanet
631e58a585 chore(form): implement components on top of BaseComponents 2016-05-12 20:42:25 +02:00
Julien Fontanet
63571d06bf chore(editable/Text): implement on top of BaseComponent 2016-05-12 20:38:04 +02:00
Julien Fontanet
1979758fab chore(ActionButton): implement on top of BaseComponent 2016-05-12 20:35:51 +02:00
Julien Fontanet
6ef5a23000 chore(vm): minor optimizations 2016-05-12 20:35:48 +02:00
Julien Fontanet
ae4aa23d27 chore(Tags): clean & optimize 2016-05-12 20:29:46 +02:00
Julien Fontanet
710d1f13cd feat(BaseComponent): React component with reasonable defaults & debugging traces 2016-05-12 20:29:43 +02:00
Julien Fontanet
57a4d366d7 chore(shallow-equal): split out of selectors 2016-05-12 17:52:30 +02:00
Julien Fontanet
8060c66c08 feat(home): put the filter in the URL 2016-05-12 17:23:50 +02:00
Julien Fontanet
d5a58fbec2 perf(home): do not create functions in render 2016-05-12 16:15:53 +02:00
Julien Fontanet
059256de3e chore(home): simplify by treating no VMs case first 2016-05-12 15:50:54 +02:00
Julien Fontanet
2c52d4c867 feat(utils/firstDefined) 2016-05-12 15:50:54 +02:00
Julien Fontanet
d9bfde2e47 chore(selectors): use props.routeParams instead of props.params 2016-05-12 15:50:54 +02:00
ABHAMON Ronan
757acd8d92 fix(backups): remoteId vs remote param name(#938) 2016-05-12 14:52:31 +02:00
Pierre Donias
3e92252e2e fix(BlockLink): correctly behaves with links/buttons/inputs (#934) 2016-05-12 13:21:00 +02:00
ABHAMON Ronan
ce7aeb1a27 chore(Scheduler): use react-intl for month names translation (#935) 2016-05-12 13:13:43 +02:00
ABHAMON Ronan
236d2ad39a feat: backups overview (#932) 2016-05-12 13:04:15 +02:00
Julien Fontanet
63b37714b1 chore(page): use number for plain numeric styles 2016-05-12 11:53:06 +02:00
Julien Fontanet
dc81fd0622 chore(style): remove unused .xo-icon-action-row styles 2016-05-12 11:52:45 +02:00
Julien Fontanet
ccec2bf7ee chore(ActionRow): renamed to ActionRowButton 2nd pass 2016-05-12 11:51:48 +02:00
Julien Fontanet
c93b93331e chore(ActionRow): renamed to ActionRowButton 2016-05-12 11:31:43 +02:00
Julien Fontanet
1f194f1680 chore(ActionRow): reimplemented on top of ActionButton 2016-05-12 11:29:54 +02:00
Julien Fontanet
2251123c1d perf(invoke): minor optim when no param 2016-05-12 11:17:23 +02:00
Julien Fontanet
1bfe1c3370 chore(complex-matcher/parse): rename pattern to input 2016-05-12 11:17:23 +02:00
Olivier Lambert
0044eeb6d1 chore(health): use components and remove useless arrays 2016-05-12 09:52:55 +02:00
Olivier Lambert
669302d46b regain focus on the search field 2016-05-11 18:54:25 +02:00
Olivier Lambert
fce2f44197 feat(home): pre-existing filters (#931) 2016-05-11 18:06:45 +02:00
Pierre Donias
64db1df248 feat(user): new user page (#930) 2016-05-11 17:14:49 +02:00
Olivier Lambert
9109d55019 new VM button working on home view 2016-05-11 16:31:09 +02:00
Olivier Lambert
412e13ccd5 better header content style 2016-05-11 16:25:08 +02:00
Olivier Lambert
0c7e0528b6 better patches refresh when installing a patch 2016-05-11 15:49:13 +02:00
Pierre Donias
ccb22a2f40 chore(Page): remove unnecessary component Header (#928) 2016-05-11 15:48:40 +02:00
Pierre Donias
2f3e463aca feat: move tabs in header for host and VM views (#927)
Fixes #926
2016-05-11 15:13:12 +02:00
Olivier Lambert
c548e08aea react component usage for action buttons in rows 2016-05-11 15:02:19 +02:00
ABHAMON Ronan
150e0171f0 feat: initial VM backups view (#924) 2016-05-11 14:27:58 +02:00
Olivier Lambert
bfcaca7bc0 tab button for VM view 2016-05-11 12:17:30 +02:00
Olivier Lambert
fcb0482193 use TabButton component in host view 2016-05-11 12:02:50 +02:00
Olivier Lambert
714ea7c236 a bit better home responsive view 2016-05-11 11:29:15 +02:00
Pierre Donias
cc30799f0d feat: browser notifications (#921)
* Browser notifications.

* Browser notifications: XO logo in notification.

* Browser notifications: multiple enhancements.
2016-05-11 11:13:08 +02:00
Pierre Donias
df71259a10 style: removed incorrect JSX whitespaces (#925) 2016-05-11 10:36:55 +02:00
Olivier Lambert
9a0ae5d4b9 quick buttons for home view 2016-05-10 16:26:30 +02:00
Olivier Lambert
eea4648ada add docker icon for VM with docker XS plugin installed 2016-05-10 14:27:11 +02:00
Olivier Lambert
2883398c2a add modal for VM convert and delete 2016-05-10 14:17:47 +02:00
Olivier Lambert
84f6e14b89 display the number of snapshot in the expanded home VM view 2016-05-10 13:53:29 +02:00
Pierre Donias
30fb9ed65a feat(modal): alert() and confirm() methods (#918) 2016-05-10 12:00:48 +02:00
Olivier Lambert
a809f2d1f2 re add install all patches in host patch tab 2016-05-10 10:53:35 +02:00
Olivier Lambert
6b5a19983d missing translation in server view 2016-05-09 19:41:36 +02:00
Olivier Lambert
2471f447b3 add current status info for VMs 2016-05-09 17:18:44 +02:00
Julien Fontanet
413e944d7a fix(home): fix imports 2016-05-09 13:46:20 +02:00
Julien Fontanet
3b952819d6 feat(home): remember the last used filter 2016-05-09 13:41:06 +02:00
Greenkeeper
2445c10c1c chore(package): update modular-css to version 0.21.0 (#920)
https://greenkeeper.io/
2016-05-09 11:46:00 +02:00
Julien Fontanet
7e26593d04 feat(complex-matcher): quoted strings 2016-05-08 17:53:34 +02:00
Greenkeeper
73595c683b chore(package): update modular-css to version 0.20.0 (#919)
https://greenkeeper.io/
2016-05-08 10:02:02 +02:00
Julien Fontanet
570f56a4cc chore(complex-matcher): test parse() and toString() 2016-05-08 00:25:16 +02:00
Julien Fontanet
f9e940871e feat(home): add group/or syntax 2016-05-08 00:24:32 +02:00
Julien Fontanet
c2b724a54a chore(test): add AVA test runner 2016-05-08 00:21:58 +02:00
Olivier Lambert
f52db472ed clear search button 2016-05-07 17:52:17 +02:00
Julien Fontanet
324fe98a5b chore(complex-matcher): minor parser simplification 2016-05-07 17:32:25 +02:00
Julien Fontanet
2fd9833580 chore(complex-matcher): move into its own module 2016-05-07 17:25:44 +02:00
Olivier Lambert
0ab4827d6f default search value with a space and search autofocus 2016-05-07 17:19:42 +02:00
Julien Fontanet
f0bd7d7eee perf(home): debounce filter by 250ms 2016-05-07 17:02:55 +02:00
Olivier Lambert
8cd1209602 display OS icon if possible, even when we do not detect xen tools 2016-05-07 16:57:04 +02:00
Julien Fontanet
e2781adc81 feat(home): allow search on nested properties 2016-05-07 16:49:45 +02:00
Julien Fontanet
b91ac2fe89 feat(home): initial complex filter 2016-05-07 16:16:22 +02:00
Julien Fontanet
683a7a1851 perf(selectors/tags): sort the tags and wraps the selector 2016-05-07 13:11:28 +02:00
Olivier Lambert
8b05aa7b59 remove useless array 2016-05-07 13:00:41 +02:00
Olivier Lambert
883f839bfd action for servers 2016-05-07 12:55:22 +02:00
Julien Fontanet
90dc00ac6b perf: throttle object updates by 500ms 2016-05-07 12:51:41 +02:00
Julien Fontanet
9c1cecbb7d chore(dev-tools): disable for now
It's not used at the moment and it has an major perf impact.
2016-05-07 12:05:19 +02:00
Julien Fontanet
1fce11bfba chore(store): do not add useless enhancers 2016-05-07 12:05:19 +02:00
Olivier Lambert
fba3ebdf49 basic filtering in home view 2016-05-07 11:34:05 +02:00
Olivier Lambert
e9585e08a4 only count pending tasks 2016-05-07 00:50:37 +02:00
Olivier Lambert
24a89985fb remove useless icon in vm general tab 2016-05-06 22:20:18 +02:00
Olivier Lambert
33026e8281 more logical order display for the home view 2016-05-06 21:23:54 +02:00
Olivier Lambert
50d9b832a9 better tag component 2016-05-06 21:14:54 +02:00
Olivier Lambert
1df82c3380 do not display a filter if there isn't any object associated with it (ACLs or nothing to display) 2016-05-06 18:59:57 +02:00
Olivier Lambert
0a8db4ebbf object number for filters 2016-05-06 18:39:44 +02:00
Julien Fontanet
928b19aef4 chore(TabButton): move into its own module 2016-05-06 17:48:53 +02:00
Julien Fontanet
a7ec98cef6 chore(icons): move into its own stylesheet 2016-05-06 17:46:13 +02:00
Julien Fontanet
67326a1859 chore(button): remove unused module 2016-05-06 17:33:30 +02:00
Julien Fontanet
631a8a5edf fix(ActionButton): fixed width icon 2016-05-06 17:32:40 +02:00
Julien Fontanet
7e5e463ef2 feat(ActionButton): better feedback on async actions 2016-05-06 17:20:53 +02:00
Julien Fontanet
add65e41da chore(actions): move business code in xo 2016-05-06 17:20:48 +02:00
Julien Fontanet
351b4571cd chore(store): move into common 2016-05-06 17:18:56 +02:00
Julien Fontanet
793258a91f chore(dev-tools): move into common 2016-05-06 17:18:56 +02:00
Julien Fontanet
ed3e1933c3 fix(xo): names of VM clones and snapshots 2016-05-06 17:18:56 +02:00
Olivier Lambert
2f63b26458 use react boostrap button component 2016-05-06 16:56:52 +02:00
Olivier Lambert
f1d14da3dd UI improvements on home view 2016-05-06 16:03:49 +02:00
Pierre Donias
4a3d90bdf3 feat(notification): new module (#914)
* Notifications

* Notification: global notification fired by `notify(...)`

* Notification: Bootstrap colors.

* Notification: Simplified version. Usage example.
2016-05-06 14:27:59 +02:00
Olivier Lambert
f8f24fbc37 feat(home view): major rework 2016-05-06 14:26:30 +02:00
Pierre Donias
8f6c53e111 feat(Modal): new component (#916) 2016-05-06 14:04:38 +02:00
Pierre Donias
c23f55b1d4 feat(Wizard): new component (#896) 2016-05-06 13:41:05 +02:00
Olivier Lambert
88f94f5d6f improved XO title in menu collapse 2016-05-04 21:07:58 +02:00
Olivier Lambert
3e99a179b7 add tool tip for action bar 2016-05-04 11:56:13 +02:00
Olivier Lambert
c7271f94a5 remove xo call for vdi and sr set 2016-05-04 10:46:43 +02:00
Olivier Lambert
5c0ced942c fix tags 2016-05-03 18:41:55 +02:00
Julien Fontanet
d9b07e76f9 chore(package): update standard to version 7.0.0 2016-05-03 18:31:15 +02:00
Julien Fontanet
c75793df20 chore(package): use commit-msg hook instead of commit-msg
It avoids running the tests when there is nothing in the stage.
2016-05-03 18:28:08 +02:00
Julien Fontanet
1bee5121f0 chore(package): remove unused Babel config 2016-05-03 18:28:08 +02:00
Julien Fontanet
932e7eb374 chore(README): remove dependencies status 2016-05-03 18:28:08 +02:00
Julien Fontanet
abdbcfe42b chore(README): add Travis status in title 2016-05-03 18:28:08 +02:00
Julien Fontanet
a62e888732 chore(gitignore): remove unused config.json entry 2016-05-03 18:28:08 +02:00
Olivier Lambert
16b982b953 fix patches issues 2016-05-03 18:25:33 +02:00
Olivier Lambert
e92b87095b container for header 2016-05-03 17:37:42 +02:00
Olivier Lambert
e5f1aa689b flex shrink fix 2016-05-03 17:35:59 +02:00
Olivier Lambert
f39a05cd8d smaller header for VM and host view 2016-05-03 16:42:32 +02:00
Olivier Lambert
c5e22b785a replace xo.call 2016-05-03 15:24:39 +02:00
Olivier Lambert
11f93a125c add header for host view 2016-05-03 15:22:25 +02:00
Pierre Donias
38a9cb002d Menu and sticky header (#891)
Fixes #705
2016-05-03 14:13:11 +02:00
ABHAMON Ronan
cf1a38a004 Recursive forms implementation. (#894) 2016-05-03 12:09:22 +02:00
Olivier Lambert
d6e823d19d fix missingpatches call 2016-05-03 10:24:30 +02:00
Olivier Lambert
763a23d9d0 add recoveryStartVm method 2016-05-01 11:19:58 +02:00
Olivier Lambert
f266577f2f fix cloneVM method 2016-05-01 11:00:49 +02:00
Olivier Lambert
1bb5e73668 less choices in VM action bar, advanced actions are in advanced tab 2016-05-01 10:39:26 +02:00
Olivier Lambert
b07bc755f6 add label on tab for number of patches missing 2016-05-01 09:57:05 +02:00
Olivier Lambert
db4b39c54b upgrade on poolpatches 2016-04-29 18:53:43 +02:00
Julien Fontanet
ffd95261c3 4.16.0 2016-04-29 12:23:59 +02:00
Olivier Lambert
82f38040c1 changelog 2016-04-29 12:22:14 +02:00
Olivier Lambert
7bb4f9f8e3 update host 2016-04-28 22:33:59 +02:00
Olivier Lambert
c2345df275 Merge pull request #905 from vatesfr/v5-toolbar-improved
V5 toolbar improved, missing patches and xo call removal
2016-04-28 19:15:46 +02:00
Olivier Lambert
b0c341da3f minor fixes 2016-04-28 19:11:56 +02:00
Olivier Lambert
b1ccc16da7 add missing patches 2016-04-28 19:11:44 +02:00
Julien Fontanet
16856a5911 feat(utils/routes): support plain route def 2016-04-28 17:32:06 +02:00
Olivier Lambert
d4ee364349 add host action bar 2016-04-28 17:03:11 +02:00
Olivier Lambert
db62ca7b4b proper col/row for vm tabs 2016-04-28 16:20:36 +02:00
Olivier Lambert
d0b99b854d remove more xo direct call 2016-04-28 15:57:17 +02:00
Olivier Lambert
b3b13b3e01 less xo calls 2016-04-28 15:55:46 +02:00
Julien Fontanet
5cb738b82b chore: reduce xo.call() direct use 2016-04-28 15:22:40 +02:00
Olivier Lambert
cfe3b15cbe Toolbar actions 2016-04-28 15:06:28 +02:00
Julien Fontanet
a1bde80925 chore: move IntlProvider inside XoApp. 2016-04-28 12:37:49 +02:00
Julien Fontanet
4c958dd584 chore(selectors): remove unused code 2016-04-28 12:05:37 +02:00
Olivier Lambert
a05d4d3d18 Merge pull request #892 from vatesfr/pierre-v5-better-action-bar-ui
Better action bar UI.
2016-04-28 10:28:05 +02:00
Pierre
503b6dc914 Dropdown on hover. 2016-04-28 09:24:29 +02:00
Pierre
141cbcd1c0 [WIP] Dropdown on hover. 2016-04-28 09:24:29 +02:00
Olivier Lambert
e340d2d3f5 remove useless import 2016-04-27 18:27:57 +02:00
Olivier Lambert
436d5a3a66 remove dup message keys 2016-04-27 18:27:57 +02:00
Julien Fontanet
3e04fd4790 feat: initial ACLs handling 2016-04-27 16:44:01 +02:00
Julien Fontanet
3f6d149f9d feat(reducer/combineActionHandlers): perf for n=1 & common errors
Warnings when:

- no handlers defined
- there is an handler for the action type `type` (likely an error)

There is now an optimized implementation when there is only one handler.
2016-04-27 16:44:01 +02:00
Olivier Lambert
977fc7832a initial network tab added for host 2016-04-27 15:23:39 +02:00
Olivier Lambert
2e2f0e2e3d handle basic log removal 2016-04-27 14:59:40 +02:00
Julien Fontanet
5628beee72 fix(selectors/createCollectionWrapper): handle removed props 2016-04-27 14:14:53 +02:00
Olivier Lambert
a88fea560b fix indentation 2016-04-26 18:14:37 +02:00
Olivier Lambert
5832345b96 Merge pull request #902 from vatesfr/v5-hosts
V5 hosts view
2016-04-26 15:57:32 +02:00
Olivier Lambert
d3d2daa12f use createFinder 2016-04-26 15:39:58 +02:00
Julien Fontanet
f6f90982f4 feat(selectors/createPager): selector creator to return a page of items 2016-04-26 15:36:21 +02:00
Olivier Lambert
98323b08f0 add host console 2016-04-26 15:31:13 +02:00
Olivier Lambert
8f3112a5e2 advanced tab completed 2016-04-26 15:31:12 +02:00
Olivier Lambert
3408bd41ad additionnal host info 2016-04-26 15:31:12 +02:00
Olivier Lambert
68e12e86c1 add start time in host view 2016-04-26 15:31:12 +02:00
Olivier Lambert
890e0b4906 add control domain VM 2016-04-26 15:31:12 +02:00
Olivier Lambert
3aca7c7ae5 more working host tabs 2016-04-26 15:31:11 +02:00
Olivier Lambert
e8077ddbc5 initial work on host view 2016-04-26 15:31:11 +02:00
Julien Fontanet
6e04907357 feat(selectors/createFinder): selector creator to find an item in a collection 2016-04-26 15:26:28 +02:00
Greenkeeper
3d8c9a99fe chore(package): update modular-css to version 0.18.0 (#895)
https://greenkeeper.io/
2016-04-20 08:05:43 +01:00
Greenkeeper
730768705b chore(package): update modular-css to version 0.17.1 (#890)
http://greenkeeper.io/
2016-04-19 10:04:12 +01:00
Julien Fontanet
13f75a37ab CSS: remove 60em min-width on .xo-content. 2016-04-18 15:58:43 +01:00
Julien Fontanet
f74c69ea6f Disable Bootstrap flex for now as it is broken in responsive mode. 2016-04-18 15:57:56 +01:00
Greenkeeper
fe7be0f518 chore(package): update react-key-handler to version 0.2.0 (#886)
http://greenkeeper.io/
2016-04-18 09:33:30 +01:00
Olivier Lambert
2f0e656c45 menu: move remote in settings 2016-04-15 14:44:53 +02:00
Julien Fontanet
de489b799b Fix npm run build. 2016-04-14 19:12:25 +02:00
Julien Fontanet
6250ef49b6 Minor change in VM/Snapshots. 2016-04-14 19:12:25 +02:00
Julien Fontanet
afdab8dcde Fix About page. 2016-04-14 19:12:25 +02:00
Olivier Lambert
8e1d39f37f navbar style 2016-04-14 18:45:18 +02:00
Julien Fontanet
2a8c346a65 Add basic Page not found. 2016-04-14 18:06:56 +02:00
Olivier Lambert
71e431e744 better dashboard 2016-04-14 17:45:00 +02:00
Julien Fontanet
265cb75d70 Dashboard/Overview: 10 top SRs by size. 2016-04-14 16:51:29 +02:00
Olivier Lambert
4d0470838a move storage list to health 2016-04-14 16:41:05 +02:00
Olivier Lambert
d4ed3aeac0 better dashboard 2016-04-14 16:14:35 +02:00
Julien Fontanet
89fa89fe98 utils/@routes(): TODO add support for function childRoutes. 2016-04-14 16:02:05 +02:00
Julien Fontanet
2d663f0ac5 Remove unused import. 2016-04-14 15:58:38 +02:00
Julien Fontanet
bc9b3f1c5c utils/@routes(): accepts a plain object for child routes. 2016-04-14 15:48:38 +02:00
Julien Fontanet
2f0a46a46d utils/@routes(): use it AMAP. 2016-04-14 15:45:27 +02:00
Julien Fontanet
9559604d1e menu: Remove home special case. 2016-04-14 15:45:27 +02:00
Julien Fontanet
b779ab9bc5 utils/@routes(): accepts a subpath as index route. 2016-04-14 15:45:26 +02:00
Julien Fontanet
481943051c Tooltip component. 2016-04-14 15:45:26 +02:00
Olivier Lambert
cb49b7a906 fix styles 2016-04-14 11:55:14 +02:00
Olivier Lambert
090e4b3117 working health view 2016-04-14 10:35:58 +02:00
Olivier Lambert
9a40d5f784 style fixes 2016-04-13 19:49:59 +02:00
Olivier Lambert
1485637c6d more dashboard work 2016-04-13 17:52:54 +02:00
Olivier Lambert
b0ec8e26e8 sr table on dashboard 2016-04-13 13:53:01 +02:00
Olivier Lambert
e4c12e08cb sr panel on dashboard view 2016-04-13 12:55:24 +02:00
Olivier Lambert
a1675745b5 first charts on Dashboard view 2016-04-13 12:23:38 +02:00
Olivier Lambert
40beb5b104 dashboard work 2016-04-13 11:26:07 +02:00
Olivier Lambert
e49e2f51c2 more work on dashboard 2016-04-12 22:57:45 +02:00
Olivier Lambert
99669f2678 add missing files 2016-04-12 19:03:40 +02:00
Olivier Lambert
9aa88b9dad initial work on dashboard 2016-04-12 19:02:53 +02:00
Julien Fontanet
10de29795a settings/servers: implement edition. 2016-04-12 18:49:25 +02:00
Julien Fontanet
c4767e74f4 Initial server view. 2016-04-12 18:16:07 +02:00
Julien Fontanet
19f8666e1e Use Babel transforms to optimize React in prod. 2016-04-12 18:16:07 +02:00
Julien Fontanet
2e2bbdf0d7 Update gulp-csso to v2. 2016-04-12 18:16:07 +02:00
Olivier Lambert
973bee0ffa fix links to submenus 2016-04-12 18:05:20 +02:00
Olivier Lambert
9470cdf774 better wording 2016-04-12 18:00:11 +02:00
Olivier Lambert
ab198ea60b add import entry 2016-04-12 17:56:59 +02:00
Olivier Lambert
c78f4bb6d2 add dashboard submenu 2016-04-12 17:47:38 +02:00
Olivier Lambert
ff054ca47f Add menu 2016-04-12 17:28:24 +02:00
Olivier Lambert
30cc804022 backup submenu 2016-04-12 17:13:35 +02:00
Olivier Lambert
890d733bf8 snapshot name edition 2016-04-12 15:19:05 +02:00
Olivier Lambert
a554a9c4a1 add editable vdi name and description 2016-04-12 15:16:40 +02:00
Olivier Lambert
9ef212937d log view style 2016-04-12 14:05:30 +02:00
Pierre Donias
82d9c53f3e Left side menu and navbar. (#869)
Merge PR #869 from @pdonias.
2016-04-08 18:34:20 +02:00
Julien Fontanet
d9d91c4953 Update react/react-dom to v15 and react-intl to v2. 2016-04-08 15:16:55 +02:00
Julien Fontanet
dd5f5282e0 Remove special uglify setting for Angular. 2016-04-07 17:44:53 +02:00
Julien Fontanet
cc116defc6 Remove unused browser override ws.js. 2016-04-07 17:44:00 +02:00
Julien Fontanet
90c755e120 Remove unused browserify-plain-jade. 2016-04-07 17:43:40 +02:00
Julien Fontanet
85d1c80581 Move common modules out of src/node_modules. 2016-04-07 15:03:14 +02:00
Julien Fontanet
5bb04d3857 reducers: Fix REMOVE_OBJECTS handling. 2016-04-07 15:03:13 +02:00
Olivier Lambert
dde8404242 fix average computing on sparkline graphs 2016-04-07 14:00:10 +02:00
Julien Fontanet
7940bd2dcc Merge pull request #858 from vatesfr/abhamonr-vm-stats-only-when-vm-running
Get stats only when vm running.
2016-04-07 13:45:30 +02:00
Julien Fontanet
da48d117a0 Merge pull request #872 from vatesfr/pierre-v5-vm-state-icon
VM: state icon in front of VM's name.
2016-04-07 10:16:23 +02:00
Pierre
ffc74967fc VM: State icon in front of VM's name 2016-04-07 10:12:46 +02:00
wescoeur
c7388d5836 Get stats only when vm running. 2016-04-06 16:24:31 +02:00
Julien Fontanet
ee4b8fc66f Merge pull request #868 from vatesfr/pierre-v5-add-multiple-tags
Tags: Adding a tag does not remove the input field
2016-04-06 15:41:55 +02:00
Julien Fontanet
c73f22ca45 home: refactor filtering on top of selectors/createFilter(). 2016-04-06 15:35:45 +02:00
Julien Fontanet
bafa053fd1 vm/logs: initial listing. 2016-04-06 14:55:44 +02:00
Pierre
a00406d2b3 Tags: Adding a tag should keep the input field on 2016-04-06 11:59:10 +02:00
Julien Fontanet
df91e17dc6 vm/disks: use more variables. 2016-04-06 11:01:05 +02:00
Julien Fontanet
7c6eeababc vm/disks: add SR name. 2016-04-06 10:52:34 +02:00
Julien Fontanet
911a5067f9 Factorize selectors. 2016-04-06 10:47:35 +02:00
Julien Fontanet
bfa0fe9c51 editable/Text: initial implementation. 2016-04-05 15:40:16 +02:00
Julien Fontanet
7dafe31d51 react-novnc: Ungrab keyboard/mouse on mouse leave. 2016-04-04 15:44:26 +02:00
Olivier Lambert
1271ecedb4 Merge pull request #859 from vatesfr/pierre-v5-icon-fixed-width
Icon: `fixedWidth` attribute
2016-04-01 19:17:50 +02:00
Pierre
80be18068f Icon: lodash/isInteger instead of Number.isInteger 2016-04-01 18:31:16 +02:00
Julien Fontanet
ecbf2c0958 utils/BlockLink: To use for a block link :) 2016-04-01 18:16:56 +02:00
Pierre
cfb84b677f Icon: fixedWidth attribute 2016-04-01 18:12:33 +02:00
Olivier Lambert
dfbd7a5d76 Merge pull request #857 from vatesfr/abhamonr-fix-select-vm-stats
Fix select vm stats.
2016-04-01 17:30:27 +02:00
wescoeur
8e157f8ff7 Fix select vm stats. 2016-04-01 17:16:04 +02:00
Olivier Lambert
c75580e852 style fixes 2016-04-01 17:03:21 +02:00
Olivier Lambert
e21934fd55 more work on VM advanced tab 2016-04-01 17:03:21 +02:00
Julien Fontanet
844f1609c8 utils/@autobind: bind a method at first call. 2016-04-01 17:00:03 +02:00
Julien Fontanet
6f7de28672 Vm/Console: disable for non-running VMs. 2016-04-01 16:42:27 +02:00
Olivier Lambert
247212d768 update advanced view 2016-04-01 16:31:11 +02:00
Olivier Lambert
6592b880ea reorder stuff 2016-04-01 16:31:11 +02:00
Julien Fontanet
e27a4bf119 Merge pull request #855 from vatesfr/abhamonr-vms-stats-stateful
Stateful component vms stats.
2016-04-01 16:29:23 +02:00
wescoeur
dce5a83093 Stateful component vms stats.
Delay of stats interval is used to get stats.
2016-04-01 16:26:01 +02:00
Julien Fontanet
78c70f35a1 Merge pull request #856 from vatesfr/pierre-v5-col-offset
Added offset management in `Col` component.
2016-04-01 16:25:26 +02:00
Pierre
f4b84a0902 Added offset management in Col component 2016-04-01 16:15:46 +02:00
Julien Fontanet
b16237b514 Vm/Stats: Fix translation of select. 2016-04-01 15:37:26 +02:00
Olivier Lambert
b0077539fa Merge pull request #854 from vatesfr/abhamonr-vms-charts-tooltips
Tooltips on vms charts.
2016-04-01 15:21:14 +02:00
Olivier Lambert
63602216eb using icon component 2016-04-01 14:38:58 +02:00
Olivier Lambert
2f93935009 more links 2016-04-01 14:30:21 +02:00
Olivier Lambert
c0a0b653c1 add links and fix icons 2016-04-01 14:25:41 +02:00
wescoeur
24d6354467 Tooltips on vms charts. 2016-04-01 13:03:02 +02:00
Julien Fontanet
54a07469fd Fix granularity on stats tab. 2016-04-01 12:49:55 +02:00
Olivier Lambert
12f37bead1 translate inside option 2016-04-01 12:42:55 +02:00
Olivier Lambert
8d76dc2511 tab styles 2016-04-01 12:01:52 +02:00
Olivier Lambert
8ef7d6defc more i18n 2016-04-01 11:49:35 +02:00
Olivier Lambert
2f2cbbe3f0 various improvements 2016-04-01 11:05:27 +02:00
Julien Fontanet
2662ac719b Fix tabs display. 2016-04-01 10:50:38 +02:00
Olivier Lambert
1b0fe6e847 center console 2016-04-01 10:29:04 +02:00
Olivier Lambert
9fa9e26324 disk tab 2016-04-01 10:25:56 +02:00
Julien Fontanet
cb434df099 Move prod deps to dev deps. 2016-04-01 10:21:04 +02:00
Julien Fontanet
e3cb3002fe Replace react-tabs with Bootstrap styles + router. 2016-04-01 10:21:00 +02:00
Olivier Lambert
8b2a09b522 better tag placement 2016-03-31 18:44:33 +02:00
Olivier Lambert
03265d2545 stats select translation 2016-03-31 18:10:16 +02:00
Olivier Lambert
9b25e07b5e sort tags and fix snapshot icon 2016-03-31 17:57:56 +02:00
Julien Fontanet
60a21ca58d Merge pull request #853 from vatesfr/abhamonr-vms-charts-time-labels
Intl labels on vms charts.
2016-03-31 17:52:05 +02:00
wescoeur
05172492ef Intl labels on vms charts. 2016-03-31 17:44:42 +02:00
Julien Fontanet
a9c089a994 Merge pull request #851 from vatesfr/pierre-v5-action-bar
VM action bar (without handlers) with react-bootstrap-4
2016-03-31 17:37:18 +02:00
Julien Fontanet
988d018c8e Merge pull request #852 from vatesfr/pierre-v5-fix-sparklines-proptype
Fixed Sparklines propType: array --> object
2016-03-31 17:35:53 +02:00
Pierre
853c611fde Fixed Sparklines propType: array --> object 2016-03-31 17:30:43 +02:00
Pierre
0886f5335f DropdownButton instead of SplitButton (custom width). 2016-03-31 17:22:00 +02:00
Pierre
024d481a4d Using react-bootstrap-4. VM action bar done (no handlers). Minor fixes. 2016-03-31 17:22:00 +02:00
Pierre
b80de1af95 ERR action-bar: DropdownButton incompatible with React 15 2016-03-31 17:21:59 +02:00
Julien Fontanet
da44268f0d ReactNoVnc: remove unnecessary logs. 2016-03-31 17:16:15 +02:00
Julien Fontanet
b921a3ed8e Split the VM view. 2016-03-31 17:15:23 +02:00
Julien Fontanet
d2c9c824f9 Declare routes directly on component. 2016-03-31 16:41:13 +02:00
Olivier Lambert
21c255051c better console panel style 2016-03-31 16:39:30 +02:00
Olivier Lambert
dccf09c5dd charts improvements 2016-03-31 16:12:57 +02:00
Julien Fontanet
0772a17e4c grid/Col: use xs for smallSize. 2016-03-31 15:21:26 +02:00
Olivier Lambert
644ee782d2 fix style issues 2016-03-31 15:11:40 +02:00
Olivier Lambert
ea180cc415 fix col sizes 2016-03-31 15:11:10 +02:00
Olivier Lambert
68e3c4e6ed change stats titles 2016-03-31 15:07:59 +02:00
Julien Fontanet
47b1c2d680 grid/Col: now expects smallSize, mediumSize or largeSize. 2016-03-31 15:07:32 +02:00
Olivier Lambert
2e513043b7 Merge pull request #850 from vatesfr/abhamonr-vms-graphs-titles
Abhamonr vms graphs titles
2016-03-31 14:16:40 +02:00
wescoeur
ea942635f7 VMs graphs titles. 2016-03-31 13:12:41 +02:00
wescoeur
8a5f5cc673 Display spinner icon when stats granularity is selected. 2016-03-31 12:51:29 +02:00
Julien Fontanet
6cc673035d Work around babel-eslint bug. 2016-03-31 11:36:42 +02:00
Olivier Lambert
02d717e5a8 better graph style 2016-03-31 11:35:12 +02:00
Julien Fontanet
77d83b06bd Merge pull request #848 from vatesfr/abhamonr-vm-stats-various-time-steps
Possibility to select time interval on vm stats.
2016-03-31 11:19:57 +02:00
wescoeur
e3d8eabb05 Possibility to select time interval on vm stats. 2016-03-31 11:16:49 +02:00
Julien Fontanet
0596b0106f Handle removeObjects action. 2016-03-30 18:07:40 +02:00
Julien Fontanet
765bbd90fc Install ghooks. 2016-03-30 18:01:51 +02:00
Julien Fontanet
3799902a8a More efficient sort of snapshots. 2016-03-30 17:43:42 +02:00
Julien Fontanet
ca1dbb4556 Initial VM console. 2016-03-30 17:37:37 +02:00
Julien Fontanet
df551d457c Merge pull request #847 from vatesfr/abhamonr-chartist-graphs
VM's graphs. (Chartist)
2016-03-30 16:57:17 +02:00
wescoeur
278d518d8f VM's graphs. (Chartist) 2016-03-30 16:53:20 +02:00
Olivier Lambert
b06fa191f7 prepare limit space in vm advanced tab 2016-03-30 16:02:34 +02:00
Olivier Lambert
13d73f6f27 add translations 2016-03-30 15:54:49 +02:00
Olivier Lambert
d59af117c0 start work of Advanced vm tab 2016-03-30 15:51:23 +02:00
Olivier Lambert
69efd85ad6 check if VBD is a cd drive 2016-03-30 12:33:54 +02:00
Olivier Lambert
ead51787a8 Merge pull request #846 from vatesfr/pierre-v5-tags
Add and remove tag actions. Tag UI improvements.
2016-03-30 12:23:09 +02:00
Olivier Lambert
4bf0fee20d protection again counting twice or more a VDI size 2016-03-30 12:09:04 +02:00
Pierre
209693ee3f Add and remove tag actions. Tag UI improvements. Icon handles lg size. 2016-03-30 11:48:01 +02:00
Olivier Lambert
796d4f5b08 not pill in snapshots tab when no snapshots 2016-03-30 11:37:55 +02:00
Olivier Lambert
96914c9901 compute total vm disk space 2016-03-30 11:29:37 +02:00
Olivier Lambert
9e6073bf56 improve translation 2016-03-30 11:00:16 +02:00
Olivier Lambert
f8ad58159c minor i18n fix 2016-03-29 23:30:34 +02:00
Olivier Lambert
ede4a02315 various UI improvements 2016-03-29 22:43:06 +02:00
Olivier Lambert
e25faba990 basic VM disk tab 2016-03-29 22:35:27 +02:00
Olivier Lambert
4ccf272e53 add snapshot pill in tab name and more i18n 2016-03-29 21:08:58 +02:00
Olivier Lambert
5ad0951db3 improve snapshot view 2016-03-29 20:56:18 +02:00
Olivier Lambert
f72fcb76e3 start to work on snapshot view 2016-03-29 18:58:36 +02:00
Olivier Lambert
8ca00c81b2 display network name_label in vm network view 2016-03-29 18:02:55 +02:00
Olivier Lambert
5c7f87b8ae case when not VM network interface at all 2016-03-29 16:38:16 +02:00
Julien Fontanet
9f4f7ec88c utils/If: experimental component. 2016-03-29 16:14:54 +02:00
Olivier Lambert
ba7676f778 using keys for array 2016-03-29 16:01:46 +02:00
Julien Fontanet
ac248c32bb Fix handling of empty VM stats. 2016-03-29 15:44:46 +02:00
Julien Fontanet
dd6a3e8535 Update babel-eslint to 6.0.0. 2016-03-29 15:32:20 +02:00
Julien Fontanet
69b538cfd6 Remove unused redux-promise. 2016-03-29 15:32:20 +02:00
Julien Fontanet
e248c22f4b Remove unused redux-router. 2016-03-29 15:32:20 +02:00
Olivier Lambert
eff3c43483 better network page 2016-03-29 15:31:56 +02:00
Olivier Lambert
1cbfc3ccd4 various updates and fixes 2016-03-29 14:58:23 +02:00
Olivier Lambert
2fa72838f9 add sparklines to console view 2016-03-28 21:48:56 +02:00
Olivier Lambert
a559ec1fda better vm network translations 2016-03-28 21:42:13 +02:00
Olivier Lambert
8ddbc8b1fb minor network improvements 2016-03-28 21:21:59 +02:00
Olivier Lambert
c650a43d38 add translations 2016-03-28 21:12:30 +02:00
Olivier Lambert
ab4cc20c8c better network tab 2016-03-28 21:09:21 +02:00
Olivier Lambert
30d613ff04 minor vm action reordering 2016-03-28 20:09:41 +02:00
Olivier Lambert
b93da5a281 tag minor style modification 2016-03-28 18:06:57 +02:00
Olivier Lambert
9e2cf67a93 mockup layout for console view 2016-03-28 12:52:00 +02:00
Olivier Lambert
e87720ffdd minor style modification 2016-03-25 23:22:43 +01:00
Olivier Lambert
5108ed7da5 Merge pull request #842 from vatesfr/pierre-v5-tags
`Tags` component, basic version
2016-03-25 23:21:05 +01:00
Olivier Lambert
77936d86f2 position tags in VM view 2016-03-25 23:10:30 +01:00
Olivier Lambert
cbe8927d73 move tag style in main css file + PR comments 2016-03-25 22:57:47 +01:00
Pierre
47a7e98da8 Tags component 2016-03-25 22:56:36 +01:00
Olivier Lambert
d4f27cd2e0 default large buttons 2016-03-25 22:39:35 +01:00
Olivier Lambert
c355ad7a86 css fix 2016-03-25 19:16:48 +01:00
Julien Fontanet
c8c9ec081d Remove incorrect divs in VM page. 2016-03-25 19:12:46 +01:00
Julien Fontanet
1289e46401 Fix xo-sparlines/CpuSparkLines proptypes. 2016-03-25 19:12:46 +01:00
Julien Fontanet
0b54292130 VIFs in VM view. 2016-03-25 19:04:54 +01:00
Olivier Lambert
d1e2b91116 comment ationbar button to merge 2016-03-25 18:49:09 +01:00
Julien Fontanet
cd96b3e8f6 Merge pull request #844 from vatesfr/olivierlambert-clipboard
5.x clipboard
2016-03-25 18:19:27 +01:00
Olivier Lambert
9119f4d06f style fix 2016-03-25 18:16:40 +01:00
Olivier Lambert
4e6ccf2c81 reverse order on general tab 2016-03-25 18:08:18 +01:00
Olivier Lambert
6294e43762 various improvements 2016-03-25 18:08:18 +01:00
Olivier Lambert
feca78e55d reorder stuff and add clipboard support for UUID 2016-03-25 18:08:17 +01:00
Olivier Lambert
0c86526ad2 better sparklines styles 2016-03-25 18:07:33 +01:00
Olivier Lambert
9fd1d26067 minor graph improvements 2016-03-25 17:44:55 +01:00
Julien Fontanet
5ec02078d1 Merge pull request #843 from vatesfr/abhamonr-vm-charts
Vm Sparklines (Cpu, ram, vif, xvd)
2016-03-25 16:02:26 +01:00
wescoeur
012c6f3d41 Vm Sparklines (Cpu, ram, vif, xvd) 2016-03-25 15:41:44 +01:00
Olivier Lambert
8e89d492fc fix style 2016-03-25 14:55:46 +01:00
Olivier Lambert
69fe2f0443 date stuff 2016-03-25 14:52:14 +01:00
Julien Fontanet
ebd7f4ae1b Ignore Redux dev tools in production. 2016-03-25 12:40:46 +01:00
Julien Fontanet
f9ca4f339e Name components for better error messages. 2016-03-25 12:40:46 +01:00
Olivier Lambert
5919020e1c add cpu weight 2016-03-25 12:26:27 +01:00
Olivier Lambert
9d09c2356d add VM style 2016-03-25 12:08:24 +01:00
Julien Fontanet
3cfb597fc6 Properly forward props arg to super(). 2016-03-25 11:39:24 +01:00
Julien Fontanet
03f2da19e5 utils#Debug: component printing an object in JSON. 2016-03-25 11:39:24 +01:00
Julien Fontanet
951e62d984 Do not declare global variable osToFamily in utils. 2016-03-25 11:39:23 +01:00
Olivier Lambert
146039c4c5 translate UUID and use definitions for Advanced tab 2016-03-25 11:37:13 +01:00
Olivier Lambert
98a216fdb9 better translations 2016-03-25 11:00:45 +01:00
Olivier Lambert
28ea09a0c4 More translations 2016-03-24 23:32:59 +01:00
Olivier Lambert
a98a772360 remove trailing comma 2016-03-24 23:11:43 +01:00
Olivier Lambert
4f1da8a24b more translations 2016-03-24 23:11:03 +01:00
Olivier Lambert
cb83e71f2b better VM view 2016-03-24 19:29:49 +01:00
Olivier Lambert
646d174616 VM view stuff 2016-03-24 19:17:59 +01:00
Julien Fontanet
34cf78fd33 Require npm >=3. 2016-03-24 18:55:34 +01:00
Olivier Lambert
457e1bee2f fix osfamily 2016-03-24 18:45:02 +01:00
Olivier Lambert
6b95c63c1e style and some fixes 2016-03-24 18:25:46 +01:00
Julien Fontanet
e965f222db Merge pull request #841 from vatesfr/pierre-v5-action-bar
Action bar fixes
2016-03-24 18:17:50 +01:00
Olivier Lambert
d1591bc01c fix style 2016-03-24 17:36:12 +01:00
Olivier Lambert
a72846be7a uncomment icons 2016-03-24 17:36:12 +01:00
Julien Fontanet
5072661369 utils#{format,parse}Size() 2016-03-24 17:30:04 +01:00
Olivier Lambert
5417a83662 Add icon distro display 2016-03-24 17:09:28 +01:00
Pierre
23be006932 Removed console button in action bar 2016-03-24 16:54:33 +01:00
Pierre
e28553767e Label and icon are required for an ActionBar 2016-03-24 16:53:19 +01:00
Julien Fontanet
c1f64c043d Merge pull request #836 from vatesfr/pierre-v5-action-bar
v5 action bar
2016-03-24 16:50:02 +01:00
Pierre
9e6d0183d4 Button component. 2016-03-24 16:44:57 +01:00
Pierre
8dc7f3fb9e Better propTypes for ActionBar 2016-03-24 16:44:56 +01:00
Pierre
ac7b3b9b67 Icons CSS. propTypes on ActionBar and Icon. ActionBar in separate file. 2016-03-24 16:44:56 +01:00
Pierre
af0ebba5db Default icon. Lint fixes 2016-03-24 16:44:56 +01:00
Pierre
b454709e5e xo-icon- prefix in icon component. Default icon size: 1 2016-03-24 16:44:56 +01:00
Pierre
78684607bd Added VM actions icons 2016-03-24 16:44:56 +01:00
Pierre
3790cad5e5 Editable icon size. VM actions messages. 2016-03-24 16:44:56 +01:00
Pierre
77b7c091a8 Icon component 2016-03-24 16:44:56 +01:00
Pierre
450ad62fa9 ActionButton component. Added delete button in VM view. 2016-03-24 16:44:56 +01:00
Julien Fontanet
1d138c33a4 Col/Row can take a className prop. 2016-03-24 16:32:20 +01:00
Julien Fontanet
fb2a0e4a1e Add font-mfizz. 2016-03-24 16:18:04 +01:00
Julien Fontanet
da3db0b0f9 Cache node_modules on Travis. 2016-03-24 15:54:08 +01:00
Julien Fontanet
bc8aaadd90 Initial VM stats. 2016-03-24 15:39:18 +01:00
Olivier Lambert
e6054a4971 use Col from grid component 2016-03-24 13:44:54 +01:00
Olivier Lambert
26548e1929 using grid component 2016-03-24 13:37:45 +01:00
Julien Fontanet
4424cf8190 Initial grid component. 2016-03-24 12:53:19 +01:00
Julien Fontanet
67c1aacd54 @propTypes decorator. 2016-03-24 12:53:19 +01:00
Olivier Lambert
39b046f18b center properly 2016-03-24 12:50:29 +01:00
Julien Fontanet
d630f04872 Add missing dependency vinyl. 2016-03-24 11:43:22 +01:00
Julien Fontanet
35403c87bd Add missing dependency jsonrcp-websocket-client. 2016-03-24 11:30:27 +01:00
Julien Fontanet
f2b247e042 No tests for now: remove unused deps. 2016-03-24 11:16:04 +01:00
Julien Fontanet
94022bd9f2 Add missing dependency readable-stream. 2016-03-24 11:16:04 +01:00
Julien Fontanet
ccb2abb950 Remove support for Node < 4. 2016-03-24 11:16:00 +01:00
Julien Fontanet
cbafc15292 Various updates. 2016-03-24 10:52:49 +01:00
Julien Fontanet
e12c52294a Various updates. 2016-03-24 10:46:51 +01:00
Olivier Lambert
63b529da00 add react tabs 2016-03-24 10:06:51 +01:00
Olivier Lambert
e4be2fd19e add icons 2016-03-23 19:02:19 +01:00
Julien Fontanet
269bf4feec Various updates. 2016-03-23 18:40:56 +01:00
Olivier Lambert
9d4c4a1e2b translate tabs 2016-03-23 18:38:52 +01:00
Olivier Lambert
2e41fdcb41 More work on VM view 2016-03-23 18:01:32 +01:00
Olivier Lambert
2e48218623 conforming to the planned template display 2016-03-23 16:15:01 +01:00
Olivier Lambert
8576a54056 start the VM in the action bar 2016-03-23 16:02:30 +01:00
Julien Fontanet
79d924f920 Various updates. 2016-03-23 15:23:08 +01:00
Julien Fontanet
305beb3af8 Fix indent. 2016-03-23 13:00:04 +01:00
Julien Fontanet
06fceded14 Various updates. 2016-03-23 12:46:30 +01:00
Olivier Lambert
0eadfd5a58 Merge pull request #835 from vatesfr/abhamonr-fix-plugin-multiple-users-groups-loading
Avoid multiple users/groups loading in plugins view. (fix vatesfr/xo-…
2016-03-23 12:11:44 +01:00
wescoeur
eea34a4f6c Avoid multiple users/groups loading in plugins view. (fix vatesfr/xo-web#829) 2016-03-23 12:03:56 +01:00
Julien Fontanet
500ec36522 Various updates. 2016-03-23 11:21:47 +01:00
Julien Fontanet
ca525bd08c 4.15.1 2016-03-22 15:28:15 +01:00
Olivier Lambert
ac2ffc4586 Fix #830 2016-03-22 14:35:57 +01:00
Olivier Lambert
5781269557 Remove old message about not supported SMB remote for delta 2016-03-22 13:32:19 +01:00
Olivier Lambert
e4422b9fe7 Display only permitted SR for VM copy 2016-03-22 12:14:25 +01:00
Olivier Lambert
269f76d546 Fix #832 2016-03-22 11:47:34 +01:00
Julien Fontanet
540e3f0aaa 4.15.0 2016-03-21 16:50:22 +01:00
Olivier Lambert
5f64ae28e0 Merge pull request #800 from vatesfr/abhamonr-delta-backup-on-smb
SMB can be used with delta backups.
2016-03-21 09:37:40 +01:00
Julien Fontanet
ece364c823 Various updates. 2016-03-18 18:09:44 +01:00
Olivier Lambert
f669f64fcb add changelog 2016-03-18 16:17:36 +01:00
Julien Fontanet
2a53ed93c4 Various updates. 2016-03-18 15:00:33 +01:00
Julien Fontanet
2b731fb30c Various updates. 2016-03-18 12:14:06 +01:00
Julien Fontanet
be2db2dd8e Fix immediately spelling (thx @Danp2). 2016-03-18 10:02:30 +01:00
Julien Fontanet
09c08df1b9 Various updates. 2016-03-18 09:17:49 +01:00
Olivier Lambert
9ccd3438ad Fix #821 2016-03-17 19:15:35 +01:00
Julien Fontanet
393bcbcca5 Various updates. 2016-03-17 18:14:57 +01:00
Julien Fontanet
7fac0958b8 Various updates. 2016-03-17 16:35:45 +01:00
Olivier Lambert
c6a0874b3b Merge pull request #758 from vatesfr/marsaudf-backup-ui-fixes
Fixed backup remote lists
2016-03-17 14:23:56 +01:00
Olivier Lambert
9c80470185 Merge pull request #820 from vatesfr/pierre-server-version
Added server version in About view (See #807)
2016-03-17 14:21:56 +01:00
Julien Fontanet
2034445f5b Various updates. 2016-03-17 12:13:29 +01:00
Pierre
fd8da5ffba Added server version in About view 2016-03-17 10:52:32 +01:00
Olivier Lambert
e987af87f6 Merge pull request #809 from vatesfr/abhamonr-recursive-plugins-config
Complex configurations plugins. (recursion, array of objects...)
2016-03-17 10:31:31 +01:00
Pierre
0074cc3933 Fixed refresh bug. 2016-03-17 09:51:53 +01:00
Pierre
5f2ce89316 Fixed Object/String/Array display in plugin config. 2016-03-17 09:51:53 +01:00
Pierre
60492c48a6 Typo fix. 2016-03-17 09:51:53 +01:00
Pierre
eed2d70017 Better array display when items are objects 2016-03-17 09:51:53 +01:00
Pierre
b859adaa8c Fixes 2016-03-17 09:51:53 +01:00
Pierre
89a587f9ae enum handling 2016-03-17 09:51:53 +01:00
wescoeur
fb56bcff80 Complex configurations plugins. (recursion, array of objects...) 2016-03-17 09:51:53 +01:00
Fabrice Marsaud
99eb6907dd updater will block nav after 1 min out 2016-03-16 16:32:52 +01:00
Fabrice Marsaud
3743fad899 when updater-blocked, any nav attempt will retry connection 2016-03-16 16:18:02 +01:00
Olivier Lambert
c1e59a7e03 Merge pull request #818 from vatesfr/pierre-feedback-when-error-on-sr
Feedback when disconnecting a host from an SR does not work (See #810)
2016-03-16 15:54:11 +01:00
Pierre
b34dee1f83 Error message formatting: leading capital and trailing period. 2016-03-16 15:45:36 +01:00
Pierre
6edd65ad8f xo.pbd.disconnect instead of xoApi.call(...). Better error notification. 2016-03-16 15:01:29 +01:00
Olivier Lambert
0959ca6a40 Merge pull request #813 from vatesfr/pierre-network-management
Host & pool views: better network management (See #805)
2016-03-16 10:57:34 +01:00
Pierre
1287fa2cd0 Allowed network creation without PIF 2016-03-16 10:50:47 +01:00
Pierre
a5a07f250d UI improvements 2016-03-16 10:46:08 +01:00
Pierre
089fb526f5 IP configuration: DHCP & No IP. UI fixes. 2016-03-16 10:46:08 +01:00
Pierre
af58b7593a Configure PIF IP. UI fixes. 2016-03-16 10:46:07 +01:00
Pierre
d4508b25ce Only physical PIFs should be shown when creating network 2016-03-16 10:46:07 +01:00
Pierre
9edc218eaa Delete network 2016-03-16 10:46:07 +01:00
Olivier Lambert
3790f753aa Merge pull request #801 from vatesfr/pierre-pool-networks
Pool view: Interface panel and network creation (See #226)
2016-03-16 10:45:06 +01:00
Julien Fontanet
82b30d8388 Do not use nice-pipe (too buggy). 2016-03-10 17:23:04 +01:00
Julien Fontanet
e9a0dc7826 Various updates. 2016-03-10 17:01:16 +01:00
wescoeur
8ce3a4f904 SMB can be used with delta backups. 2016-03-09 17:12:15 +01:00
Julien Fontanet
1d42e9c348 Disable tests for now. 2016-03-09 14:29:27 +01:00
Julien Fontanet
2340a6bc37 Various updates. 2016-03-09 14:28:18 +01:00
Pierre
be0b9c7e53 Removed log 2016-03-09 10:38:45 +01:00
Pierre
6d75cd9025 Minor fix 2016-03-09 10:33:09 +01:00
Pierre
345d6f369e network.create instead of createNetwork for host and pool 2016-03-09 10:33:09 +01:00
Pierre
959ea86d85 Pool view: Interface panel and network creation 2016-03-09 10:33:09 +01:00
Olivier Lambert
b67a99af3d Add types for ISO SRs 2016-03-04 13:36:35 +01:00
Olivier Lambert
fa3b848d40 Merge pull request #799 from vatesfr/pierre-add-smb-sr
UI to add an SMB SR (user and password inputs) (Fix #731)
2016-03-03 18:25:55 +01:00
Pierre
0f971e9e7d Minor fix. 2016-03-03 17:55:43 +01:00
Pierre
c17f76c009 SMB case fix. 2016-03-03 17:50:23 +01:00
Pierre
bf23b5d295 Enabled Create button and removed search button when SMB. 2016-03-03 17:39:18 +01:00
Pierre
09c7256d42 UI to add an SMB SR (user and password inputs) 2016-03-03 16:58:46 +01:00
Olivier Lambert
eaee8a2fbb Merge pull request #798 from vatesfr/pierre-new-vm-from-pool-enhancement
New VM on pool: display all SRs (Fix #790)
2016-03-03 16:41:45 +01:00
Pierre
3b18dd67be Compatibility with self service 2016-03-03 16:16:03 +01:00
Pierre
c3f87b4248 Explicit message in summary 2016-03-03 15:27:07 +01:00
Julien Fontanet
f6b91ad652 Update deps. 2016-03-03 14:54:29 +01:00
Pierre
1c79edc52f Detection of incompatible SRs 2016-03-03 13:05:26 +01:00
Julien Fontanet
fe2dfd0e8f Merge branch 'stable' into next-release 2016-03-03 13:03:18 +01:00
Olivier Lambert
fa6056c1b1 Add unknown state 2016-03-03 12:26:06 +01:00
Olivier Lambert
d5762c7ad8 limit VDI form for admin users 2016-03-03 12:26:06 +01:00
Julien Fontanet
d9c9dd2a4f Welcome message in the issue template 2016-03-03 11:31:27 +01:00
Olivier Lambert
3a4d945c68 Merge pull request #793 from Danp2/next-release
Fix issue with gathering NFS shares
2016-03-02 23:41:04 +01:00
Danp2
f4a364816b Fix issue with gathering NFS shares
scsiList vs nfsList
2016-03-02 16:11:26 -06:00
Olivier Lambert
931bc03cab Inverse critical/warning SR usage in health view 2016-03-02 18:30:05 +01:00
Olivier Lambert
1abd4937cd Merge pull request #792 from vatesfr/pierre-licenses
Host and pool licenses (Fix #763)
2016-03-02 17:39:09 +01:00
Pierre
0df8b51c62 Host view and pool view: License panel. 2016-03-02 17:09:20 +01:00
Julien Fontanet
e5b7190015 Fix file uploads (2). 2016-03-02 16:24:15 +01:00
Olivier Lambert
279b8aacf6 add missing map filters 2016-03-02 15:44:23 +01:00
Olivier Lambert
9eebaab2f4 Add a map param for backup schedule view 2016-03-02 15:42:33 +01:00
Julien Fontanet
16e9d60033 Fix file uploads. 2016-03-02 15:27:30 +01:00
Olivier Lambert
335b378e9a Merge pull request #789 from vatesfr/pierre-disk-names
Default VDI names and descriptions (Fix #780)
2016-03-02 15:20:39 +01:00
Pierre
9c41bc33a3 Default names for template VDIs 2016-03-02 15:13:34 +01:00
Olivier Lambert
7f7d6b4d5d Merge pull request #772 from vatesfr/pierre-cpu-weight-in-resource-set
CPU weight edition should be disabled for resource set members (Fix #…
2016-03-02 14:56:05 +01:00
Olivier Lambert
cc0e3bbce0 Merge pull request #773 from vatesfr/pierre-host-patches-in-pool-view
Pool view: host updates panel (Fix #762)
2016-03-02 14:41:11 +01:00
Pierre
2eead65fef CPU weight should not be editable when creating a VM from a resource set 2016-03-02 14:36:46 +01:00
Pierre
e664be451f VDIs default names initialization in view 2016-03-02 12:40:44 +01:00
Pierre
d2d8160096 Default VDI names and descriptions 2016-03-02 10:28:29 +01:00
Olivier Lambert
3bd503c28d remove useless device number in VM migration window 2016-03-02 10:27:57 +01:00
Olivier Lambert
aa1df8eb33 Add Misc panel in host view to deliver system S/N etc. Fix #760 2016-03-01 17:17:36 +01:00
Olivier Lambert
c1aace45ae Match target network names for migration. Fix #782 2016-03-01 16:40:45 +01:00
Olivier Lambert
217a60aadc Improve the migration VM modal. Fix #785 2016-03-01 15:47:37 +01:00
Olivier Lambert
5654f528ca fix the task list for angular 1.5 2016-03-01 15:35:48 +01:00
Olivier Lambert
4da036a064 Add a missing angular map 2016-03-01 15:16:55 +01:00
Olivier Lambert
2256b3d262 Merge pull request #788 from Danp2/next-release
Minor fix to unitConfirms
2016-03-01 13:36:24 +01:00
Danp2
d84ecc307d Minor fix to unitConfirms 2016-03-01 06:32:30 -06:00
Julien Fontanet
237313d5fb Merge pull request #781 from vatesfr/fix-vdi-iteration-in-vm-creation
VM creation: correctly iterate over template VDIs (fix #778).
2016-02-29 18:49:33 +01:00
Olivier Lambert
7caf766bca Do not display CDs VBDs 2016-02-29 18:44:08 +01:00
Julien Fontanet
0a3f9f5ef1 Add issue template. 2016-02-29 16:37:13 +01:00
Julien Fontanet
e890b8f7c1 VM creation: correctly iterate over template VDIs (fix #778). 2016-02-29 15:14:52 +01:00
Olivier Lambert
dc4d5f0ecb fixing angular 1.5 2016-02-26 18:03:07 +01:00
Pierre
a2f0980731 Pool view: host updates panel (Fix #762) 2016-02-26 17:59:33 +01:00
Julien Fontanet
0a5c029f8b lodash.sum(): does not work with objects anymore. 2016-02-26 17:36:06 +01:00
Julien Fontanet
85bb79e4fb Move shims to app/node_modules.
Hopefully this will avoid there accidental deletion by users.
2016-02-26 15:55:05 +01:00
Julien Fontanet
f18d1e50f8 Fix ng-file-upload import. 2016-02-26 15:55:05 +01:00
Julien Fontanet
943b10dd5d Update angular-chart.js to 0.8. 2016-02-26 15:55:05 +01:00
Julien Fontanet
0a48e17c88 Update ng-angular-upload to 12.0. 2016-02-26 15:55:05 +01:00
Olivier Lambert
da1381e14e Map usage before orderBy for dashboard view 2016-02-26 15:53:14 +01:00
Pierre
bdffb0ee10 CPU weight edition should be disabled for resource set members (Fix #767) 2016-02-26 14:51:53 +01:00
Julien Fontanet
7bdb7d2ca8 Fix angular-bootstrap usage in updater. 2016-02-26 14:34:56 +01:00
Julien Fontanet
92567561b8 Update angularjs-toaster to 1.2. 2016-02-26 13:06:02 +01:00
Julien Fontanet
335bdcd89d Update angular-xeditable-npm to 0.1.9. 2016-02-26 13:05:56 +01:00
Julien Fontanet
4c2fc13abb Update ui-select to 0.14. 2016-02-26 13:05:51 +01:00
Julien Fontanet
7f8f29daa2 Update angular-ui-bootstrap to 0.14. 2016-02-26 12:43:48 +01:00
Julien Fontanet
8fac845ecb Use angular-ui-{event,indeterminate} instead of deprecated angular-ui-utils. 2016-02-26 12:34:56 +01:00
Olivier Lambert
d8076e7630 Merge pull request #756 from vatesfr/pierre-self-service-dashboard
Self service dashboard (Fix #741)
2016-02-26 11:56:27 +01:00
Pierre
b370bc27c4 Fixed condition when no resource set found 2016-02-26 11:41:08 +01:00
Pierre
334c3f4488 Removed arrows when only 1 page is needed 2016-02-26 11:34:46 +01:00
Pierre
33822109c0 Minor fixes 2016-02-26 11:20:11 +01:00
Pierre
b1f18b0f5b Added templates, SRs and networks to details 2016-02-26 10:00:32 +01:00
Julien Fontanet
c0d6284368 Fix Jade compilation for Node < v4. 2016-02-25 17:48:16 +01:00
Pierre
16e294f6fc Resource sets details 2016-02-25 17:47:46 +01:00
Pierre
5f7925b2b8 Display page number 2016-02-25 17:47:46 +01:00
Pierre
2e001b0ce4 Pages layout 2016-02-25 17:47:46 +01:00
Pierre
c1ca3ff5b5 [WIP] All resource sets together (other layout commented). 2016-02-25 17:47:46 +01:00
Pierre
1de33cd4ca Self service dashboard (Fix #741)
Fixed no limit condition and icons.
2016-02-25 17:47:46 +01:00
Olivier Lambert
77b773388f fix network list in new vm, using map before orderby 2016-02-25 15:47:34 +01:00
Olivier Lambert
3e668ee439 Merge pull request #759 from vatesfr/fix-babel-6-imports
Fix Babel 6: `require module` --> `require(module).default`
2016-02-25 11:38:23 +01:00
Pierre
0d3ea9af36 Fix Babel 6: require module --> require(module).default 2016-02-25 11:21:47 +01:00
Fabrice Marsaud
0d81bc8056 Fixed backup remote lists 2016-02-25 09:24:38 +01:00
Julien Fontanet
e4b532a34d week heatmap: fix lodash.pluck usage. 2016-02-24 17:49:32 +01:00
Julien Fontanet
61f86c0ac3 Merge pull request #739 from vatesfr/update-deps
Update deps
2016-02-24 16:58:35 +01:00
Julien Fontanet
66fad37116 Remove unused lodash.puck dep. 2016-02-24 16:53:12 +01:00
Julien Fontanet
c7bbd8c823 Update nice-pipe to v3. 2016-02-24 16:44:07 +01:00
Julien Fontanet
dc6f8baf1e Update Babel to v6. 2016-02-24 16:44:07 +01:00
Julien Fontanet
0e76e65d65 Update Coffeeify to v2. 2016-02-24 16:44:07 +01:00
Julien Fontanet
877dbed999 Update Browserify to v13. 2016-02-24 16:44:07 +01:00
Julien Fontanet
668fd05fae Update Bluebird to v3. 2016-02-24 16:44:07 +01:00
Julien Fontanet
3e49998f41 Update Lodash to v4. 2016-02-24 16:44:07 +01:00
Julien Fontanet
99b183ac17 Update Angular to v1.5. 2016-02-24 16:42:51 +01:00
Julien Fontanet
ccbcaa94fe Various updates. 2016-01-28 09:45:40 +01:00
Julien Fontanet
0ffa9d4225 Various updates. 2016-01-22 16:03:33 +01:00
Julien Fontanet
8e5dee79e0 Use Babel 6. 2015-12-04 11:49:19 +01:00
Julien Fontanet
9abd9d20ec Initial work on React/Redux. 2015-12-03 18:22:13 +01:00
401 changed files with 34889 additions and 23599 deletions

View File

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

View File

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

11
.gitignore vendored
View File

@@ -1,10 +1,9 @@
/.nyc_output/
/bower_components/
/dist/
/node_modules/*
!/node_modules/*.js
/node_modules/*.js/
jsconfig.json
.idea
npm-debug.log
npm-debug.log.*
!node_modules/*
node_modules/*/

View File

@@ -1,5 +1,6 @@
/examples/
example.js
example.js.map
*.example.js
*.example.js.map

View File

@@ -1,10 +1,12 @@
language: node_js
node_js:
- 'stable'
#- '4' # Disabled for now because npm 2 cannot properly handled broken peer dependencies.
cache:
directories:
- node_modules
# Use containers.
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
sudo: false
before_install:
- npm i -g npm

View File

@@ -1,5 +1,133 @@
# ChangeLog
## **5.0.0** (2016-06-24)
### Enhancements
- Handle failed quiesce in snapshots [\#1088](https://github.com/vatesfr/xo-web/issues/1088)
- Sparklines stats [\#1061](https://github.com/vatesfr/xo-web/issues/1061)
- Task view [\#1060](https://github.com/vatesfr/xo-web/issues/1060)
- Improved import system [\#1048](https://github.com/vatesfr/xo-web/issues/1048)
- Backup restore view improvements [\#1021](https://github.com/vatesfr/xo-web/issues/1021)
- Restore VM - Wrong VLAN on the VMs interface [\#1016](https://github.com/vatesfr/xo-web/issues/1016)
- Fast Disk Cloning [\#960](https://github.com/vatesfr/xo-web/issues/960)
- Disaster recovery job should target SRs, not pools [\#955](https://github.com/vatesfr/xo-web/issues/955)
- Improve Header/Content interaction in a page [\#926](https://github.com/vatesfr/xo-web/issues/926)
- New default view [\#912](https://github.com/vatesfr/xo-web/issues/912)
- Xen Patching - Restart Pending [\#883](https://github.com/vatesfr/xo-web/issues/883)
- Hide About page for user that are not admin [\#877](https://github.com/vatesfr/xo-web/issues/877)
- ACL: Ability to view/sort/group by User/Group, Objects or Role [\#875](https://github.com/vatesfr/xo-web/issues/875)
- ACL: Ability to select multiple users & group when creating a rule [\#874](https://github.com/vatesfr/xo-web/issues/874)
- Translation [\#839](https://github.com/vatesfr/xo-web/issues/839)
- XO offer useless network interfaces for XenMontion [\#833](https://github.com/vatesfr/xo-web/issues/833)
- Show HVM, PVM, PVHVM modes in guest details [\#806](https://github.com/vatesfr/xo-web/issues/806)
- Tree view: display cpu available/total for each host [\#696](https://github.com/vatesfr/xo-web/issues/696)
- Greenkeeper integration [\#667](https://github.com/vatesfr/xo-web/issues/667)
- Clarify vCPUs and RAM editor [\#658](https://github.com/vatesfr/xo-web/issues/658)
- Backup LZ4 compression [\#647](https://github.com/vatesfr/xo-web/issues/647)
- Support enum in plugins configuration [\#638](https://github.com/vatesfr/xo-web/issues/638)
- Add configuration option to disable xoa-updater [\#535](https://github.com/vatesfr/xo-web/issues/535)
- Use cursors to add more context to actions [\#523](https://github.com/vatesfr/xo-web/issues/523)
- Review UI for flat view [\#354](https://github.com/vatesfr/xo-web/issues/354)
- Review UI for the tree view [\#353](https://github.com/vatesfr/xo-web/issues/353)
- Tag filtering [\#233](https://github.com/vatesfr/xo-web/issues/233)
- GUI review [\#230](https://github.com/vatesfr/xo-web/issues/230)
- Review UI for VM creation [\#214](https://github.com/vatesfr/xo-web/issues/214)
- Ability to collapse pools/hosts in main view [\#173](https://github.com/vatesfr/xo-web/issues/173)
- Issue importing .xva VM via xo-web [\#1022](https://github.com/vatesfr/xo-web/issues/1022)
- Enhancement Proposal - Cancel In Progress Backups [\#1003](https://github.com/vatesfr/xo-web/issues/1003)
- Can't create VM with CloudConfigDrive [\#917](https://github.com/vatesfr/xo-web/issues/917)
- Auth: LDAP User causes problems [\#893](https://github.com/vatesfr/xo-web/issues/893)
- No tags in Continuous Replication [\#838](https://github.com/vatesfr/xo-web/issues/838)
- Delta backup Depth not working [\#802](https://github.com/vatesfr/xo-web/issues/802)
- Update Section - Running version info missing - gui enhancement [\#795](https://github.com/vatesfr/xo-web/issues/795)
- On reboot, vnc console wrongly scaled [\#722](https://github.com/vatesfr/xo-web/issues/722)
- Make the object name \(title\) "sticky" at the top of the page [\#705](https://github.com/vatesfr/xo-web/issues/705)
- pool view: display Local SR from hosts in the current pool [\#692](https://github.com/vatesfr/xo-web/issues/692)
- tree view: display all IPs [\#689](https://github.com/vatesfr/xo-web/issues/689)
- XO5 parallel distribution [\#462](https://github.com/vatesfr/xo-web/issues/462)
- Load balancing with XO [\#423](https://github.com/vatesfr/xo-web/issues/423)
### Bug fixes
- vCPUs number when no tools installed [\#1089](https://github.com/vatesfr/xo-web/issues/1089)
- Config Drive textbox disappears when content is deleted [\#1012](https://github.com/vatesfr/xo-web/issues/1012)
- storage status not changed in host view page after disconnect/connect [\#1009](https://github.com/vatesfr/xo-web/issues/1009)
- Cannot Delete Logs From Backup Overview [\#1004](https://github.com/vatesfr/xo-web/issues/1004)
- \[v5.x\] Plugins configuration: optional non-used objects are sent [\#1000](https://github.com/vatesfr/xo-web/issues/1000)
- "@" char in remote password break the remote view [\#997](https://github.com/vatesfr/xo-web/issues/997)
- Handle MEMORY\_CONSTRAINT\_VIOLATION correctly [\#970](https://github.com/vatesfr/xo-web/issues/970)
- VM creation error on XenServer Dundee [\#964](https://github.com/vatesfr/xo-web/issues/964)
- Copy VMs doesn't display all SRs [\#945](https://github.com/vatesfr/xo-web/issues/945)
- Autopower\_on wrong value [\#937](https://github.com/vatesfr/xo-web/issues/937)
- Correctly handle unknown users in group view [\#900](https://github.com/vatesfr/xo-web/issues/900)
- Importing into Dundee [\#887](https://github.com/vatesfr/xo-web/issues/887)
- update status - gui resize issue [\#803](https://github.com/vatesfr/xo-web/issues/803)
- Backup Remote Stores Problem [\#751](https://github.com/vatesfr/xo-web/issues/751)
- VM view is broken when changing a disk SR twice [\#670](https://github.com/vatesfr/xo-web/issues/670)
- console mouse sync [\#280](https://github.com/vatesfr/xo-web/issues/280)
## **4.16.0** (2016-04-29)
Maintenance release
### Enhancements
- TOO\_MANY\_PENDING\_TASKS [\#861](https://github.com/vatesfr/xo-web/issues/861)
### Bug fixes
- Incorrect VM target name with continuous replication [\#904](https://github.com/vatesfr/xo-web/issues/904)
- Error while deleting users [\#901](https://github.com/vatesfr/xo-web/issues/901)
- Use an available path to the SR to create a config drive [\#882](https://github.com/vatesfr/xo-web/issues/882)
- VM autoboot don't set the right pool parameter [\#879](https://github.com/vatesfr/xo-web/issues/879)
- BUG: ACL with NFS ISO Library not working! [\#870](https://github.com/vatesfr/xo-web/issues/870)
- Broken paths in backups in SMB [\#865](https://github.com/vatesfr/xo-web/issues/865)
- Plugins page loads users/groups multiple times [\#829](https://github.com/vatesfr/xo-web/issues/829)
- "Ghost" VM remains after migration [\#769](https://github.com/vatesfr/xo-web/issues/769)
## **4.15.0** (2016-03-21)
Load balancing, SMB delta support, advanced network operations...
### Enhancements
- Add the job name inside the backup email report [\#819](https://github.com/vatesfr/xo-web/issues/819)
- Delta backup with quiesce [\#812](https://github.com/vatesfr/xo-web/issues/812)
- Hosts: No user feedback when error occurs with SR connect / disconnect [\#810](https://github.com/vatesfr/xo-web/issues/810)
- Expose components versions [\#807](https://github.com/vatesfr/xo-web/issues/807)
- Rework networks/PIFs management [\#805](https://github.com/vatesfr/xo-web/issues/805)
- Displaying all SRs and a list of available hosts for creating VM from a pool [\#790](https://github.com/vatesfr/xo-web/issues/790)
- Add "Source network" on "VM migration" screen [\#785](https://github.com/vatesfr/xo-web/issues/785)
- Migration queue [\#783](https://github.com/vatesfr/xo-web/issues/783)
- Match network names for VM migration [\#782](https://github.com/vatesfr/xo-web/issues/782)
- Disk names [\#780](https://github.com/vatesfr/xo-web/issues/780)
- Self service: should the user be able to set the CPU weight? [\#767](https://github.com/vatesfr/xo-web/issues/767)
- host & pool Citrix license status [\#763](https://github.com/vatesfr/xo-web/issues/763)
- pool view: Provide "updates" section [\#762](https://github.com/vatesfr/xo-web/issues/762)
- XOA ISO image: ambigious root disk label [\#761](https://github.com/vatesfr/xo-web/issues/761)
- Host info: provide system serial number [\#760](https://github.com/vatesfr/xo-web/issues/760)
- CIFS ISO SR Creation [\#731](https://github.com/vatesfr/xo-web/issues/731)
- MAC address not preserved on VM restore [\#707](https://github.com/vatesfr/xo-web/issues/707)
- Failing replication job should send reports [\#659](https://github.com/vatesfr/xo-web/issues/659)
- Display networks in the Pool view [\#226](https://github.com/vatesfr/xo-web/issues/226)
### Bug fixes
- Broken link to backup remote [\#821](https://github.com/vatesfr/xo-web/issues/821)
- Issue with self-signed cert for email plugin [\#817](https://github.com/vatesfr/xo-web/issues/817)
- Plugins view, reset form and errors [\#815](https://github.com/vatesfr/xo-web/issues/815)
- HVM recovery mode is broken [\#794](https://github.com/vatesfr/xo-web/issues/794)
- Disk bug when creating vm from template [\#778](https://github.com/vatesfr/xo-web/issues/778)
- Can't mount NFS shares in remote stores [\#775](https://github.com/vatesfr/xo-web/issues/775)
- VM disk name and description not passed during creation [\#774](https://github.com/vatesfr/xo-web/issues/774)
- NFS mount problem for Windows share [\#771](https://github.com/vatesfr/xo-web/issues/771)
- lodash.pluck not installed [\#757](https://github.com/vatesfr/xo-web/issues/757)
- this.\_getAuthenticationTokensForUser is not a function [\#755](https://github.com/vatesfr/xo-web/issues/755)
- CentOS 6.x 64bit template creates a VM that won't boot [\#733](https://github.com/vatesfr/xo-web/issues/733)
- Lot of xo:perf leading to XO crash [\#575](https://github.com/vatesfr/xo-web/issues/575)
- New collection checklist [\#262](https://github.com/vatesfr/xo-web/issues/262)
## **4.14.0** (2016-02-23)
Self service, custom CloudInit...

28
ISSUE_TEMPLATE.md Normal file
View File

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

View File

@@ -1,4 +1,4 @@
# Xen Orchestra Web
# Xen Orchestra Web [![Build Status](https://travis-ci.org/vatesfr/xo-web.png?branch=master)](https://travis-ci.org/vatesfr/xo-web)
![](http://i.imgur.com/tRffA5y.png)
@@ -6,9 +6,6 @@ XO-Web is part of [Xen Orchestra](https://github.com/vatesfr/xo), a web interfac
It is a web client for [XO-Server](https://github.com/vatesfr/xo-server).
[![Dependency Status](https://david-dm.org/vatesfr/xo-web.svg?theme=shields.io)](https://david-dm.org/vatesfr/xo-web)
[![devDependency Status](https://david-dm.org/vatesfr/xo-web/dev-status.svg?theme=shields.io)](https://david-dm.org/vatesfr/xo-web#info=devDependencies)
___
## Installation
@@ -29,10 +26,37 @@ Development build:
$ npm run dev
```
### Environment
#### `NODE_ENV`
Set to *production* it disables many checks which result in increased
performance.
#### `XOA_PLAN`
- 1: Free
- 2: Starter
- 3: Enterprise
- 4: Premium
- 5: Sources
```js
if (process.env.XOA_PLAN < 5) {
console.log('included only in XOA')
}
if (process.env.XOA_PLAN > 3) {
console.log('included only in Premium and Sources')
}
```
## How to report a bug?
If you are certain the bug is exclusively related to XO-Web, you may use the [bugtracker of this repository](https://github.com/vatesfr/xo-web/issues).
Otherwise, please consider using the [bugtracker of the general repository](https://github.com/vatesfr/xo/issues).
## Process for new release
```bash

View File

@@ -1,216 +0,0 @@
// Must be loaded before angular.
import 'angular-file-upload'
import angular from 'angular'
import uiBootstrap from'angular-ui-bootstrap'
import uiIndeterminate from'angular-ui-indeterminate'
import uiRouter from'angular-ui-router'
import uiSelect from'angular-ui-select'
import naturalSort from 'angular-natural-sort'
import xeditable from 'angular-xeditable'
import xoDirectives from 'xo-directives'
import xoFilters from 'xo-filters'
import xoServices from 'xo-services'
import aboutState from './modules/about'
import backupState from './modules/backup'
import consoleState from './modules/console'
import dashboardState from './modules/dashboard'
import deleteVmsState from './modules/delete-vms'
import genericModalState from './modules/generic-modal'
import hostState from './modules/host'
import listState from './modules/list'
import migrateVmState from './modules/migrate-vm'
import navbarState from './modules/navbar'
import newSrState from './modules/new-sr'
import newVmState from './modules/new-vm'
import poolState from './modules/pool'
import selfState from './modules/self'
import settingsState from './modules/settings'
import srState from './modules/sr'
import taskScheduler from './modules/task-scheduler'
import treeState from './modules/tree'
import updater from './modules/updater'
import vmState from './modules/vm'
import '../dist/bower_components/angular-chart.js/dist/angular-chart.js'
// ===================================================================
export default angular.module('xoWebApp', [
uiBootstrap,
uiIndeterminate,
uiRouter,
uiSelect,
naturalSort,
xeditable,
xoDirectives,
xoFilters,
xoServices,
aboutState,
backupState,
consoleState,
dashboardState,
deleteVmsState,
genericModalState,
hostState,
listState,
migrateVmState,
navbarState,
newSrState,
newVmState,
poolState,
selfState,
settingsState,
srState,
taskScheduler,
treeState,
updater,
vmState,
'chart.js'
])
// Prevent Angular.js from mangling exception stack (interfere with
// source maps).
.factory('$exceptionHandler', () => function (exception) {
console.log(exception && exception.stack || exception)
})
.config(function (
$compileProvider,
$stateProvider,
$urlRouterProvider,
$tooltipProvider,
uiSelectConfig
) {
// Disable debug data to improve performance.
//
// In case of a bug, simply use `angular.reloadWithDebugInfo()` in
// the console.
//
// See https://docs.angularjs.org/guide/production
$compileProvider.debugInfoEnabled(false)
// Redirect to default state.
$stateProvider.state('index', {
url: '/',
controller: function ($state, xoApi) {
let isAdmin = xoApi.user && (xoApi.user.permission === 'admin')
$state.go(isAdmin ? 'tree' : 'list')
}
})
// Redirects unmatched URLs to `/`.
$urlRouterProvider.otherwise('/')
// Changes the default settings for the tooltips.
$tooltipProvider.options({
appendToBody: true,
placement: 'bottom'
})
uiSelectConfig.theme = 'bootstrap'
uiSelectConfig.resetSearchInput = true
})
.run(function (
$anchorScroll,
$cookies,
$rootScope,
$state,
editableOptions,
editableThemes,
modal,
notify,
updater,
xoApi
) {
// Milliseconds are not necessary.
const now = Math.floor(Date.now() / 1e3)
const oneWeekAgo = now - 7 * 24 * 3600
const previousDisclaimer = $cookies.get('previousDisclaimer')
if (
!previousDisclaimer ||
+previousDisclaimer < oneWeekAgo
) {
modal.alert({
title: 'Xen Orchestra from the sources',
htmlMessage: [
'You are using XO from the sources! That\'s great for a personal/non-profit usage.',
'If you are a company, it\'s better to use it with <a href="https://xen-orchestra.com/#!/xoa">XOA (turnkey appliance)</a> and our dedicated pro support!',
'This version is <strong>not bundled with any support nor updates</strong>. Use it with caution for critical tasks.'
].map(p => `<p>${p}</p>`).join('')
})
$cookies.put('previousDisclaimer', now)
}
let requestedStateName, requestedStateParams
$rootScope.$watch(() => xoApi.user, (user, previous) => {
// The user just signed in.
if (user && !previous) {
if (requestedStateName) {
$state.go(requestedStateName, requestedStateParams)
requestedStateName = requestedStateParams = null
} else {
$state.go('index')
}
}
})
$rootScope.$on('$stateChangeStart', function (event, state, stateParams, fromState) {
const { user } = xoApi
if (!user) {
event.preventDefault()
requestedStateName = state.name
requestedStateParams = stateParams
return
}
if (user.permission === 'admin') {
return
}
function forbidState () {
event.preventDefault()
notify.error({
title: 'Restricted area',
message: 'You do not have the permission to view this page'
})
if (fromState.url === '^') {
$state.go('index')
}
}
// Some pages requires the admin permission.
if (state.data && state.data.requireAdmin) {
forbidState()
return
}
const { id } = stateParams
if (id && !xoApi.canInteract(id, 'view')) {
forbidState()
return
}
})
// Work around UI Router bug (https://github.com/angular-ui/ui-router/issues/1509)
$rootScope.$on('$stateChangeSuccess', function () {
$anchorScroll()
})
editableThemes.bs3.inputClass = 'input-sm'
editableThemes.bs3.buttonsClass = 'btn-sm'
editableOptions.theme = 'bs3'
})
.name

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,119 +0,0 @@
angular = require 'angular'
forEach = require('lodash.foreach')
includes = require('lodash.includes')
Clipboard = require('clipboard')
isoDevice = require('iso-device')
#=====================================================================
module.exports = angular.module 'xoWebApp.console', [
require 'angular-ui-router'
require 'angular-no-vnc'
isoDevice
]
.config ($stateProvider) ->
$stateProvider.state 'consoles_view',
url: '/consoles/:id'
controller: 'ConsoleCtrl'
template: require './view'
.controller 'ConsoleCtrl', ($scope, $stateParams, xoApi, xo, xoHideUnauthorizedFilter, modal) ->
{id} = $stateParams
{get} = xoApi
pool = null
host = null
do (
srsByContainer = xoApi.getIndex('srsByContainer')
poolSrs = null
hostSrs = null
) ->
updateSrs = () =>
srs = []
poolSrs and forEach(poolSrs, (sr) => srs.push(sr))
hostSrs and forEach(hostSrs, (sr) => srs.push(sr))
$scope.SRs = xoHideUnauthorizedFilter(srs)
$scope.$watchCollection(
() => pool and srsByContainer[pool.id],
(srs) =>
poolSrs = srs
updateSrs()
)
$scope.$watchCollection(
() => host and srsByContainer[host.id],
(srs) =>
hostSrs = srs
updateSrs()
)
$scope.$watch(
-> xoApi.get id
(VM) ->
$scope.consoleUrl = null
unless xoApi.user
$scope.VDIs = []
return
$scope.VM = VM
return unless (
VM? and
VM.power_state is 'Running' and
not includes(VM.current_operations, 'clean_reboot')
)
pool = get VM.$poolId
return unless pool
$scope.consoleUrl = "./api/consoles/#{id}"
host = get VM.$container # host because the VM is running.
)
$scope.startVM = xo.vm.start
$scope.stopVM = (id) ->
modal.confirm
title: 'VM shutdown'
message: 'Are you sure you want to shutdown this VM ?'
.then ->
xo.vm.stop id
$scope.rebootVM = (id) ->
modal.confirm
title: 'VM reboot'
message: 'Are you sure you want to reboot this VM ?'
.then ->
xo.vm.restart id
$scope.eject = ->
xo.vm.ejectCd id
$scope.insert = (disc_id) ->
xo.vm.insertCd id, disc_id, true
$scope.vmClipboard = ''
$scope.setClipboard = (text) ->
$scope.vmClipboard = text
$scope.$applyAsync()
$scope.shutdownHost = (id) ->
modal.confirm({
title: 'Shutdown host'
message: 'Are you sure you want to shutdown this host?'
}).then ->
xo.host.stop id
$scope.rebootHost = (id) ->
modal.confirm({
title: 'Reboot host'
message: 'Are you sure you want to reboot this host? It will be disabled then rebooted'
}).then ->
xo.host.restart id
$scope.startHost = (id) ->
xo.host.start id
clipboard = new Clipboard('.copy')
clipboard.on('error', (e) -> console.log('Clipboard', e))
# A module exports its name.
.name

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,442 +0,0 @@
angular = require 'angular'
forEach = require 'lodash.foreach'
intersection = require 'lodash.intersection'
map = require 'lodash.map'
omit = require 'lodash.omit'
sum = require 'lodash.sum'
throttle = require 'lodash.throttle'
find = require 'lodash.find'
filter = require 'lodash.filter'
pluck = require 'lodash.pluck'
#=====================================================================
module.exports = angular.module 'xoWebApp.host', [
require 'angular-file-upload'
require 'angular-ui-router'
require 'tag'
]
.config ($stateProvider) ->
$stateProvider.state 'hosts_view',
url: '/hosts/:id'
controller: 'HostCtrl'
template: require './view'
.controller 'HostCtrl', (
$scope, $stateParams, $http
$upload
$window
$timeout
dateFilter
xoApi, xo, modal, notify, bytesToSizeFilter
) ->
do (
hostId = $stateParams.id
controllers = xoApi.getIndex('vmControllersByContainer')
poolPatches = xoApi.getIndex('poolPatchesByPool')
srs = xoApi.getIndex('srsByContainer')
tasks = xoApi.getIndex('runningTasksByHost')
vms = xoApi.getIndex('vmsByContainer')
) ->
Object.defineProperties($scope, {
controller: {
get: () => controllers[hostId]
},
poolPatches: {
get: () => $scope.host && poolPatches[$scope.host.$poolId]
},
sharedSrs: {
get: () => $scope.host && srs[$scope.host.$poolId]
},
srs: {
get: () => srs[hostId]
},
tasks: {
get: () => tasks[hostId]
},
vms: {
get: () => vms[hostId]
}
})
$window.bytesToSize = bytesToSizeFilter # FIXME dirty workaround to custom a Chart.js tooltip template
host = null
$scope.currentPatchPage = 1
$scope.currentLogPage = 1
$scope.currentPCIPage = 1
$scope.currentGPUPage = 1
$scope.refreshStatControl = refreshStatControl = {
baseStatInterval: 5000
baseTimeOut: 10000
period: null
running: false
attempt: 0
start: () ->
return if this.running
this.stop()
this.running = true
this._reset()
$scope.$on('$destroy', () => this.stop())
return this._trig(Date.now())
_trig: (t1) ->
if this.running
timeoutSecurity = $timeout(
() => this.stop(),
this.baseTimeOut
)
return $scope.refreshStats($scope.host.id)
.then () => this._reset()
.catch (err) =>
if !this.running || this.attempt >= 2 || $scope.host.power_state isnt 'Running' || $scope.isVMWorking($scope.host)
return this.stop()
else
this.attempt++
.finally () =>
$timeout.cancel(timeoutSecurity)
if this.running
t2 = Date.now()
return this.period = $timeout(
() => this._trig(t2),
Math.max(this.baseStatInterval - (t2 - t1), 0)
)
_reset: () ->
this.attempt = 0
stop: () ->
if this.period
$timeout.cancel(this.period)
this.running = false
return
}
$scope.$watch(
-> xoApi.get $stateParams.id
(host) ->
$scope.host = host
return unless host?
pool = $scope.pool = xoApi.get host.$poolId
SRsToPBDs = $scope.SRsToPBDs = Object.create null
for PBD in host.$PBDs
PBD = xoApi.get PBD
# If this PBD is unknown, just skips it.
continue unless PBD
SRsToPBDs[PBD.SR] = PBD
$scope.listMissingPatches($scope.host.id)
if host.power_state is 'Running'
refreshStatControl.start()
else
refreshStatControl.stop()
)
$scope.$watch('vms', (vms) =>
$scope.vCPUs = sum(vms, (vm) => vm.CPUs.number)
)
$scope.cancelTask = (id) ->
modal.confirm({
title: 'Cancel task'
message: 'Are you sure you want to cancel this task?'
}).then ->
xo.task.cancel id
$scope.destroyTask = (id) ->
modal.confirm({
title: 'Destroy task'
message: 'Are you sure you want to destroy this task?'
}).then ->
xo.task.destroy id
$scope.disconnectPBD = xo.pbd.disconnect
$scope.removePBD = xo.pbd.delete
$scope.new_sr = xo.pool.new_sr
$scope.pool_addHost = (id) ->
xo.host.attach id
$scope.pools = xoApi.getView('pools')
$scope.hostsByPool = xoApi.getIndex('hostsByPool')
$scope.pool_moveHost = (target) ->
modal.confirm({
title: 'Move host to another pool'
message: 'Are you sure you want to move this host?'
}).then ->
xo.pool.mergeInto({ source: $scope.pool.id, target: target.id })
$scope.pool_removeHost = (id) ->
modal.confirm({
title: 'Remove host from pool'
message: 'Are you sure you want to detach this host from its pool? It will be automatically rebooted AND LOCAL STORAGE WILL BE ERASED.'
}).then ->
xo.host.detach id
$scope.rebootHost = (id) ->
modal.confirm({
title: 'Reboot host'
message: 'Are you sure you want to reboot this host? It will be disabled then rebooted'
}).then ->
xo.host.restart id
$scope.enableHost = (id) ->
xo.host.enable id
notify.info {
title: 'Host action'
message: 'Host is enabled'
}
$scope.disableHost = (id) ->
modal.confirm({
title: 'Disable host'
message: 'Are you sure you want to disable this host? In disabled state, no new VMs can be started and currently active VMs on the host continue to execute.'
}).then ->
xo.host.disable id
.then ->
notify.info {
title: 'Host action'
message: 'Host is disabled'
}
$scope.restartToolStack = (id) ->
modal.confirm({
title: 'Restart XAPI'
message: 'Are you sure you want to restart the XAPI toolstack?'
}).then ->
xo.host.restartToolStack id
$scope.shutdownHost = (id) ->
modal.confirm({
title: 'Shutdown host'
message: 'Are you sure you want to shutdown this host?'
}).then ->
xo.host.stop id
$scope.emergencyShutdownHost = (hostId) ->
modal.confirm({
title: 'Shutdown host'
message: 'Are you sure you want to suspend all the VMs on this host and shut the host down?'
}).then ->
xo.host.emergencyShutdownHost hostId
$scope.saveHost = ($data) ->
{host} = $scope
{name_label, name_description, enabled} = $data
$data = {
id: host.id
}
if name_label isnt host.name_label
$data.name_label = name_label
if name_description isnt host.name_description
$data.name_description = name_description
if enabled isnt host.enabled
if host.enabled
$scope.disableHost($data.id)
else
$scope.enableHost($data.id)
# enabled is not set via the "set" method, so we remove it before send it
delete $data.enabled
xoApi.call 'host.set', $data
$scope.deleteAllLog = ->
modal.confirm({
title: 'Log deletion'
message: 'Are you sure you want to delete all the logs?'
}).then ->
forEach $scope.host.messages, (log) ->
console.log "Remove log #{log.id}"
xo.log.delete log.id
return
$scope.deleteLog = (id) ->
console.log "Remove log #{id}"
xo.log.delete id
$scope.connectPBD = (id) ->
console.log "Connect PBD #{id}"
xoApi.call 'pbd.connect', {id: id}
$scope.disconnectPBD = (id) ->
console.log "Disconnect PBD #{id}"
xoApi.call 'pbd.disconnect', {id: id}
$scope.removePBD = (id) ->
console.log "Remove PBD #{id}"
xoApi.call 'pbd.delete', {id: id}
$scope.connectPIF = (id) ->
console.log "Connect PIF #{id}"
xoApi.call 'pif.connect', {id: id}
$scope.disconnectPIF = (id) ->
console.log "Disconnect PIF #{id}"
xoApi.call 'pif.disconnect', {id: id}
$scope.removePIF = (id) ->
console.log "Remove PIF #{id}"
xoApi.call 'pif.delete', {id: id}
$scope.importVm = ($files, id) ->
file = $files[0]
notify.info {
title: 'VM import started'
message: "Starting the VM import"
}
xo.vm.import id
.then ({ $sendTo: url }) ->
return $upload.http {
method: 'POST'
url
data: file
}
.then (result) ->
throw result.status if result.status isnt 200
notify.info
title: 'VM import'
message: 'Success'
$scope.createNetwork = (name, description, pif, mtu, vlan) ->
$scope.createNetworkWaiting = true # disables form fields
notify.info {
title: 'Network creation...'
message: 'Creating the network'
}
params = {
host: $scope.host.id
name,
}
if mtu then params.mtu = mtu
if pif then params.pif = pif
if vlan then params.vlan = vlan
if description then params.description = description
xoApi.call 'host.createNetwork', params
.then ->
$scope.creatingNetwork = false
$scope.createNetworkWaiting = false
$scope.isPoolPatch = (patch) ->
return false if $scope.poolPatches is undefined
return $scope.poolPatches.hasOwnProperty(patch.uuid)
$scope.isPoolPatchApplied = (patch) ->
return true if patch.applied
hostPatch = intersection(patch.$host_patches, $scope.host.patches)
return false if not hostPatch.length
hostPatch = xoApi.get(hostPatch[0])
return hostPatch.applied
$scope.listMissingPatches = (id) ->
return xo.host.listMissingPatches id
.then (result) ->
$scope.updates = omit(result,map($scope.poolPatches,'id'))
$scope.installPatch = (id, patchUid) ->
console.log("Install patch "+patchUid+" on "+id)
notify.info {
title: 'Patch host'
message: "Patching the host, please wait..."
}
xo.host.installPatch id, patchUid
$scope.installAllPatches = (id) ->
modal.confirm({
title: 'Install all the missing patches'
message: 'Are you sure you want to install all the missing patches on this host? This could take a while...'
}).then ->
console.log('Installing all patches on host ' + id)
xo.host.installAllPatches id
$scope.refreshStats = (id) ->
return xo.host.refreshStats id
.then (result) ->
result.stats.cpuSeries = []
if result.stats.cpus.length >= 12
nValues = result.stats.cpus[0].length
nCpus = result.stats.cpus.length
cpuAVG = (0 for [1..nValues])
forEach result.stats.cpus, (cpu) ->
forEach cpu, (stat, index) ->
cpuAVG[index] += stat
return
return
forEach cpuAVG, (cpu, index) ->
cpuAVG[index] /= nCpus
return
result.stats.cpus = [cpuAVG]
result.stats.cpuSeries.push 'CPU AVG'
else
forEach result.stats.cpus, (v,k) ->
result.stats.cpuSeries.push 'CPU ' + k
return
result.stats.pifSeries = []
pifsArray = []
forEach result.stats.pifs.rx, (v,k) ->
return unless v
result.stats.pifSeries.push '#' + k + ' in'
result.stats.pifSeries.push '#' + k + ' out'
pifsArray.push (v || [])
pifsArray.push (result.stats.pifs.tx[k] || [])
return
result.stats.pifs = pifsArray
forEach result.stats.memoryUsed, (v, k) ->
result.stats.memoryUsed[k] = v*1024
forEach result.stats.memory, (v, k) ->
result.stats.memory[k] = v*1024
result.stats.date = []
timestamp = result.endTimestamp
for i in [result.stats.memory.length-1..0] by -1
result.stats.date.unshift new Date(timestamp*1000).toLocaleTimeString()
timestamp -= 5
$scope.stats = result.stats
$scope.statView = {
cpuOnly: false,
ramOnly: false,
netOnly: false,
loadOnly: false
}
$scope.canAdmin = (id = undefined) ->
if id == undefined
id = $scope.host && $scope.host.id
return id && xoApi.canInteract(id, 'administrate') || false
$scope.canOperate = (id = undefined) ->
if id == undefined
id = $scope.host && $scope.host.id
return id && xoApi.canInteract(id, 'operate') || false
$scope.canView = (id = undefined) ->
if id == undefined
id = $scope.host && $scope.host.id
return id && xoApi.canInteract(id, 'view') || false
# A module exports its name.
.name

View File

@@ -1,547 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-host(class="xo-color-{{host.power_state | lowercase}}")
| {{host.name_label}}
small(ng-if="pool.name_label")
| (
a(ui-sref="pools_view({id: pool.id})") {{pool.name_label}}
| )
p.center {{host.bios_strings["system-manufacturer"]}} {{host.bios_strings["system-product-name"]}}
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| General
span.quick-edit(tooltip="Edit General settings", ng-click="hostSettings.$show()", ng-if = '!hostSettings.$visible && canAdmin()')
i.fa.fa-edit.fa-fw
span.quick-edit(ng-if="hostSettings.$visible", tooltip="Cancel Edit", ng-click="hostSettings.$cancel()")
i.fa.fa-undo.fa-fw
.panel-body
form(editable-form="", name="hostSettings", onbeforesave="saveHost($data)")
dl.dl-horizontal
dt Name
dd
span(editable-text="host.name_label", e-name="name_label", e-form="hostSettings")
| {{host.name_label}}
dt Description
dd
span(editable-text="host.name_description", e-name="name_description", e-form="hostSettings")
| {{host.name_description}}
dt Enabled
dd
span(editable-select="host.enabled", e-ng-options="ap.v as ap.t for ap in [{v: true, t:'Yes'}, {v: false, t:'No'}]", e-name="enabled", e-form="hostSettings")
| {{host.enabled ? 'Yes' : 'No'}}
dt Tags
dd
xo-tag(ng-if = 'host', object = 'host')
dt CPUs
dd {{host.CPUs["cpu_count"]}}x {{host.CPUs["modelname"]}}
dt Hostname
dd
| {{host.hostname}}
dt UUID
dd {{host.UUID}}
dt iQN
dd {{host.iSCSI_name}}
dt(ng-if="refreshStatControl.running && stats") vCPUs/CPUs:
dd(ng-if="refreshStatControl.running && stats") {{vCPUs}}/{{host.CPUs['cpu_count']}}
dt(ng-if="refreshStatControl.running && stats") Running VMs:
dd(ng-if="refreshStatControl.running && stats") {{vms | count}}
dt(ng-if="refreshStatControl.running && stats") RAM (used/free):
dd(ng-if="refreshStatControl.running && stats") {{host.memory.usage | bytesToSize}}/{{host.memory.size | bytesToSize}}
.btn-form(ng-show="hostSettings.$visible")
p.center
button.btn.btn-default(type="button", ng-disabled="hostSettings.$waiting", ng-click="hostSettings.$cancel()")
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(type="submit", ng-disabled="hostSettings.$waiting")
i.fa.fa-save
| Save
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-stats
| Stats
.panel-body(ng-if="refreshStatControl.running && stats")
div(ng-if="statView.cpuOnly", ng-click="statView.cpuOnly = false")
p.stat-name
i.xo-icon-cpu
| &nbsp; CPU usage
canvas.chart.chart-line.chart-stat-full(
id="bigCpu"
data="stats.cpus"
labels="stats.date"
series="stats.cpuSeries"
colours="['#0000ff', '#9999ff', '#000099', '#5555ff', '#000055']"
legend="true"
options='{responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= Math.round(10*value)/10 %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= Math.round(10*value)/10 %>", pointDot: false, showScale: false, animation: false, datasetStrokeWidth: 0.8, scaleOverride: true, scaleSteps: 100, scaleStartValue: 0, scaleStepWidth: 1, pointHitDetectionRadius: 0}'
)
div(ng-if="statView.ramOnly", ng-click="statView.ramOnly = false")
p.stat-name
i.xo-icon-memory
//- i.fa.fa-bar-chart
//- i.fa.fa-tasks
//- i.fa.fa-server
| &nbsp; RAM usage
canvas.chart.chart-line.chart-stat-full(
id="bigRam"
data="[stats.memoryUsed,stats.memory]"
labels="stats.date"
series="['Used RAM', 'Total RAM']"
colours="['#ff0000', '#ffbbbb']"
legend="true"
options=' {responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
)
div(ng-if="statView.netOnly", ng-click="statView.netOnly = false")
p.stat-name
i.xo-icon-network
| &nbsp; Network I/O
canvas.chart.chart-line.chart-stat-full(
id="bigNet"
data="stats.pifs"
labels="stats.date"
series="stats.pifSeries"
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
legend="true"
options=' {responsive: true, maintainAspectRatio: false, tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= bytesToSize(value) %>", multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
)
div(ng-if="statView.loadOnly", ng-click="statView.loadOnly = false")
p.stat-name
i.fa.fa-cogs
| &nbsp; Load Average
canvas.chart.chart-line.chart-stat-full(
id="bigLoad"
data="[stats.load]"
labels="stats.date"
series="['Load']"
colours="['#960094']"
legend="true"
options=' {responsive: true, maintainAspectRatio: false, multiTooltipTemplate:"<%if (datasetLabel){%><%=datasetLabel%>: <%}%><%= bytesToSize(value) %>", datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false, pointHitDetectionRadius: 0}'
)
div(ng-if="!statView.netOnly && !statView.loadOnly && !statView.cpuOnly && !statView.ramOnly")
.row
.col-md-6(ng-click="statView.cpuOnly=true")
p.stat-name
i.xo-icon-cpu
| &nbsp; CPU usage
canvas.chart.chart-line.chart-stat-preview(
id="smallCpu"
data="stats.cpus"
labels="stats.date"
series="stats.cpuSeries"
colours="['#0000ff', '#9999ff', '#000099', '#5555ff', '#000055']"
options='{responsive: true, maintainAspectRatio: false, showTooltips: false, pointDot: false, showScale: false, animation: false, datasetStrokeWidth: 0.8, scaleOverride: true, scaleSteps: 100, scaleStartValue: 0, scaleStepWidth: 1}'
)
.col-md-6(ng-click="statView.ramOnly=true")
p.stat-name
i.xo-icon-memory
//- i.fa.fa-bar-chart
//- i.fa.fa-tasks
//- i.fa.fa-server
| &nbsp; RAM usage
canvas.chart.chart-line.chart-stat-preview(
id="smallRam"
data="[stats.memoryUsed,stats.memory]"
labels="stats.date"
series="['Used RAM', 'Total RAM']"
colours="['#ff0000', '#ffbbbb']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.row
.col-md-6(ng-click="statView.netOnly=true")
p.stat-name
i.xo-icon-network
| &nbsp; Network I/O
canvas.chart.chart-line.chart-stat-preview(
id="smallNet"
data="stats.pifs"
labels="stats.date"
series="stats.pifSeries"
colours="['#dddd00', '#dddd77', '#777700', '#dddd55', '#555500', '#ffdd00']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.col-md-6(ng-click="statView.loadOnly=true")
p.stat-name
i.fa.fa-cogs
| &nbsp; Load Average
canvas.chart.chart-line.chart-stat-preview(
id="smallDisk"
data="[stats.load]"
labels="stats.date"
series="['Load']"
colours="['#960094']"
options="{responsive: true, maintainAspectRatio: false, showTooltips: false, datasetStrokeWidth: 0.8, pointDot: false, showScale: false, scaleBeginAtZero: true, animation: false}"
)
.panel-body(ng-if="!refreshStatControl.running || !stats")
.row
.col-sm-4
p.stat-name CPU usage:
p.center.mid-stat {{vCPUs}}/{{host.CPUs['cpu_count']}}
.col-sm-4
p.stat-name RAM used:
p.center.mid-stat {{host.memory.usage | bytesToSize}}
.col-sm-4
p.stat-name Running VMs:
p.center.mid-stat {{vms | count}}
p.center(ng-if="refreshStatControl.running")
i.xo-icon-loading
| &nbsp; Fetching stats...
//- Action panel
.grid-sm(ng-if = 'canOperate()')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flash
| Actions
.panel-body.text-center
.grid-sm.grid--gutters
.grid.grid-cell
.grid-cell.btn-group
button.btn(tooltip="Add SR", tooltip-placement="top", type="button", style="width: 90%", xo-sref="SRs_new({container: host.id})", ng-if = 'canAdmin()')
i.xo-icon-sr.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Add VM", tooltip-placement="top", type="button", style="width: 90%", xo-sref="VMs_new({container: host.id})", ng-if = 'canAdmin()')
i.xo-icon-vm.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Reboot host", tooltip-placement="top", type="button", style="width: 90%", xo-click="rebootHost(host.id)", ng-if = 'canOperate()')
i.fa.fa-refresh.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Shutdown host", tooltip-placement="top", type="button", style="width: 90%", xo-click="shutdownHost(host.id)", ng-if = 'canOperate()')
i.fa.fa-power-off.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Suspend all VMs and shutdown host", tooltip-placement="top", type="button", style="width: 90%", xo-click="emergencyShutdownHost(host.id)", ng-if = 'canOperate()')
i.fa.fa-exclamation-triangle.fa-2x.fa-fw
.grid.grid-cell
.grid-cell.btn-group(ng-if="host.enabled")
button.btn(tooltip="Disable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="disableHost(host.id)", ng-if = 'canAdmin()')
i.fa.fa-times-circle.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="!host.enabled")
button.btn(tooltip="Enable host", tooltip-placement="top", type="button", style="width: 90%", xo-click="enableHost(host.id)", ng-if = 'canAdmin()')
i.fa.fa-check-circle.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Restart toolstack", tooltip-placement="top", type="button", style="width: 90%", xo-click="restartToolStack(host.id)", ng-if = 'canAdmin()')
i.fa.fa-retweet.fa-2x.fa-fw
.grid-cell.btn-group(ng-if="pool.name_label && (hostsByPool[pool.id] | count)>1")
button.btn(tooltip="Remove from pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_removeHost(host.id)", ng-if = 'canAdmin()')
i.fa.fa-cloud-upload.fa-2x.fa-fw
.grid-cell.btn-group.dropdown(
ng-if="pool.name_label && (hostsByPool[pool.id] | count)==1"
dropdown
)
button.btn.dropdown-toggle(
ng-if = 'canAdmin()'
dropdown-toggle
tooltip="Move host to another pool"
tooltip-placement="top"
type="button"
style="width: 90%"
)
i.fa.fa-cloud-download.fa-2x.fa-fw
span.caret
ul.dropdown-menu.left(role="menu")
li(ng-repeat="p in pools.all | map | orderBy:natural('name_label') track by p.id" ng-if="p!=pool")
a(xo-click="pool_moveHost(p)")
i.xo-icon-host.fa-fw
| To {{p.name_label}}
.grid-cell.btn-group(ng-if="!pool.name_label")
button.btn(tooltip="Add to pool", tooltip-placement="top", style="width: 90%", type="button", xo-click="pool_addHost(host.id)", ng-if = 'canAdmin()')
i.fa.fa-cloud-download.fa-2x.fa-fw
.grid-cell.btn-group(style="margin-bottom: 0.5em")
button.btn(
ng-if = 'canAdmin()'
tooltip="Import VM"
tooltip-placement="top"
type="button"
style="width: 90%"
ng-file-select = 'importVm($files, host.id)'
)
i.fa.fa-upload.fa-2x.fa-fw
.grid-cell.btn-group(style="margin-bottom: 0.5em")
button.btn(
tooltip="Host console"
tooltip-placement="top"
type="button"
style="width: 90%"
xo-sref="consoles_view({id: controller.id})"
)
i.xo-icon-console.fa-2x.fa-fw
//- TODO: Memory panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-memory
| Memory
.panel-body.text-center
.progress
.progress-bar-host(role="progressbar", aria-valuemin="0", aria-valuenow="{{controller.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[controller.memory.size, host.memory.size] | percentage}}", tooltip="{{host.name_label}}: {{[controller.memory.size, host.memory.size] | percentage}}")
small {{host.name_label}}
.progress-bar.progress-bar-vm(ng-repeat="VM in vms | map | orderBy:natural('name_label') track by VM.id", role="progressbar", aria-valuemin="0", aria-valuenow="{{VM.memory.size}}", aria-valuemax="{{host.memory.size}}", style="width: {{[VM.memory.size, host.memory.size] | percentage}}", xo-sref="VMs_view({id: VM.id})", tooltip="{{VM.name_label}}: {{[VM.memory.size, host.memory.size] | percentage}}")
small {{VM.name_label}}
ul.list-inline.text-center
li Total: {{host.memory.size | bytesToSize}}
li Currently used: {{host.memory.usage | bytesToSize}}
li Available: {{host.memory.size-host.memory.usage | bytesToSize}}
//- SR panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr
| Storage
.panel-body
table.table.table-hover
tr
th Name
th Format
th Size
th Physical usage
th Type
th Status
//- TODO: display PBD status for each SR of this host (connected or not)
//- Shared SR
tr(xo-sref="SRs_view({id: SR.id})", ng-repeat="SR in sharedSrs | map | orderBy:natural('name_label') track by SR.id")
td.oneliner
| {{SR.name_label}}
td {{SR.SR_type}}
td {{SR.size | bytesToSize}}
td
.progress-condensed
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
td
span.label.label-primary Shared
td(ng-if="SRsToPBDs[SR.id].attached")
span.label.label-success Connected
span.pull-right.btn-group.quick-buttons
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
i.fa.fa-unlink.fa-lg
td(ng-if="!SRsToPBDs[SR.id].attached")
span.label.label-default Disconnected
span.pull-right.btn-group.quick-buttons
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
i.fa.fa-link.fa-lg
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
i.fa.fa-ban.fa-lg
//- Local SR
//- TODO: migrate to SRs and not PBDs when implemented in xo-server spec
tr(xo-sref="SRs_view({id: SR.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.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
td
span.label.label-info Local
td(ng-if="SRsToPBDs[SR.id].attached")
span.label.label-success Connected
span.pull-right.btn-group.quick-buttons
a(tooltip="Disconnect this SR", xo-click="disconnectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
i.fa.fa-unlink.fa-lg
td(ng-if="!SRsToPBDs[SR.id].attached")
span.label.label-default Disconnected
span.pull-right.btn-group.quick-buttons
a(tooltip="Reconnect this SR", xo-click="connectPBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
i.fa.fa-link.fa-lg
a(tooltip="Forget this SR", xo-click="removePBD(SRsToPBDs[SR.id].id)", ng-if = 'canAdmin()')
i.fa.fa-ban.fa-lg
//- Networks/Interfaces panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-network
| Interfaces
.panel-body
table.table.table-hover
th.col-md-1 Device
th.col-md-1 VLAN
th.col-md-1 Address
th.col-md-2 MAC
th.col-md-1 MTU
th.col-md-1 Link status
tr(ng-repeat="PIF in host.$PIFs | resolve | orderBy:natural('name_label') track by PIF.id")
td
| {{PIF.device}}&nbsp;
span.label.label-primary(ng-if="PIF.management") XAPI
td
span(ng-if="PIF.vlan > -1")
| {{PIF.vlan}}
span(ng-if="PIF.vlan == -1")
| -
td.oneliner {{PIF.IP}} ({{PIF.mode}})
td.oneliner {{PIF.MAC}}
td {{PIF.MTU}}
td(ng-if="PIF.attached")
span.label.label-success Connected
span.pull-right.btn-group.quick-buttons
a(tooltip="Disconnect this interface", xo-click="disconnectPIF(PIF.id)", ng-if = 'canAdmin()')
i.fa.fa-unlink.fa-lg
td(ng-if="!PIF.attached")
span.label.label-default Disconnected
span.pull-right.btn-group.quick-buttons
a(tooltip="Connect this interface", xo-click="connectPIF(PIF.id)", ng-if = 'canAdmin()')
i.fa.fa-link.fa-lg
a(tooltip="Remove this interface", xo-click="removePIF(PIF.id)", ng-if = 'canAdmin()')
i.fa.fa-trash-o.fa-lg
.text-right
button.btn(type="button", ng-class = '{"btn-success": creatingNetwork, "btn-primary": !creatingNetwork}', ng-click="creatingNetwork = !creatingNetwork", ng-hide = '!canAdmin()', ng-disabled = '!canAdmin()')
i.fa.fa-plus(ng-if = '!creatingNetwork')
i.fa.fa-minus(ng-if = 'creatingNetwork')
| Create Network
br
form.form-inline.text-right#createNetworkForm(ng-if = 'creatingNetwork', name = 'createNetworkForm', ng-submit = 'createNetwork(newNetworkName, newNetworkDescription, newNetworkPIF, newNetworkMTU, newNetworkVlan)')
fieldset(ng-disabled = 'createNetworkWaiting || !canAdmin()')
.form-group
label(for = 'newNetworkPIF') Interface&nbsp;
select.form-control(ng-model = 'newNetworkPIF', ng-change = 'updateMTU(newNetworkPIF)', ng-options='(PIF | resolve).device for PIF in host.$PIFs')
option(value = '', disabled) None
| &nbsp;
.form-group
label.control-label(for = 'newNetworkName') Name&nbsp;
input#newNetworkName.form-control(type = 'text', ng-model = 'newNetworkName', required)
| &nbsp;
.form-group
label.control-label(for = 'newNetworkDescription') Description&nbsp;
input#newNetworkDescription.form-control(type = 'text', ng-model = 'newNetworkDescription', placeholder= 'Network created with Xen Orchestra')
| &nbsp;
.form-group
label.control-label(for = 'newNetworkVlan') VLAN&nbsp;
input#newNetworkVlan.form-control(type = 'text', ng-model = 'newNetworkVlan', placeholder = 'Defaut: no VLAN')
| &nbsp;
.form-group
label(for = 'newNetworkMTU') MTU&nbsp;
input#newNetworkMTU.form-control(type = 'text', ng-model = 'newNetworkMTU', placeholder = 'Default: 1500')
| &nbsp;
.form-group
button.btn.btn-primary(type = 'submit')
i.fa.fa-plus-square
| Create
span(ng-if = 'createNetworkWaiting')
| &nbsp;
i.xo-icon-loading-sm
br
//- CPU and Logs panels
.grid-sm
//- Task panel
.panel.panel-default
.panel-heading.panel-title(ng-if="tasks | isNotEmpty")
i.fa.fa-spinner.fa-pulse
| Pending tasks
.panel-heading.panel-title(ng-if="tasks | isEmpty")
i.fa.fa-spinner
| Pending tasks
.panel-body
p.center(ng-if="tasks | isEmpty") No recent tasks
table.table.table-hover(ng-if="tasks | isNotEmpty")
th Date
th Progress
th Name
//- TODO: working reverse order, from recent to oldest
tr(ng-repeat="task in tasks | map | orderBy:'created':true track by task.id")
td.oneliner {{task.created * 1e3 | date:'medium'}}
td
.progress-condensed
.progress-bar.progress-bar-success.progress-bar-striped.active.progress-bar-black(role="progressbar", aria-valuemin="0", aria-valuenow="{{task.progress*100}}", aria-valuemax="100", style="width: {{task.progress*100}}%", tooltip="Progress: {{task.progress*100 | number:1}}%")
| {{task.progress*100 | number:1}}%
td.oneliner
| {{task.name_label}}
span.pull-right.btn-group.quick-buttons(ng-if = 'canAdmin()')
a(xo-click="cancelTask(task.id)")
i.fa.fa-times.fa-lg(tooltip="Cancel this task")
a(xo-click="destroyTask(task.id)")
i.fa.fa-trash-o.fa-lg(tooltip="Destroy this task")
//- Logs panel
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-comments
| Logs
span.quick-edit(ng-if="(host.messages | isNotEmpty) && canAdmin()", tooltip="Remove all logs", ng-click="deleteAllLog()")
i.fa.fa-trash-o.fa-fw
.panel-body
p.center(ng-if="host.messages | isEmpty") No recent logs
table.table.table-hover(ng-if="host.messages | isNotEmpty")
th Date
th Name
tr(ng-repeat="message in host.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
td {{message.time*1e3 | date:"medium"}}
td
| {{message.name}}
span.pull-right.btn-group.quick-buttons(ng-if = 'canAdmin()')
a(xo-click="deleteLog(message.id)")
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
.center(ng-if = '(host.messages | count) > 5 || currentLogPage > 1')
pagination(boundary-links="true", total-items="host.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.grid-sm
//- Patches panel
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-file-code-o
| Patches
span.quick-edit(ng-click="listMissingPatches(host.id)", tooltip="Check for updates")
i.fa.fa-question-circle
span.quick-edit(ng-click="installAllPatches(host.id)", tooltip="Install all the missing patches", style="margin-right:5px", ng-if = 'canAdmin()')
i.fa.fa-download
.panel-body
table.table.table-hover(ng-if="poolPatches || updates")
th.col-sm-2 Name
th.col-sm-5 Description
th.col-sm-3 Applied/Released date
th.col-sm-1 Size
th.col-sm-1 Status
tr(
ng-repeat="patch in updates"
ng-if="!isPoolPatch(patch)"
)
td.oneliner {{patch.name}}
td.oneliner
a(href="{{patch.documentationUrl}}", target="_blank") {{patch.description}}
td.oneliner {{patch.date | date:"medium"}}
td -
td
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to install the patch on this host", ng-if = 'canAdmin()')
span.label.label-danger Missing
span.label.label-danger(ng-if = '!canAdmin()') Missing
tr(ng-repeat="patch in poolPatches | map | orderBy:'-name'| slice:(5*(currentPatchPage-1)):(5*currentPatchPage)")
td.oneliner {{patch.name}}
td.oneliner {{patch.description}}
//- TODO: use a proper function for patch date, like poolPatchToHostPatch
td.oneliner {{((patch.$host_patches[0]) | resolve).time*1e3 | date:"medium"}}
td {{patch.size | bytesToSize}}
td
span(ng-if="isPoolPatchApplied(patch)")
span.label.label-success Applied
span(ng-if="!isPoolPatchApplied(patch)")
span(ng-click="installPatch(host.id, patch.uuid)", tooltip="Click to apply the patch on this host", ng-if = 'canAdmin()')
span.label.label-warning Not applied
span.label.label-warning(ng-if = '!canAdmin()') Not applied
.center(ng-if = '(poolPatches | count) > 5 || currentPatchPage > 1')
pagination(boundary-links="true", total-items="poolPatches | count", ng-model="$parent.currentPatchPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-plug
| PCI Devices
.panel-body
p.center(ng-if="!host.$PCIs") No PCI devices available
table.table.table-hover(ng-if="host.$PCIs")
th PCI Info
th Device Name
tr(ng-repeat="pci in host.$PCIs | resolve | orderBy:'pci_id' | slice:(5*(currentPCIPage-1)):(5*currentPCIPage) track by pci.id")
td.oneliner {{pci.pci_id}} ({{pci.class_name}})
td.oneliner {{pci.device_name}}
.center(ng-if = '(host.$PCIs | resolve).length > 5')
pagination(boundary-links="true", total-items="(host.$PCIs | resolve).length", ng-model="$parent.currentPCIPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-desktop
| GPUs
.panel-body
p.center(ng-if="host.$PGPUs.length === 0") No GPUs available
table.table.table-hover(ng-if="host.$PGPUs.length !== 0")
th Device
tr(ng-repeat="pgpu in host.$PGPUs | resolve | orderBy:'device' | slice:(5*(currentGPUPage-1)):(5*currentGPUPage) track by pgpu.id")
td.oneliner {{pgpu.device}}
.center(ng-if = '(host.$PGPUs | resolve).length > 5')
pagination(boundary-links="true", total-items="(host.$PGPUs | resolve).length", ng-model="$parent.currentGPUPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,145 +0,0 @@
nav.navbar.navbar-inverse.navbar-fixed-top(role = 'navigation')
//- Brand and toggle get grouped for better mobile display
.navbar-header
//- Button used to (un)collapse on mobile display.
button.navbar-toggle(type="button", ng-init="collapsed = true", ng-click="collapsed = !collapsed")
span.sr-only Toggle navigation
span.icon-bar
span.icon-bar
span.icon-bar
//- Brand name.
a.navbar-brand(ui-sref = 'index')
img.navbar-logo(src="images/logo.png")
| Xen Orchestra
//- All navbar items are collapsed on mobile display.
.collapse.navbar-collapse(ng-class="!collapsed && 'in'")
//- Search form of the navbar.
form.navbar-form.navbar-left(role="search", style="width: 250px")
//- Forced width due to issue with `input`s (https://github.com/twbs/bootstrap/issues/9950.
.input-group
input.form-control.inverse(
type = 'text'
placeholder = ''
ng-model = '$root.listFilter'
ng-change = 'ensureListView($root.listFilter)'
)
span.input-group-btn
button.btn.btn-search(
type = 'button'
ng-click = 'ensureListView($root.listFilter)'
)
i.fa.fa-search
//- /Search form.
ul.nav.navbar-nav
li
a(href="https://xen-orchestra.com/#/pricing?pk_campaign=xoa_source", target="_blank", tooltip="Source version without Pro support. Use in production at your own risk.")
i.xo-icon-info.text-danger
span.hidden-sm No Pro Support!
//- Right items of the navbar.
ul.nav.navbar-nav.navbar-right
li.navbar-text(ng-if="'disconnected' === navbar.status")
i.xo-icon-error
| Disconnected from XO-Server
li.navbar-text(ng-if="'connecting' === navbar.status")
i.fa.fa-refresh.fa-spin
| Connecting to XO-Server
//- Running tasks
li.disabled(ng-if="!navbar.tasks.size", tooltip="No running tasks")
a.dropdown-toggle.inverse
i.xo-icon-task
li.dropdown(dropdown, ng-if="navbar.tasks.size")
a.dropdown-toggle.inverse(dropdown-toggle)
i.xo-icon-task
ul.dropdown-menu.inverse
li.task-menu(
ng-repeat="task in navbar.tasks.all | orderBy:natural('name_label') track by task.id"
)
a(
ui-sref="hosts_view({id: task.$host})"
tooltip = "{{task.name_label}}"
)
//- i.fa.fa-spinner.fa-fw
//- | {{task.name_label}}
.progress-condensed
.progress-bar.progress-bar-success.progress-bar-striped.active.progress-bar-black(
role = "progressbar"
aria-valuemin = "0"
aria-valuenow = "{{task.progress*100}}"
aria-valuemax = "100"
style = "width: {{task.progress*100}}%"
)
| {{task.progress*100 | number:1}}%
//- Main menu.
li.dropdown(dropdown)
a.dropdown-toggle.inverse(dropdown-toggle)
i.fa.fa-th
ul.dropdown-menu.inverse
li(
ui-sref-active = 'active'
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
)
a(ui-sref = 'tree')
i.fa.fa-indent
| Tree view
li(ui-sref-active="active")
a(ui-sref="list")
i.fa.fa-align-justify
| Flat view
li(
ui-sref-active="active"
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
)
a(ui-sref="dashboard.index")
i.fa.fa-dashboard
| Dashboard
li.divider
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
a(ui-sref = 'self.index')
i.fa.fa-cloud
| Self Service
li.divider
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
a(ui-sref = 'backup.index')
i.fa.fa-archive
| Backup
li(ng-class = '{ disabled: navbar.user.permission !== "admin" }')
a(ui-sref = 'taskscheduler.index')
i.fa.fa-cogs
| Job Manager
li.divider
li(
ui-sref-active = 'active'
ng-class = '{ disabled: navbar.user.permission !== "admin" }'
)
a(ui-sref="settings.index")
i.fa.fa-cog
| Settings
li.divider
li(ui-sref-active="active")
a(ui-sref="about")
i.fa.fa-info-circle(style="color:#5bc0de")
| About
//- /Main menu.
li
a(ui-sref="settings.update")
i.fa.fa-question-circle.text-warning(ng-if = '!navbar.updater.state', tooltip = 'No update information available')
i.fa.fa-question-circle.text-info(ng-if = 'navbar.updater.state == "connected"', tooltip = 'Update information may be available')
i.fa.fa-check.text-success(ng-if = 'navbar.updater.state == "upToDate"', tooltip = 'Your XOA is up-to-date')
i.fa.fa-bell.text-primary(ng-if = 'navbar.updater.state == "upgradeNeeded"', tooltip = 'You need to update your XOA (new version is available)')
i.fa.fa-bell-slash.text-warning(ng-if = 'navbar.updater.state == "registerNeeded"', tooltip = 'Your XOA is not registered for updates')
i.fa.fa-exclamation-triangle.text-danger(ng-if = 'navbar.updater.state == "error"', tooltip = 'Can\'t fetch update information')
li
a(ng-if = '!navbar.user.provider', ui-sref="{{navbar.user.provider ? 'settings.users' : 'settings.user'}}", tooltip="{{navbar.user.email}}")
i.fa.fa-user
span.hidden-sm {{navbar.user.email}}
li
a(ng-click = 'navbar.logOut()')
i.fa.fa-sign-out
| &nbsp;
| &nbsp;
//- /Right items.
//- /Navbar items.
//- /Navbar.

View File

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

View File

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

View File

@@ -1,503 +0,0 @@
angular = require 'angular'
cloneDeep = require 'lodash.clonedeep'
filter = require 'lodash.filter'
forEach = require 'lodash.foreach'
trim = require 'lodash.trim'
includes = require 'lodash.includes'
forEach = require 'lodash.foreach'
#=====================================================================
module.exports = angular.module 'xoWebApp.newVm', [
require 'angular-ui-router'
]
.config ($stateProvider) ->
$stateProvider.state 'VMs_new',
url: '/vms/new/:container'
controller: 'NewVmsCtrl as ctrl'
template: require './view'
.controller 'NewVmsCtrl', (
$scope, $stateParams, $state
xoApi, xo
bytesToSizeFilter, sizeToBytesFilter
notify
) ->
$scope.min = Math.min
user = xoApi.user
$scope.isAdmin = user.permission == 'admin'
userGroups = user.groups
if !$scope.isAdmin
$scope.resourceSets = []
$scope.userResourceSets = []
$scope.resourceSet = ''
xo.resourceSet.getAll()
.then (sets) ->
$scope.resourceSets = sets
$scope.resourceSet = $scope.resourceSets[0]
$scope.updateResourceSet($scope.resourceSet)
$scope.updateResourceSet = (resourceSet) ->
$scope.resourceSet = resourceSet
$scope.template = ''
$scope.templates = []
$scope.writable_SRs = []
$scope.ISO_SRs = []
srs = []
$scope.resourceSetNetworks = []
$scope.pools = []
forEach $scope.resourceSet.objects, (id) ->
obj = xoApi.get id
if obj.type is 'VM-template'
$scope.templates.push(obj)
else if obj.type is 'SR'
srs.push(obj)
else if obj.type is 'network'
$scope.resourceSetNetworks.push(obj)
$scope.writable_SRs = filter(srs, (sr) => sr.content_type isnt 'iso')
$scope.ISO_SRs = filter(srs, (sr) => sr.content_type is 'iso')
$scope.multipleVmsActive = false
$scope.vmsNames = ['VM1', 'VM2']
$scope.numberOfVms = 1
$scope.newNumberOfVms = 2
$scope.checkNumberOfVms = ->
if $scope.newNumberOfVms && Number.isInteger($scope.newNumberOfVms)
$scope.newNumberOfVms = $scope.numberOfVms = Math.min(100,Math.max(2,$scope.newNumberOfVms))
else
$scope.newNumberOfVms = $scope.numberOfVms = 2
$scope.refreshNames = ->
$scope.defaultName = 'VM'
$scope.defaultName = $scope.name_label if $scope.name_label
forEach($scope.vmsNames, (name, index) ->
$scope.vmsNames[index] = $scope.defaultName + (index+1)
)
$scope.toggleBootAfterCreate = ->
$scope.bootAfterCreate = false if $scope.multipleVmsActive
$scope.configDriveActive = false
existingDisks = {}
$scope.saveChange = (position, propertyName, value) ->
if not existingDisks[position]?
existingDisks[position] = {}
existingDisks[position][propertyName] = value
$scope.updateVdiSize = (position) ->
$scope.saveChange(position, 'size', bytesToSizeFilter(sizeToBytesFilter($scope.existingDiskSizeValues[position] + ' ' + $scope.existingDiskSizeUnits[position])))
$scope.updateTotalDiskBytes()
$scope.initExistingValues = (template) ->
$scope.name_label = template.name_label
sizes = {}
$scope.existingDiskSizeValues = {}
$scope.existingDiskSizeUnits = {}
forEach xoApi.get(template.$VBDs), (VBD) ->
sizes[VBD.position] = bytesToSizeFilter xoApi.get(VBD.VDI).size
$scope.existingDiskSizeValues[VBD.position] = +sizes[VBD.position].split(' ')[0]
$scope.existingDiskSizeUnits[VBD.position] = sizes[VBD.position].split(' ')[1]
$scope.VIFs.length = 0
if template.VIFs.length
forEach xoApi.get(template.VIFs), (VIF) ->
network = xoApi.get(VIF.$network)
$scope.addVIF(network)
return
else $scope.addVIF()
$scope.memory = template.memory.size
{get} = xoApi
removeItems = do ->
splice = Array::splice.call.bind Array::splice
(array, index, n) -> splice array, index, n ? 1
merge = do ->
push = Array::push.apply.bind Array::push
(args...) ->
result = []
for arg in args
push result, arg if arg?
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) ->
$scope.container = container
# If the container was not found, no need to continue.
return unless container?
if container.type is 'host'
host = container
pool = (get container.$poolId) ? {}
else
host = {}
pool = container
default_SR = get pool.default_SR
default_SR = if default_SR then default_SR.id else ''
)
$scope.availableMethods = {}
$scope.CPUs = ''
$scope.pv_args = ''
$scope.installation_cdrom = ''
$scope.installation_method = ''
$scope.installation_network = ''
$scope.memory = null
$scope.memoryValue = null
$scope.units = ['MiB', 'GiB', 'TiB']
$scope.memoryUnit = $scope.units[1]
$scope.name_description = 'Created by XO'
$scope.name_label = ''
$scope.template = ''
$scope.totalDiskBytes = 0
$scope.firstSR = ''
$scope.VDIs = []
$scope.VIFs = []
$scope.isDiskTemplate = false
$scope.cloudConfigSshKey = ''
$scope.cloudConfigCustom = '#cloud-config\n#hostname: myhostname\n#ssh_authorized_keys:\n# - ssh-rsa <myKey>\n#packages:\n# - htop\n'
$scope.cloudConfigLoading = false
$scope.cloudConfigError = false
$scope.bootAfterCreate = true
$scope.updateMemory = ->
if $scope.memoryValue
$scope.memory = sizeToBytesFilter $scope.memoryValue + ' ' + $scope.memoryUnit
else
$scope.memory = $scope.template.memory.size
$scope.updateMemoryUnit = (memoryUnit) ->
$scope.memoryUnit = memoryUnit
$scope.updateMemory()
$scope.updateTotalDiskBytes = ->
$scope.totalDiskBytes = 0
forEach $scope.existingDiskSizeValues, (value, key) ->
$scope.totalDiskBytes += sizeToBytesFilter value + ' ' + $scope.existingDiskSizeUnits[key]
forEach $scope.VDIs, (VDI) ->
$scope.totalDiskBytes += (sizeToBytesFilter VDI.sizeValue + ' ' + VDI.sizeUnit) || 0
$scope.addVIF = do ->
id = 0
(network = '') ->
$scope.VIFs.push {
id: id++
network
}
$scope.removeVIF = (index) -> removeItems $scope.VIFs, index
$scope.moveVDI = (index, direction) ->
{VDIs} = $scope
newIndex = index + direction
[VDIs[index], VDIs[newIndex]] = [VDIs[newIndex], VDIs[index]]
$scope.removeVDI = (index) ->
removeItems $scope.VDIs, index
$scope.updateTotalDiskBytes()
VDI_id = 0
$scope.addVDI = ->
$scope.VDIs.push {
id: VDI_id++
bootable: false
size: ''
sizeValue: ''
sizeUnit: $scope.units[1]
SR: default_SR || $scope.writable_SRs[0] && $scope.writable_SRs[0].id
type: 'system'
}
# When the selected template changes, updates other variables.
$scope.$watch 'template', (template) ->
return unless template
# After each template change, initialize coreOsCloudConfig to empty
$scope.coreOsCloudConfig = ''
# Fetch the PV args
$scope.pv_args = template.PV_args
{install_methods} = template.template_info
availableMethods = $scope.availableMethods = Object.create null
for method in install_methods
availableMethods[method] = true
if install_methods.length is 1 # FIXME: does not work with network.
$scope.installation_method = install_methods[0]
else
delete $scope.installation_method
delete $scope.installation_method
delete $scope.installation_network
# if the template already have a configured install repository
installRepository = template.template_info.install_repository
if installRepository
if installRepository is 'cdrom'
$scope.installation_method = 'cdrom'
else
$scope.installation_network = template.template_info.install_repository
$scope.installation_method = 'network'
VDIs = $scope.VDIs = cloneDeep template.template_info.disks
# if the template has no config disk
# nor it's Other install media (specific case)
if VDIs.length is 0 and template.name_label isnt 'Other install media'
$scope.isDiskTemplate = true
else $scope.isDiskTemplate = false
for VDI in VDIs
VDI.id = VDI_id++
VDI.SR or= default_SR || $scope.writable_SRs[0] && $scope.writable_SRs[0].id
VDI.size = bytesToSizeFilter VDI.size
VDI.sizeValue = if VDI.size then +VDI.size.split(' ')[0] else null
VDI.sizeUnit = VDI.size.split(' ')[1]
# if the template is labeled CoreOS
# we'll use config drive setup
if template.name_label == 'CoreOS'
return xo.vm.getCloudInitConfig template.id
.then (result) ->
$scope.coreOsCloudConfig = result
$scope.updateTotalDiskBytes()
$scope.uploadCloudConfig = (file) ->
$scope.cloudConfigError = false
return unless file
reader = new FileReader()
reader.onerror = () ->
$scope.cloudConfigError = true
reader.onload = (event) ->
$scope.cloudConfigCustom = event.target.result
reader.onloadend = (event) ->
$scope.cloudConfigLoading = false
if file.size > 2e6
reader.onerror()
return
$scope.cloudConfigLoading = true
reader.readAsText(file)
$scope.createVMs = ->
if !$scope.multipleVmsActive
$scope.createVM($scope.name_label)
return
forEach($scope.vmsNames, (name) ->
$scope.createVM(name)
)
# Send the client on the tree view
$state.go 'index'
xenDefaultWeight = 256
$scope.weightMap = {
'Quarter (1/4)': xenDefaultWeight / 4,
'Half (1/2)': xenDefaultWeight / 2,
'Normal': xenDefaultWeight,
'Double (x2)': xenDefaultWeight * 2
}
$scope.createVM = (name_label) ->
{
resourceSet
CPUs
cpuWeight
pv_args
installation_cdrom
installation_method
installation_network
memoryValue
memoryUnit
name_description
template
VDIs
VIFs
} = $scope
forEach VDIs, (vdi) ->
vdi.size = bytesToSizeFilter(sizeToBytesFilter(vdi.sizeValue + ' ' + vdi.sizeUnit))
# Does not edit the displayed data directly.
VDIs = cloneDeep VDIs
for VDI, index in VDIs
# store the first VDI's SR for later use (e.g: coreOsCloudConfig)
if VDI.id == 0
$scope.firstSR = VDI.SR or default_SR
# Removes the dummy identifier used for AngularJS.
delete VDI.id
# Adds the device number based on the index.
VDI.device = "#{index}"
# TODO: handles invalid values.
# Does not edit the displayed data directly.
VIFs = cloneDeep VIFs
for VIF in VIFs
# Removes the dummy identifier used for AngularJS.
delete VIF.id
# xo-server expects a network id, not the whole object
VIF.network = VIF.network.id
# Removes the mac address if empty.
if 'mac' of VIF
VIF.mac = trim(VIF.mac)
delete VIF.mac unless VIF.mac
if installation_method is 'cdrom'
installation = {
method: 'cdrom'
repository: installation_cdrom
}
else if installation_network
matches = /^(http|ftp|nfs)/i.exec installation_network
throw new Error 'invalid network URL' unless matches
installation = {
method: matches[1].toLowerCase()
repository: installation_network
}
else if installation_method is 'pxe'
installation = {
method: 'network'
repository: 'pxe'
}
else
installation = undefined
data = {
resourceSet: resourceSet && resourceSet.id
installation
pv_args
name_label
template: template.id
VDIs
VIFs
existingDisks
}
# TODO:
# - disable the form during creation
# - indicate the progress of the operation
notify.info {
title: 'VM creation'
message: 'VM creation started'
}
$scope.creatingVM = true
id = null
xoApi.call('vm.create', data)
.then (id_) ->
id = id_
# If nothing to sets, just stops.
return unless CPUs or name_description or memoryValue
data = {
id
}
data.CPUs = +CPUs if CPUs
if cpuWeight
data.cpuWeight = cpuWeight
if name_description
data.name_description = name_description
if pv_args
data.pv_args = pv_args
if memoryValue
# FIXME: handles invalid entries.
data.memory = memoryValue + ' ' + memoryUnit
return xo.vm.set(data)
.then () ->
# If a CloudConfig drive needs to be created
if $scope.coreOsCloudConfig
# Use the CoreOS specific Cloud Config creation
return xo.vm.createCloudInitConfigDrive(id, $scope.firstSR, $scope.coreOsCloudConfig, true).then ->
return xo.docker.register(id)
if $scope.configDriveActive
# User creation is less universal...
# $scope.cloudContent = '#cloud-config\nhostname: ' + name_label + '\nusers:\n - name: olivier\n sudo: ALL=(ALL) NOPASSWD:ALL\n groups: sudo\n shell: /bin/bash\n ssh_authorized_keys:\n - ' + $scope.cloudConfigSshKey + '\n'
# So keep it basic for now: hostname and ssh key
hostname = name_label
# Remove leading and trailing spaces.
.replace(/^\s+|\s+$/g, '')
# Replace spaces with '-'.
.replace(/\s+/g, '-')
if $scope.configDriveMethod == 'standard'
$scope.cloudContent = '#cloud-config\nhostname: ' + hostname + '\nssh_authorized_keys:\n - ' + $scope.cloudConfigSshKey + '\n'
else
$scope.cloudContent = $scope.cloudConfigCustom
# The first SR for a template with an existing disk
$scope.firstSR = (get (get template.$VBDs[0]).VDI).$SR
# Use the generic CloudConfig creation
return xo.vm.createCloudInitConfigDrive(id, $scope.firstSR, $scope.cloudContent).then ->
# Boot directly on disk
return xo.vm.setBootOrder({vm: id, order: 'c'})
.then () ->
if $scope.bootAfterCreate
xo.vm.start id
if !$scope.multipleVmsActive
if resourceSet
# FIXME When using self service, ACL permissions are not updated fast enough to access VM view right after creation
$state.go 'index'
else
# Send the client on the VM view
$state.go 'VMs_view', { id }
.catch (error) ->
notify.error {
title: 'VM creation'
message: 'The creation failed'
}
$scope.creatingVM = false
console.log error
# A module exports its name.
.name

View File

@@ -1,474 +0,0 @@
.grid
.panel.panel-default
.col-sm-4
p.page-title.col-sm-4
i.xo-icon-vm
| Create VM
span(ng-if='isAdmin') on
a(ng-if="'pool' === container.type", ui-sref="pools_view({id: container.id})")
| {{container.name_label}}
a(ng-if="'host' === container.type", ui-sref="hosts_view({id: container.id})")
| {{container.name_label}}
form.col-sm-4.form-horizontal(ng-if="resourceSet")
.form-group(style="margin-top:4px;margin-bottom:4px;")
label.col-sm-5.control-label Resource set:
.col-sm-7(ng-if='resourceSets.length > 1')
select.form-control(
style="max-width:20em;"
ng-model="$parent.resourceSet"
ng-options="resourceSet.name for resourceSet in resourceSets | orderBy:natural('name') track by resourceSet.id"
ng-change="updateResourceSet(resourceSet)"
required=""
)
.col-sm-7.form-control-static(ng-if='resourceSets.length === 1')
| {{ resourceSet.name }}
//- Add server panel
form.form-horizontal(ng-submit="createVMs()")
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-info-circle
| VM info
.panel-body
.form-group
label.col-sm-3.control-label Template
.col-sm-9
select.form-control(ng-model="template", ng-options="template.name_label for template in templates | orderBy:natural('name_label') track by template.id", required="", ng-change = 'initExistingValues(template)')
.form-group
label.col-sm-3.control-label Name
.col-sm-9
input.form-control(type="text", placeholder="Name of your new VM", ng-required="!multipleVmsActive", ng-model="name_label")
.form-group
label.col-sm-3.control-label Description
.col-sm-9
input.form-control(type="text", placeholder="Optional description of you new VM", ng-model="name_description")
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-dashboard
| Performances
.panel-body
.form-group
label.col-sm-3.control-label vCPUs
.col-sm-9
input.form-control(type="text", placeholder="{{template.CPUs.number}}", ng-model="CPUs")
.form-group
label.col-sm-3.control-label CPU Weight
.col-sm-9
select.form-control(ng-model = "cpuWeight", ng-options='value as key for (key, value) in weightMap track by value')
option(value = '') default
.form-group
label.col-sm-3.control-label RAM
.col-sm-9
.input-group
input.form-control(type='number' min="0" step="0.01" placeholder="{{ template.memory.size | bytesConvert:memoryUnit:'iB' }}" ng-model="memoryValue" ng-change="updateMemory()")
span.input-group-btn.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle)
| {{ memoryUnit }}&nbsp;
span.caret
ul.dropdown-menu(role = 'menu' style='min-width:0')
li(ng-repeat="memoryUnit in units")
a(ng-click="updateMemoryUnit(memoryUnit)") {{ memoryUnit }}
.grid
//- Cloud Config Panel, only for templates with existing disks
.panel.panel-default(ng-if="isDiskTemplate")
.panel-heading.panel-title
i.fa.fa-cloud
| Config Drive
span.pull-right
label(style = 'cursor: pointer;')
input.hidden(type = 'checkbox', ng-model = '$parent.configDriveActive', ng-click = '$parent.configDriveMethod = "standard"')
i.fa(ng-class = '{"fa-toggle-on": $parent.configDriveActive, "text-success": $parent.configDriveActive, "fa-toggle-off": !$parent.configDriveActive}', style = 'font-size: 1.5em;')
.panel-body
fieldset(ng-disabled = '!$parent.configDriveActive')
.form-group
label.col-sm-3.control-label SSH Key
.col-sm-9
.input-group
span.input-group-addon
input(
type = 'radio'
name = 'configDriveMethod'
ng-model = '$parent.configDriveMethod'
value = 'standard'
)
input.form-control(
type="text"
placeholder="ssh-rsa AAAA.... you@machine"
ng-model="$parent.cloudConfigSshKey"
ng-disabled = '$parent.configDriveMethod !== "standard"'
name="cloudConfigSshKey"
required
)
.form-group
label.col-sm-3.control-label
a(href='http://cloudinit.readthedocs.org/en/latest/topics/examples.html', target='_blank') Custom config
.col-sm-9
.input-group
span.input-group-addon
input(
type = 'radio'
name = 'configDriveMethod'
ng-model = '$parent.configDriveMethod'
value = 'custom'
)
textarea.form-control(
rows='4'
style="resize: none;"
ng-model="$parent.cloudConfigCustom"
ng-disabled = '$parent.configDriveMethod !== "custom"'
name="cloudConfigCustom"
required
)
br
button.btn.btn-default(
type = 'button'
ng-disabled = '$parent.configDriveMethod !== "custom"'
ng-file-select = '$parent.uploadCloudConfig($files[0]); fileName = $files[0].name'
) Select file
span(style='max-width: 1em' ng-init='fileName = "None"')
| Selected file : {{ fileName }}
i.fa.fa-spinner.fa-spin(ng-show = 'cloudConfigLoading')
i.fa.fa-exclamation-triangle.text-danger(ng-show = 'cloudConfigError' tooltip = 'Error while loading file')
//- Install panel, only if an installation method is needed
.panel.panel-default(ng-if="!isDiskTemplate")
.panel-heading.panel-title
i.fa.fa-download
| Install settings
.panel-body
.form-group(ng-show="availableMethods.cdrom")
label.col-sm-3.control-label ISO/DVD
.col-sm-9
.input-group
span.input-group-addon
input(
type = 'radio'
name = 'installation_method'
ng-model = '$parent.installation_method'
value = 'cdrom'
)
select.form-control.disabled(
ng-disabled="'cdrom' !== installation_method"
ng-model="$parent.installation_cdrom"
)
option(value = '') Please select
optgroup(ng-repeat="SR in ISO_SRs | orderBy:natural('name_label') track by SR.id", ng-if="SR.VDIs.length", label="{{SR.name_label}}")
option(ng-repeat="VDI in SR.VDIs | resolve | orderBy:natural('name_label') track by VDI.id", ng-value="VDI.id")
| {{VDI.name_label}}
.form-group(
ng-show = '(availableMethods.http || availableMethods.ftp || availableMethods.nfs)'
)
label.col-sm-3.control-label Network
.col-sm-9
.input-group
span.input-group-addon
input(
type = 'radio'
name = 'installation_method'
ng-model = '$parent.installation_method'
value = 'network'
)
input.form-control(type="text", ng-disabled="'network' !== installation_method", placeholder="e.g: http://ftp.debian.org/debian", ng-model="$parent.installation_network")
.form-group(ng-show = 'template.virtualizationMode === "hvm"')
label.col-sm-3.control-label PXE
.col-sm-9
input(
type = 'radio'
name = 'installation_method'
ng-model = '$parent.installation_method'
value = 'pxe'
)
.form-group(ng-show='template.virtualizationMode === "pv"')
label.col-sm-3.control-label PV Args
.col-sm-9
input.form-control(type="text", placeholder="{{template.PV_args}}", ng-model="$parent.pv_args")
//- Interface panel
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-network
| Interfaces
.panel-body
table.table.table-hover
tr
th MAC
th Network
th.col-md-1 &#160;
//- Buttons
tr(ng-repeat="VIF in VIFs track by VIF.id")
td
input.form-control(type="text", ng-model="VIF.mac", ng-pattern="/^\s*[0-9a-f]{2}(:[0-9a-f]{2}){5}\s*$/i", placeholder="Auto-generated if empty")
td
select.form-control(
ng-options = 'network as (network.name_label + " (" + (network.$pool | resolve).name_label + ")") for network in (networks || resourceSetNetworks) | orderBy:natural("name_label") track by network.id'
ng-model = 'VIF.network'
required
)
option(value = '') Please select
td
.pull-right
button.btn.btn-default(type="button", ng-click="removeVIF($index)", title="Remove this interface")
i.fa.fa-times
.btn-form
p.center
.btn-form
p.center
button.btn.btn-success(type="button", ng-click="addVIF()")
i.fa.fa-plus
| Add interface
//- end of misc and interface panel
//- Cloud config panel
.grid(ng-if = 'coreOsCloudConfig')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cloud
| Cloud config
.pull-right.small
button.btn.btn-default(type = 'button', ng-click = 'isExpanded = !isExpanded'): i.fa(ng-class = '{"fa-plus": !isExpanded, "fa-minus": isExpanded}')
.panel-body
textarea.form-control(rows="20", collapse= '!isExpanded', ng-model='$parent.coreOsCloudConfig', name='coreOsCloudConfig')
| {{coreOsCloudConfig}}
//- Multiple VMs panel
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-clone
| Multiple VMs
span.pull-right
label(style = 'cursor: pointer;')
input.hidden(type = 'checkbox', ng-model = 'multipleVmsActive', ng-click='refreshNames(); checkNumberOfVms()', ng-change="toggleBootAfterCreate()")
i.fa(ng-class = '{"fa-toggle-on": multipleVmsActive, "text-success": multipleVmsActive, "fa-toggle-off": !multipleVmsActive}', style = 'font-size: 1.5em;')
.panel-body(ng-if="multipleVmsActive")
.form-group
label.col-md-offset-4.col-sm-2.control-label
i.fa.fa-refresh(ng-click = "$parent.refreshNames()" tooltip="Set VMs to default names")
| Number of VMs
.col-sm-2
.input-group(style="width:10em")
input.form-control(type="number" ng-model="$parent.newNumberOfVms")
span.input-group-btn
button.btn.btn-default(type="button" ng-click="checkNumberOfVms()")
i.fa.fa-arrow-right
.col-sm-6(ng-repeat="offset in [0, 1]")
.form-group(
ng-repeat = "n in [].constructor($parent.numberOfVms).slice(0, ($parent.numberOfVms+1)/2) track by $index"
ng-if = "2*$index+offset < $parent.numberOfVms"
)
label.col-sm-2.control-label VM \#{{ 2*$index+1+offset }}
.col-sm-10
input.form-control(
type = "text"
required
placeholder = "Name of new VM \#{{ (2*$index+1+offset) }}"
ng-model = "$parent.vmsNames[2*$index+offset]"
ng-init = "$parent.vmsNames[2*$index+offset] = $parent.defaultName + (2*$index+1+offset)"
)
//- Disk panel
.grid
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-disk
| Disks
.panel-body
table.table.table-hover
tr
th.col-md-2 SR
th.col-md-1 Bootable?
th.col-md-2 Size
th.col-md-2 Name
th.col-md-3 Description
th.col-md-2 &#160;
//- Buttons
tr(ng-repeat="VBD in (template.$VBDs | resolve) track by VBD.id", ng-if="isDiskTemplate")
td
select.form-control(ng-model="(VBD.VDI | resolve).$SR", ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.physical_usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))", ng-change = 'saveChange(VBD.position, "$SR", (VBD.VDI | resolve).$SR)', required)
option(value = '') Please select
td.text-center
i.fa.fa-check(ng-if = 'VBD.bootable')
td(style = "overflow: visible")
.input-group
input.form-control(
type='number'
min="0"
step="0.01"
placeholder="Size of this virtual disk"
ng-model="existingDiskSizeValues[VBD.position]"
ng-readonly='!configDriveActive'
ng-change = 'updateVdiSize(VBD.position)'
required
)
span.input-group-btn.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle ng-disabled='!configDriveActive')
| {{ existingDiskSizeUnits[VBD.position] }}&nbsp
span.caret
ul.dropdown-menu(role="menu" style="min-width:0")
li(ng-repeat="unit in $parent.units")
a(ng-click="existingDiskSizeUnits[VBD.position] = unit; updateVdiSize(VBD.position)") {{ unit }}
td
input.form-control(type="text", placeholder="Name of this virtual disk", ng-model="(VBD.VDI | resolve).name_label", ng-change = 'saveChange(VBD.position, "name_label", (VBD.VDI | resolve).name_label)')
td
input.form-control(type="text", placeholder="Description of this virtual disk", ng-model="(VBD.VDI | resolve).name_description", ng-change = 'saveChange(VBD.position, "name_description", (VBD.VDI | resolve).name_description)')
td
tr(ng-repeat="VDI in VDIs track by VDI.id")
td
select.form-control(ng-model="VDI.SR", ng-options="SR.id as (SR.name_label + ' (' + (SR.size - SR.physical_usage | bytesToSize) + ' free)') for SR in (writable_SRs | orderBy:natural('name_label'))")
td.text-center
input(type="checkbox", ng-model="VDI.bootable")
td(style = "overflow: visible")
.input-group
input.form-control(
type='number'
min="0"
step="0.01"
placeholder="Size of this virtual disk"
ng-model="VDI.sizeValue"
ng-change = 'updateTotalDiskBytes()'
required
)
span.input-group-btn.dropdown(dropdown)
button.btn.btn-default.dropdown-toggle(type = 'button' dropdown-toggle)
| {{ VDI.sizeUnit }}&nbsp
span.caret
ul.dropdown-menu(role="menu" style="min-width:0")
li(ng-repeat="unit in units")
a(ng-click="VDI.sizeUnit = unit; updateTotalDiskBytes()") {{ unit }}
td
input.form-control(type="text", placeholder="Name of this virtual disk", ng-model="VDI.name_label")
td
input.form-control(type="text", placeholder="Description of this virtual disk", ng-model="VDI.name_description")
td
.btn-group
button.btn.btn-default(type="button", ng-click="moveVDI($index, -1)", ng-disabled="$first", title="Move this disk up")
i.fa.fa-chevron-up
button.btn.btn-default(type="button", ng-click="moveVDI($index, 1)", ng-disabled="$last", title="Move this disk down")
i.fa.fa-chevron-down
.pull-right
button.btn.btn-default(type="button", ng-click="removeVDI($index)", title="Remove this disk")
i.fa.fa-times
.btn-form
p.center
.btn-form
p.center
button.btn.btn-success(type="button", ng-click="addVDI()")
i.fa.fa-plus
| Add disk
//- Confirmation panel
.grid
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flag-checkered
| Summary
.panel-body
.grid
.grid-cell
p.center.big
span(ng-if="!multipleVmsActive") {{name_label}}
span(ng-if="multipleVmsActive") {{numberOfVms}} new VMs
| &nbsp;
span.small(ng-if="template.name_label") ({{template.name_label}})
.grid
.grid-cell
//- p.stat-name vCPUs
p.center.big(tooltip="vCPUs")
| {{CPUs || template.CPUs.number || 0}}x&nbsp;
i.xo-icon-cpu
.grid-cell
//- p.stat-name RAM
p.center.big(tooltip="RAM")
span(ng-if="memoryValue") {{memory | bytesToSize}}
span(ng-if="!memoryValue") {{template.memory.size | bytesToSize}}
| &nbsp;
i.xo-icon-memory
.grid-cell
//- p.stat-name Disks
p.center.big(tooltip="Disks")
| {{(VDIs.length) || (template.$VBDs.length) || 0}}x&nbsp;
i.xo-icon-disk
.grid-cell
//- p.stat-name Interfaces
p.center.big(tooltip="Network interfaces")
| {{(VIFs.length) || (template.VIFs.length) || 0}}x&nbsp;
i.xo-icon-network
.grid(ng-if="template && resourceSet")
.grid-cell
.center-block(ng-if="resourceSet.limits.cpus" style="width:60%")
.progress
.progress-bar(
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{resourceSet.limits.cpus.total - resourceSet.limits.cpus.available}}",
aria-valuemax="{{resourceSet.limits.cpus.total}}",
style="width: {{[resourceSet.limits.cpus.total - resourceSet.limits.cpus.available, resourceSet.limits.cpus.total] | percentage}}",
tooltip="{{resourceSet.limits.cpus.total - resourceSet.limits.cpus.available}} vCPUs already in use"
)
.progress-bar(
ng-class = '{"progress-bar-success": numberOfVms * (CPUs || template.CPUs.number || 0) <= resourceSet.limits.cpus.available, "progress-bar-danger": numberOfVms * (CPUs || template.CPUs.number || 0) > resourceSet.limits.cpus.available}'
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{numberOfVms * (CPUs || template.CPUs.number || 0)}}",
aria-valuemax="{{resourceSet.limits.cpus.total}}",
style="width: {{[min(numberOfVms * (CPUs || template.CPUs.number || 0), resourceSet.limits.cpus.available), resourceSet.limits.cpus.total] | percentage}}",
tooltip="{{numberOfVms * (CPUs || template.CPUs.number || 0)}} vCPUs / {{resourceSet.limits.cpus.available}} remaining"
)
.grid-cell
.center-block(ng-if="resourceSet.limits.memory" style="width:60%")
.progress
.progress-bar(
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{resourceSet.limits.memory.total - resourceSet.limits.memory.available}}",
aria-valuemax="{{resourceSet.limits.memory.total}}",
style="width: {{[resourceSet.limits.memory.total - resourceSet.limits.memory.available, resourceSet.limits.memory.total] | percentage}}",
tooltip="{{resourceSet.limits.memory.total - resourceSet.limits.memory.available | bytesToSize}} already in use"
)
.progress-bar(
ng-class = '{"progress-bar-success": numberOfVms * memory <= resourceSet.limits.memory.available, "progress-bar-danger": numberOfVms * memory > resourceSet.limits.memory.available}'
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{numberOfVms * memory}}",
aria-valuemax="{{resourceSet.limits.memory.total}}",
style="width: {{[min(numberOfVms * memory, resourceSet.limits.memory.available), resourceSet.limits.memory.total] | percentage}}",
tooltip="{{numberOfVms * memory | bytesToSize}} / {{resourceSet.limits.memory.available | bytesToSize}} remaining"
)
.grid-cell
.center-block(ng-if="resourceSet.limits.disk" style="width:60%")
.progress
.progress-bar(
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{resourceSet.limits.disk.total - resourceSet.limits.disk.available}}",
aria-valuemax="{{resourceSet.limits.disk.total}}",
style="width: {{[resourceSet.limits.disk.total - resourceSet.limits.disk.available, resourceSet.limits.disk.total] | percentage}}",
tooltip="{{resourceSet.limits.disk.total - resourceSet.limits.disk.available | bytesToSize}} already in use"
)
.progress-bar(
ng-class = '{"progress-bar-success": numberOfVms * totalDiskBytes <= resourceSet.limits.disk.available, "progress-bar-danger": numberOfVms * totalDiskBytes > resourceSet.limits.disk.available}'
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{numberOfVms * totalDiskBytes}}",
aria-valuemax="{{resourceSet.limits.disk.total}}",
style="width: {{[min(numberOfVms * totalDiskBytes, resourceSet.limits.disk.available), resourceSet.limits.disk.total] | percentage}}",
tooltip="{{numberOfVms * totalDiskBytes | bytesToSize}} / {{resourceSet.limits.disk.available | bytesToSize}} remaining"
)
.grid-cell
p.center(ng-if="isDiskTemplate")
| Cloud configuration is&nbsp;
strong.text-success(ng-if = 'configDriveActive') enabled.
strong.text-danger(ng-if = '!configDriveActive') disabled.
p.center
label
input(type='checkbox', ng-model = 'bootAfterCreate')
span(ng-if='!multipleVmsActive') Boot VM after creation
span(ng-if='multipleVmsActive') Boot {{numberOfVms}} VMs after creation
p.center
button.btn.btn-lg.btn-primary(
type="submit"
ng-disabled = `creatingVM ||
resourceSet.limits.cpus && (CPUs || template.CPUs.number || 0) > resourceSet.limits.cpus.available ||
resourceSet.limits.memory && memory > resourceSet.limits.memory.available ||
resourceSet.limits.disk && totalDiskBytes > resourceSet.limits.disk.available`
)
i.fa.fa-play(ng-if = '!creatingVM')
i.fa.fa-circle-o-notch.fa-spin(ng-if = 'creatingVM')
| Create VM

View File

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

View File

@@ -1,164 +0,0 @@
//- TODO: lots of stuff.
.grid-sm
.panel.panel-default
p.page-title
i.xo-icon-pool
| {{pool.name_label}}
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| General
span.quick-edit(tooltip="Edit General settings", ng-click="poolSettings.$show()")
i.fa.fa-edit.fa-fw
.panel-body
form(editable-form="", name="poolSettings", onbeforesave="savePool($data)")
dl.dl-horizontal
dt Name
dd
span(editable-text="pool.name_label", e-name="name_label", e-form="poolSettings")
| {{pool.name_label}}
dt Description
dd
span(editable-text="pool.name_description", e-name="name_description", e-form="poolSettings")
| {{pool.name_description}}
dt Master
dd(ng-repeat="master in [pool.master] | resolve")
a(ui-sref="hosts_view({id: master.id})")
| {{master.name_label}}
dt Tags
dd
xo-tag(ng-if = 'pool', object = 'pool')
dt(ng-if="pool.default_SR") Default SR
dd(ng-if="pool.default_SR", ng-init="default_SR = (pool.default_SR | resolve)")
a(ui-sref="SRs_view({id: default_SR.id})") {{default_SR.name_label}}
dt HA
dd
| {{pool.HA_enabled}}
dt UUID
dd {{pool.UUID}}
.btn-form(ng-show="poolSettings.$visible")
p.center
button.btn.btn-default(type="button", ng-disabled="poolSettings.$waiting", ng-click="poolSettings.$cancel()")
i.fa.fa-times
| Cancel
| &nbsp;
button.btn.btn-primary(type="submit", ng-disabled="poolSettings.$waiting")
i.fa.fa-save
| Save
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-stats
| Stats
.row
.col-xs-6
p.stat-name Hosts:
p.center.big-stat {{hosts | count}}
.col-xs-6
p.stat-name Running:
p.center.big-stat {{runningHosts | count}}
//- Action panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-flash
| Actions
.panel-body
.grid-cell.text-center
.grid
.grid-cell.btn-group
button.btn(tooltip="Add SR", tooltip-placement="top", type="button", style="width: 90%", disabled)
i.xo-icon-sr.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Add VM", tooltip-placement="top", type="button", style="width: 90%", xo-sref="VMs_new({container: pool.id})")
i.xo-icon-vm.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Patch the pool", tooltip-placement="top", type="button", style="width: 90%", ng-file-select = "patchPool($files, pool.id)")
i.fa.fa-file-code-o.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Add Host", tooltip-placement="top", type="button", style="width: 90%")
i.xo-icon-host.fa-2x.fa-fw
.grid-cell.btn-group
button.btn(tooltip="Disconnect", tooltip-placement="top", type="button", style="width: 90%; margin-bottom: 0.5em")
i.fa.fa-unlink.fa-2x.fa-fw
//- Hosts panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-host
| Hosts
.panel-body
table.table.table-hover.table-condensed
th Name
th.col-md-4 Description
th.col-md-6 Memory
tr(xo-sref="hosts_view({id: host.id})", ng-repeat="host in hosts | map | orderBy:natural('name_label') track by host.id")
td.oneliner {{host.name_label}}
td.oneliner {{host.name_description}}
td
.progress-condensed
.progress-bar(
role="progressbar",
aria-valuemin="0",
aria-valuenow="{{host.memory.usage}}",
aria-valuemax="{{host.memory.size}}",
style="width: {{[host.memory.usage, host.memory.size] | percentage}}",
tooltip="RAM: {{host.memory.usage | bytesToSize}}/{{host.memory.size | bytesToSize}} ({{[host.memory.usage, host.memory.size] | percentage}})"
)
//- Shared SR panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.xo-icon-sr
| Shared SR
.panel-body
table.table.table-hover
th Name
th Description
th Type
th Size
th.col-md-4 Physical/Allocated usage
th.col-md-1 Action
tr(
ng-repeat="SR in srs | map | orderBy:natural('name_label') track by SR.id"
xo-sref="SRs_view({id: SR.id})"
)
td.oneliner
| {{SR.name_label}}&nbsp;
span.label.label-primary(ng-if="SR.id === pool.default_SR") Default SR
td.oneliner {{SR.name_description}}
td {{SR.SR_type}}
td {{SR.size | bytesToSize}}
td
.progress-condensed
.progress-bar(role="progressbar", aria-valuemin="0", aria-valuenow="{{SR.physical_usage}}", aria-valuemax="{{SR.size}}", style="width: {{[SR.physical_usage, SR.size] | percentage}}", tooltip="Physical usage: {{[SR.physical_usage, SR.size] | percentage}}")
td
span.pull-right.btn-group.quick-buttons
a(ng-if="SR.id !== pool.default_SR", xo-click="setDefaultSr(SR.id)")
i.fa.fa-hdd-o.fa-lg(tooltip="Set as default SR")
//- Logs panel
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-comments
| Logs
span.quick-edit(ng-if="pool.messages | isNotEmpty", tooltip="Remove all logs", xo-click="deleteAllLog()")
i.fa.fa-trash-o.fa-fw
.panel-body
p.center(ng-if="pool.messages | isEmpty") No recent logs
table.table.table-hover(ng-if="pool.messages | isNotEmpty")
th Date
th Name
tr(ng-repeat="message in pool.messages | map | orderBy:'-time' | slice:(5*(currentLogPage-1)):(5*currentLogPage) track by message.id")
td {{message.time*1e3 | date:"medium"}}
td
| {{message.name}}
span.pull-right.btn-group.quick-buttons
a(xo-click="deleteLog(message.id)")
i.fa.fa-trash-o.fa-lg(tooltip="Remove this log entry")
.center(ng-if = '(pool.messages | count) > 5 || currentLogPage > 1')
pagination(boundary-links="true", total-items="pool.messages | count", ng-model="$parent.currentLogPage", items-per-page="5", max-size="5", class="pagination-sm", previous-text="<", next-text=">", first-text="<<", last-text=">>")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,148 +0,0 @@
import angular from 'angular'
import forEach from 'lodash.foreach'
import includes from 'lodash.includes'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import 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, $rootScope, $interval, $filter, servers, xoApi, xo, notify) {
const orderBy = $filter('orderBy')
this.servers = orderBy(servers, $rootScope.natural('host'))
$scope.readOnly = {}
forEach(this.servers, (server) => {
$scope.readOnly[server.id] = Boolean(server.readOnly)
})
const selected = this.selectedServers = {}
const newServers = this.newServers = []
const refreshServers = () => {
xo.server.getAll().then(servers => {
this.servers = orderBy(servers, $rootScope.natural('host'))
})
}
const refreshServersIfUnfocused = () => {
if (!$scope.isFocused) {
refreshServers()
}
}
const interval = $interval(refreshServersIfUnfocused, 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 addresses = []
forEach(xoApi.getView('host').all, host => addresses.push(host.address))
const newServers = this.newServers
const servers = this.servers
const updateServers = []
for (let i = 0, len = servers.length; i < len; i++) {
const server = servers[i]
const {id} = server
if (selected[id]) {
delete selected[id]
xo.server.remove(id)
} else {
if (!server.password) {
delete server.password
}
server.readOnly = $scope.readOnly[id]
xo.server.set(server)
delete server.password
updateServers.push(server)
}
}
for (let i = 0, len = newServers.length; i < len; i++) {
const server = newServers[i]
const {host, username, password, readOnly} = server
if (!host) {
continue
}
if (includes(addresses, host)) {
notify.warning({
title: 'Server already connected',
message: `You are already connected to ${host}`
})
continue
}
xo.server.add({
host,
username,
password,
readOnly,
autoConnect: false
}).then(function (id) {
server.id = id
$scope.readOnly[id] = Boolean(readOnly)
xo.server.connect(id).catch(error => {
notify.error({
title: 'Server connection error',
message: error.message
})
})
})
delete server.password
updateServers.push(server)
}
this.servers = updateServers
this.newServers.length = 0
this.addServer()
}
})
.name

View File

@@ -1,98 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-cloud(style="color: #e25440;")
| Servers
.grid-sm
.panel.panel-default
form(ng-submit="ctrl.saveServers()", autocomplete="off").panel-body
table.table.table-hover
tr
th.col-md-5 Host
th.col-md-2 User
th.col-md-2 Password
th.col-md-1.text.center Actions
th.col-md-1.text.center Read only
th.col-md-1.text-center
i.fa.fa-trash-o.fa-lg(tooltip="Forget server")
tr(ng-repeat="server in ctrl.servers track by server.id")
td
.input-group
span.input-group-addon.hidden-xs(ng-if="server.status === 'connected'")
i.xo-icon-success.fa-lg(tooltip="Connected")
span.input-group-addon.hidden-xs(ng-if="server.status === 'disconnected'")
i.xo-icon-failure.fa-lg(tooltip="Disconnected")
span.input-group-addon.hidden-xs(ng-if="server.status === 'connecting'")
i.fa.fa-cog.fa-lg.fa-spin(tooltip="Connecting...")
input.form-control(
type="text",
ng-model="server.host",
ng-focus="$parent.isFocused = true",
ng-blur="$parent.isFocused = false"
)
td
input.form-control(
type="text",
ng-model="server.username",
ng-focus="$parent.isFocused = true",
ng-blur="$parent.isFocused = false"
)
td
input.form-control(
type="password",
ng-model="server.password",
placeholder="Fill to change the password",
ng-focus="$parent.isFocused = true",
ng-blur="$parent.isFocused = false"
)
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="readOnly[server.id]")
td.text-center
input(type="checkbox", ng-model="ctrl.selectedServers[server.id]")
tr(ng-repeat="server in ctrl.newServers")
td
input.form-control(
type = "text"
ng-model = "server.host"
placeholder = "address[:port]"
)
td
input.form-control(
type = "text"
ng-model = "server.username"
ng-required = "server.host"
placeholder = "user"
)
td
input.form-control(
type="password"
ng-model="server.password"
ng-required = "server.host"
placeholder="password"
)
td &#160;
td.text-center
input( type="checkbox", ng-model="server.readOnly")
td &#160;
p.text-center
button.btn.btn-primary(type="submit")
i.fa.fa-save
| Save
| &nbsp;
button.btn.btn-success(type="button", ng-click="ctrl.addServer()")
i.fa.fa-plus

View File

@@ -1,99 +0,0 @@
import angular from 'angular'
import uiRouter from 'angular-ui-router'
import _assign from 'lodash.assign'
import ansiUp from 'ansi_up'
import updater from '../../updater'
import xoApi from 'xo-api'
import xoServices from 'xo-services'
import {AuthenticationFailed} from '../../updater'
import view from './view'
export default angular.module('settings.update', [
uiRouter,
updater,
xoApi,
xoServices
])
.config(function ($stateProvider) {
$stateProvider.state('settings.update', {
controller: 'SettingsUpdate as ctrl',
url: '/update',
onExit: updater => {
updater.removeAllListeners('end')
},
template: view
})
})
.filter('ansitohtml', function ($sce) {
return function (input) {
return $sce.trustAsHtml(ansiUp.ansi_to_html(input))
}
})
.controller('SettingsUpdate', function (xoApi, xo, updater, notify) {
this.updater = updater
this.updater.isRegistered()
.then(() => this.updater.on('end', () => this.updater.isRegistered()))
.catch(err => console.error(err))
this.updater.getConfiguration()
.then(configuration => this.configuration = _assign({}, configuration))
.then(() => this.withAuth = Boolean(this.configuration.proxyUser))
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message
}))
this.registerXoa = (email, password, renewRegister) => {
this.regPwd = ''
this.updater.register(email, password, renewRegister)
.tap(() => this.renewRegister = false)
.then(() => this.updater.update())
.catch(AuthenticationFailed, () => {})
.catch(err => console.error(err))
}
this.update = () => {
this.updater.update()
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message
}))
}
this.upgrade = () => {
this.updater.upgrade()
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message
}))
}
this.configure = (host, port, username, password) => {
const config = {}
if (!this.withAuth) {
username = null
password = null
}
config.proxyHost = host && host.trim() || null
config.proxyPort = port && port.trim() || null
config.proxyUser = username || null
config.proxyPassword = password || null
return this.updater.configure(config)
.then(configuration => this.configuration = _assign({}, configuration))
.then(() => this.withAuth = Boolean(this.configuration.proxyUser))
.catch(error => notify.error({
title: 'XOA Updater',
message: error.message
}))
.finally(() => this.update())
}
this.valid = trial => {
return trial && trial.end && Date.now() < trial.end
}
})
.name

View File

@@ -1,128 +0,0 @@
.grid-sm
.panel.panel-default
p.page-title
i.fa.fa-refresh(style="color: #e25440;")
| Update
.grid-sm
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-globe
| Status
.panel-body
p(ng-if = '!ctrl.updater.state')
a.btn.btn-warning: i.fa.fa-question-circle(ng-if = '!ctrl.updater.state', tooltip = 'No update information available')
| &nbsp;No update information available&nbsp;
a.btn.btn-default(ng-class = '{disabled: ctrl.updater.isConnected}', ng-click = 'ctrl.update()')
i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.isConnected}')
.form-group(ng-if = 'ctrl.updater.state && ctrl.updater.state === "registerNeeded"')
a.btn.btn-warning(ng-if = 'ctrl.updater.state === "registerNeeded"'): i.fa.fa-bell-slash(tooltip = 'Your XOA is not registered for updates')
| &nbsp;Registration needed&nbsp;
button.btn.btn-default(ng-if = 'ctrl.updater.registerState === "registered"', ng-click = 'ctrl.updater.update()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}'): i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.updating || ctrl.updater.upgrading}')
.form-group(ng-if = 'ctrl.updater.state && ctrl.updater.state !== "registerNeeded"')
a.btn.btn-info(ng-if = 'ctrl.updater.state === "connected"'): i.fa.fa-question-circle(tooltip = 'Update information may be available')
a.btn.btn-success(ng-if = 'ctrl.updater.state === "upToDate"'): i.fa.fa-check(tooltip = 'Your XOA is up-to-date')
a.btn.btn-primary(ng-if = 'ctrl.updater.state === "upgradeNeeded"'): i.fa.fa-bell(tooltip = 'You need to update your XOA (new version is available)')
a.btn.btn-danger(ng-if = 'ctrl.updater.state === "error"'): i.fa.fa-exclamation-triangle(tooltip = 'Can\'t fetch update information')
| &nbsp;
button#update.btn.btn-info(type = 'button', ng-click = 'ctrl.update()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}')
| Check for updates&nbsp;
i.fa.fa-refresh(ng-class = '{"fa-spin": ctrl.updater.updating}')
| &nbsp;
button#upgrade.btn.btn-primary(ng-if = 'ctrl.updater.state === "upgradeNeeded"', type = 'button', ng-click = 'ctrl.upgrade()', ng-class = '{disabled: ctrl.updater.updating || ctrl.updater.upgrading}')
| Upgrade&nbsp;
i.fa.fa-cog(ng-class = '{"fa-spin": ctrl.updater.upgrading}')
div
p(ng-repeat = 'entry in ctrl.updater._log')
span(ng-class = '{"text-danger": entry.level === "error", "text-muted": entry.level === "info", "text-warning": entry.level === "warning", "text-success": entry.level === "success"}') {{ entry.date }}
| &nbsp;:&nbsp;
span(style = 'word-wrap: break-word;', ng-bind-html = 'entry.message | ansitohtml')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-pencil
| Registration
.panel-body.text-center
.text-warning(ng-if = '!ctrl.updater.state || ctrl.updater.registerState === "unknown"')
| No registration information available.
br
span.big-stat
i.fa.fa-exclamation-triangle.text-warning
div(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "error"')
.text-danger Can't fetch registration information.
br
span.big-stat
i.fa.fa-exclamation-triangle.text-danger
br
.text-danger {{ ctrl.updater.registerError }}
br
button.btn.btn-default(type = 'button', ng-click = 'ctrl.updater.isRegistered()')
i.fa.fa-refresh
| Refresh
form(ng-if = 'ctrl.updater.state && (ctrl.renewRegister || ctrl.updater.registerState === "unregistered")', ng-submit = 'ctrl.registerXoa(ctrl.regEmail, ctrl.regPwd, ctrl.renewRegister)')
p.form-static-control(ng-if = '!ctrl.renewRegister') XOA is not registered.
p.form-static-control(ng-if = 'ctrl.renewRegister')
| Forget previous registration ?&nbsp;
button.btn.btn-default(type = 'button', ng-click = 'ctrl.renewRegister = false') Cancel
p.small Your xen-orchestra.com email and password
.form-group
.input-group
span.input-group-addon: i.fa.fa-envelope-o.fa-fw
label.sr-only(for = 'regEmail') Email
input#regEmail.form-control(type = 'email', placeholder = 'Email', ng-model = 'ctrl.regEmail', required)
.form-group
.input-group
span.input-group-addon: i.fa.fa-key.fa-fw
label.sr-only(for = 'regPwd') Email
input#regPwd.form-control(type = 'password', placeholder = 'Password', ng-model = 'ctrl.regPwd', required)
.form-group
button.btn.btn-primary(type = 'submit')
i.fa.fa-check
| Register
p.form-static-control.text-danger {{ ctrl.updater.registerError }}
p(ng-if = 'ctrl.updater.state && ctrl.updater.registerState === "registered" && !ctrl.renewRegister')
| Your Xen Orchestra appliance is registered to
span.text-success {{ ctrl.updater.token.registrationEmail }}
| .
br
br
i.fa.fa-check-circle.fa-3x.text-success
br
br
button.btn.btn-default(type = 'button', ng-click = 'ctrl.renewRegister = true') Register to someone else ?
.grid-sm(ng-if = 'ctrl.updater.state && ctrl.configuration')
.panel.panel-default
.panel-heading.panel-title
i.fa.fa-cogs
| Settings
.panel-body
form(ng-submit = 'ctrl.configure(ctrl.configuration.proxyHost, ctrl.configuration.proxyPort, ctrl.configuration.proxyUser, ctrl.configuration.proxyPassword)')
h4
i.fa.fa-globe
| Proxy settings
p
| If you need a proxy to access the Internet&ensp;
label
input(type = 'checkbox', ng-model = 'ctrl.withAuth')
| with authentication
fieldset.form-inline
.form-group
//- label.control-label Host:&nbsp;
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyHost', placeholder = 'Host (myproxy.example.org)')
| &nbsp;
.form-group
//- label.control-label Port:&nbsp;
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyPort', placeholder = 'Port (3128 ?...)')
br
div(ng-hide = '!ctrl.withAuth')
fieldset.form-inline(ng-disabled = '!ctrl.withAuth')
.form-group
input.form-control(type = 'text', ng-model = 'ctrl.configuration.proxyUser', placeholder = 'User name', required)
| &nbsp;
.form-group
input.form-control(type = 'password', ng-model = 'ctrl.configuration.proxyPassword', placeholder = 'Password')
br
fieldset
.form-group
button.btn.btn-primary(type = 'submit')
i.fa.fa-floppy-o
| Save

View File

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

View File

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

View File

@@ -1,132 +0,0 @@
import angular from 'angular'
import passwordGenerator from 'password-generator'
import uiRouter from 'angular-ui-router'
import uiSelect from 'angular-ui-select'
import uiEvent from 'angular-ui-event'
import 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
}
this.generatePassword = (user) => {
// Generate password of 8 letters/numbers/underscore
user.password = passwordGenerator(8, false)
}
})
.name

View File

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

View File

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

View File

@@ -1,263 +0,0 @@
import angular from 'angular'
import escapeRegExp from 'lodash.escaperegexp'
import filter from 'lodash.filter'
import forEach from 'lodash.foreach'
import isEmpty from 'lodash.isempty'
import trim from 'lodash.trim'
import uiRouter from 'angular-ui-router'
import Bluebird from 'bluebird'
import xoTag from 'tag'
import view from './view'
// ===================================================================
export default angular.module('xoWebApp.sr', [
uiRouter,
xoTag
])
.config(function ($stateProvider) {
$stateProvider.state('SRs_view', {
url: '/srs/:id',
controller: 'SrCtrl',
template: view
})
})
.filter('vdiFilter', (xoApi, filterFilter) => {
return (input, search) => {
search && (search = trim(search).toLowerCase())
return filter(input, vdi => {
let vbd, vm
let vmName = vdi.$VBDs && vdi.$VBDs[0] && (vbd = xoApi.get(vdi.$VBDs[0])) && (vm = xoApi.get(vbd.VM)) && vm.name_label
vmName && (vmName = vmName.toLowerCase())
return !search || (vmName && (vmName.search(escapeRegExp(search)) !== -1) || filterFilter([vdi], search).length)
})
}
})
.controller('SrCtrl', function ($scope, $stateParams, $state, $q, notify, xoApi, xo, modal, $window, bytesToSizeFilter, sizeToBytesFilter) {
$window.bytesToSize = bytesToSizeFilter // FIXME dirty workaround to custom a Chart.js tooltip template
$scope.units = ['MiB', 'GiB', 'TiB']
$scope.currentLogPage = 1
$scope.currentVDIPage = 1
let {get} = xoApi
$scope.$watch(() => xoApi.get($stateParams.id), function (SR) {
const VDIs = []
if (SR) {
forEach(SR.VDIs, vdi => {
vdi = xoApi.get(vdi)
if (vdi) {
const size = bytesToSizeFilter(vdi.size)
VDIs.push({...vdi, size, sizeValue: size.split(' ')[0], sizeUnit: size.split(' ')[1]})
}
})
}
$scope.SR = SR
$scope.VDIs = VDIs
})
$scope.selectedForDelete = {}
$scope.deleteSelectedVdis = function () {
return modal.confirm({
title: 'VDI deletion',
message: 'Are you sure you want to delete all selected VDIs? This operation is irreversible.'
}).then(function () {
forEach($scope.selectedForDelete, (selected, id) => selected && xo.vdi.delete(id))
$scope.selectedForDelete = {}
})
}
$scope.saveSR = function ($data) {
let {SR} = $scope
let {name_label, name_description} = $data
$data = {
id: SR.id
}
if (name_label !== SR.name_label) {
$data.name_label = name_label
}
if (name_description !== SR.name_description) {
$data.name_description = name_description
}
return xoApi.call('sr.set', $data)
}
$scope.deleteVDI = function (id) {
console.log('Delete VDI', id)
return modal.confirm({
title: 'VDI deletion',
message: 'Are you sure you want to delete this VDI? This operation is irreversible.'
}).then(function () {
return xo.vdi.delete(id)
})
}
$scope.disconnectVBD = function (id) {
console.log('Disconnect VBD', id)
return modal.confirm({
title: 'VDI disconnection',
message: 'Are you sure you want to disconnect this VDI?'
}).then(function () {
return xoApi.call('vbd.disconnect', {id: id})
})
}
$scope.connectPBD = function (id) {
console.log('Connect PBD', id)
return xoApi.call('pbd.connect', {id: id})
}
$scope.disconnectPBD = function (id) {
console.log('Disconnect PBD', id)
return xoApi.call('pbd.disconnect', {id: id})
}
$scope.reconnectAllHosts = function () {
// TODO: return a Bluebird.all(promises).
for (let id of $scope.SR.$PBDs) {
let pbd = xoApi.get(id)
xoApi.call('pbd.connect', {id: pbd.id})
}
}
$scope.disconnectAllHosts = function () {
return modal.confirm({
title: 'Disconnect hosts',
message: 'Are you sure you want to disconnect all hosts to this SR?'
}).then(function () {
for (let id of $scope.SR.$PBDs) {
let pbd = xoApi.get(id)
xoApi.call('pbd.disconnect', {id: pbd.id})
console.log(pbd.id)
}
})
}
$scope.rescanSr = function (id) {
console.log('Rescan SR', id)
return xoApi.call('sr.scan', {id: id})
}
$scope.removeSR = function (id) {
console.log('Remove SR', id)
return modal.confirm({
title: 'SR deletion',
message: 'Are you sure you want to delete this SR? This operation is irreversible.'
}).then(function () {
return Bluebird.map($scope.SR.$PBDs, pbdId => {
let pbd = xoApi.get(pbdId)
return xoApi.call('pbd.disconnect', { id: pbd.id })
})
}).then(function () {
return xoApi.call('sr.destroy', {id: id})
}).then(function () {
$state.go('index')
notify.info({
title: 'SR remove',
message: 'SR is removed'
})
})
}
$scope.forgetSR = function (id) {
console.log('Forget SR', id)
return modal.confirm({
title: 'SR forget',
message: 'Are you sure you want to forget this SR? No VDI on this SR will be removed.'
}).then(function () {
return Bluebird.map($scope.SR.$PBDs, pbdId => {
let pbd = xoApi.get(pbdId)
return xoApi.call('pbd.disconnect', { id: pbd.id })
})
}).then(function () {
return xoApi.call('sr.forget', {id: id})
}).then(function () {
$state.go('index')
notify.info({
title: 'SR forget',
message: 'SR is forgotten'
})
})
}
$scope.saveDisks = function (data) {
// Group data by disk.
let disks = {}
let sizeChanges = false
forEach(data, function (value, key) {
let i = key.indexOf('/')
let id = key.slice(0, i)
let prop = key.slice(i + 1)
;(disks[id] || (disks[id] = {}))[prop] = value
})
forEach(disks, function (attributes, id) {
let disk = get(id)
attributes.size = bytesToSizeFilter(sizeToBytesFilter(attributes.sizeValue + ' ' + attributes.sizeUnit))
if (attributes.size !== bytesToSizeFilter(disk.size)) { // /!\ attributes are provided by a modified copy of disk
sizeChanges = true
return false
}
})
let promises = []
const preCheck = sizeChanges ? modal.confirm({
title: 'Disk resizing',
message: 'Growing the size of a disk is not reversible'
}) : $q.resolve()
return preCheck
.then(() => {
forEach(disks, function (attributes, id) {
let disk = get(id)
// Resize disks
attributes.size = bytesToSizeFilter(sizeToBytesFilter(attributes.sizeValue + ' ' + attributes.sizeUnit))
if (attributes.size !== bytesToSizeFilter(disk.size)) { // /!\ attributes are provided by a modified copy of disk
promises.push(xo.disk.resize(id, attributes.size))
}
delete attributes.size
// Keep only changed attributes.
forEach(attributes, function (value, name) {
if (value === disk[name]) {
delete attributes[name]
}
})
if (!isEmpty(attributes)) {
// Inject id.
attributes.id = id
// Ask the server to update the object.
promises.push(xoApi.call('vdi.set', attributes))
}
})
return $q.all(promises)
})
}
})
// A module exports its name.
.name

View File

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

View File

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

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