Compare commits

...

136 Commits

Author SHA1 Message Date
Julien Fontanet
93c7a01e62 5.1.1 2016-07-27 15:31:00 +02:00
Julien Fontanet
9c2359e8ee fix(Tooltip): better PropTypes 2016-07-27 15:29:05 +02:00
Julien Fontanet
5b9000012e fix(Tooltip): behave if children is missing or a string 2016-07-27 15:17:02 +02:00
Julien Fontanet
bf00b4e8e3 fix(Tooltip): better PropTypes 2016-07-27 15:08:52 +02:00
ABHAMON Ronan
ee7787f4ae fix(heatmap): related to last Tooltip changes (#1327)
Fixes #1326
2016-07-27 10:10:55 +02:00
Olivier Lambert
0b88e743c9 feat(changelog): update changelog 2016-07-26 17:07:23 +02:00
Julien Fontanet
f07a947580 5.1.0 2016-07-26 16:54:35 +02:00
Julien Fontanet
0b8a9eedbc feat(tooltip): float → solid, do not follow cursor 2016-07-26 16:50:46 +02:00
ABHAMON Ronan
8d24e596ac fix(tooltip): use position.top instead of position.right (#1322) 2016-07-26 14:49:07 +02:00
ABHAMON Ronan
c2378a44cd fix(tooltip): do not inject an intermediary element (#1321)
Fixes #1150
2016-07-26 14:28:11 +02:00
ABHAMON Ronan
023f7fdef1 feat(home): custom filters & configure default filters (#1308)
Fixes #1235
2016-07-25 15:20:39 +02:00
ABHAMON Ronan
5d7a64bc28 fix(scheduling): timezone support (#1318) 2016-07-25 14:57:38 +02:00
ABHAMON Ronan
8661957a97 feat(timezone-picker): xo-server timezone in the select (#1316)
Fixes #1314
2016-07-25 13:21:37 +02:00
ABHAMON Ronan
7a15d265b7 fix(new/sr): fix IQNs, LUNs selection (#1317)
Fixes #1281
2016-07-25 13:04:05 +02:00
Olivier Lambert
2736881975 fix(new sr): cast port number. See issue #1281 2016-07-23 16:42:58 +02:00
Greenkeeper
44a85f4e0c chore(package): update globby to version 6.0.0 (#1313)
https://greenkeeper.io/
2016-07-23 16:41:41 +02:00
Julien Fontanet
52a6e42e7e fix(pool/storage): display read-only SRs 2016-07-23 16:26:41 +02:00
Julien Fontanet
3dbe058d4e feat(home): add link to VMs console 2016-07-23 15:58:12 +02:00
Pierre Donias
620139efc1 feat(settings/acls): (un)select all objects of a specific type (#1310)
Fixes #1296
2016-07-22 17:45:38 +02:00
Pierre Donias
71464ac2e3 feat(menu): add types as Home sub-menus (#1309)
Fixes #1306
2016-07-22 16:18:16 +02:00
Pierre Donias
4a65489d39 fix(xo): polyfill Intl for Safari (#1307)
Fixes #1120
2016-07-22 15:51:32 +02:00
Pierre Donias
65d7eac590 feat(user): SSH keys management (#1302)
Fix #1299
2016-07-21 12:21:27 +02:00
ABHAMON Ronan
02bbc01dc4 feat(scheduling): improve utilisability (#1300)
Fixes #1295
2016-07-21 10:25:57 +02:00
Pierre Donias
3066237c86 feat(self/admin): recompute resource sets limits (#1298)
Fixes #1287
2016-07-20 11:36:49 +02:00
Pierre Donias
53f3c0bef1 fix(new-vm): fix CPU weight and add CPU cap (#1297)
Fixes #1286
2016-07-20 10:41:50 +02:00
ABHAMON Ronan
823c91b457 feat(plugins): supports predefined configurations (#1294)
Fixes #1289
2016-07-20 09:46:30 +02:00
ABHAMON Ronan
3bd7e20411 feat(backups): jobs support timezones (#1290)
Fixes #1258
2016-07-20 09:45:35 +02:00
Pierre Donias
24d4610b04 feat(vm/tab-advanced): editable CPU weight and cap (#1293)
Fixes #1283
2016-07-20 09:44:24 +02:00
ABHAMON Ronan
b16097767a feat(json-schema-input): use only schema.defaults in combobox options (#1292)
Fix #1288
2016-07-19 15:06:33 +02:00
ABHAMON Ronan
2ff74ffd39 feat(line-chart): many fixes on graphs legends (#1291)
Fixes #1247
2016-07-19 13:39:53 +02:00
Julien Fontanet
f0bb464136 fix(intl/locales/zh): fix moment import 2016-07-19 10:51:56 +02:00
Julien Fontanet
4767830386 feat(i18n): skeleton for Chinese 2016-07-19 10:02:33 +02:00
Julien Fontanet
ce23d4f164 feat(editable): change cursor to make it easier to see 2016-07-19 09:40:29 +02:00
Pierre Donias
c1380d1256 feat(home): focus search input after changing type (#1285)
Fixes #1228
2016-07-18 17:51:47 +02:00
Pierre Donias
ed9a848858 feat(new-vm): create mutiple VMs with a name pattern (#1271)
Implements parts of #949: initial sequence number.
2016-07-18 14:42:18 +02:00
ABHAMON Ronan
5e4e15fc12 fix(self/overview): display correctly resources set (#1284)
Fixes #1282
2016-07-18 09:36:46 +02:00
Greenkeeper
0dea952a2a chore(package): update modular-css to version 0.23.2 (#1239)
https://greenkeeper.io/
2016-07-15 12:19:47 +02:00
Olivier Lambert
a1818dd525 5.0.9 2016-07-14 14:49:18 +02:00
Pierre Donias
659e336f66 fix(migrate-vms-modal): fix conditions for SR and Network selectors 2016-07-14 14:43:29 +02:00
Pierre Donias
058f7ecd9f feat(Usage): new component to display a usage progress bar
Fixes #1151
2016-07-13 12:50:29 +02:00
Olivier Lambert
831d9cb49f feat(i18n): Portuguese translation
* Translation PT-BR Reviewed and Updated
2016-07-13 11:57:06 +02:00
Olivier Lambert
a5d059b0b1 fix(vm): protect JS crash if arrays[0] is empty 2016-07-13 11:43:52 +02:00
Pierre Donias
4c3b959869 fix(home): add key prop to sort options 2016-07-13 09:59:59 +02:00
Pierre Donias
d81a169a39 fix(form/SizeInput): parseSize value cannot be undefined 2016-07-13 09:59:44 +02:00
Pierre Donias
0d47332526 feat(new-vm): self service with resource sets
Fixes #1155
2016-07-13 09:59:11 +02:00
Pierre Donias
539d136936 fix(page/title): check if the container exists before displaying its name in the title 2016-07-13 09:30:58 +02:00
Pierre Donias
4c28b5775d feat(page/title): page titles for each view
Fixed #1185
2016-07-11 17:33:00 +02:00
Pierre Donias
fe6f351f84 feat(new/sr): page header
Fixes #1129
2016-07-11 17:10:27 +02:00
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
153 changed files with 11548 additions and 2640 deletions

View File

@@ -1,5 +1,88 @@
# ChangeLog
## **5.1.0** (2016-07-26)
### Enhancements
- Improve backups timezone UI [\#1314](https://github.com/vatesfr/xo-web/issues/1314)
- HOME view submenus [\#1306](https://github.com/vatesfr/xo-web/issues/1306)
- Ability for a user to save SSH keys [\#1299](https://github.com/vatesfr/xo-web/issues/1299)
- \[ACLs\] Ability to select all hosts/VMs [\#1296](https://github.com/vatesfr/xo-web/issues/1296)
- Improve scheduling UI [\#1295](https://github.com/vatesfr/xo-web/issues/1295)
- Plugins: Predefined configurations [\#1289](https://github.com/vatesfr/xo-web/issues/1289)
- Button to recompute resource sets limits [\#1287](https://github.com/vatesfr/xo-web/issues/1287)
- Credit scheduler CAP and weight configuration [\#1283](https://github.com/vatesfr/xo-web/issues/1283)
- Migration form problem on the /v5/vms/\_\_UUID\_\_ page when doing xenmotion inside a pool [\#1254](https://github.com/vatesfr/xo-web/issues/1254)
- /v5/\#/pools/\_\_UUID\_\_: patch table improvement [\#1246](https://github.com/vatesfr/xo-web/issues/1246)
- /v5/\#/hosts/\_\_UUID\_\_: patch list improvements ? [\#1245](https://github.com/vatesfr/xo-web/issues/1245)
- F\*cking patches, how do they work? [\#1236](https://github.com/vatesfr/xo-web/issues/1236)
- Change Default Filter [\#1235](https://github.com/vatesfr/xo-web/issues/1235)
- Add a property on jobs to know their state [\#1232](https://github.com/vatesfr/xo-web/issues/1232)
- Spanish translation [\#1231](https://github.com/vatesfr/xo-web/issues/1231)
- Home: "Filter" input and keyboard focus [\#1228](https://github.com/vatesfr/xo-web/issues/1228)
- Display xenserver version [\#1225](https://github.com/vatesfr/xo-web/issues/1225)
- Plugin config: presets & defaults [\#1222](https://github.com/vatesfr/xo-web/issues/1222)
- Allow halted VM migration [\#1216](https://github.com/vatesfr/xo-web/issues/1216)
- Missing confirm dialog on critical button [\#1211](https://github.com/vatesfr/xo-web/issues/1211)
- Backup logs are not sortable [\#1196](https://github.com/vatesfr/xo-web/issues/1196)
- Page title with the name of current object [\#1185](https://github.com/vatesfr/xo-web/issues/1185)
- Existing VIF management [\#1176](https://github.com/vatesfr/xo-web/issues/1176)
- Do not display fast clone option is there isn't template disks [\#1172](https://github.com/vatesfr/xo-web/issues/1172)
- UI issue when adding a user [\#1159](https://github.com/vatesfr/xo-web/issues/1159)
- Combined values on stats [\#1158](https://github.com/vatesfr/xo-web/issues/1158)
- Parallel coordinates graph [\#1157](https://github.com/vatesfr/xo-web/issues/1157)
- VM creation on self-service as user [\#1155](https://github.com/vatesfr/xo-web/issues/1155)
- VM copy bulk action on home view [\#1154](https://github.com/vatesfr/xo-web/issues/1154)
- Better VDI map [\#1151](https://github.com/vatesfr/xo-web/issues/1151)
- Missing tooltips on buttons [\#1150](https://github.com/vatesfr/xo-web/issues/1150)
- Patching from pool view [\#1149](https://github.com/vatesfr/xo-web/issues/1149)
- Missing patches in dashboard [\#1148](https://github.com/vatesfr/xo-web/issues/1148)
- Improve tasks view [\#1147](https://github.com/vatesfr/xo-web/issues/1147)
- Home bulk VM migration [\#1146](https://github.com/vatesfr/xo-web/issues/1146)
- LDAP plugin clear password field [\#1145](https://github.com/vatesfr/xo-web/issues/1145)
- Cron default behavior [\#1144](https://github.com/vatesfr/xo-web/issues/1144)
- Modal for migrate on home [\#1143](https://github.com/vatesfr/xo-web/issues/1143)
- /v5/\#/srs/\_\_UUID\_\_: UI improvements [\#1142](https://github.com/vatesfr/xo-web/issues/1142)
- /v5/\#/pools/: some name should be links [\#1141](https://github.com/vatesfr/xo-web/issues/1141)
- create the page /v5/\#/pools/ [\#1140](https://github.com/vatesfr/xo-web/issues/1140)
- Dashboard: add links to different part of XOA [\#1139](https://github.com/vatesfr/xo-web/issues/1139)
- /v5/\#/dashboard/overview: add link on the "Top 5 SR Usage" graph [\#1135](https://github.com/vatesfr/xo-web/issues/1135)
- /v5/\#/backup/overview: display the error when there is one returned by xenserver on failed job. [\#1134](https://github.com/vatesfr/xo-web/issues/1134)
- /v5/: add an option to set the number of element displayed in tables [\#1133](https://github.com/vatesfr/xo-web/issues/1133)
- Updater refresh page after update [\#1131](https://github.com/vatesfr/xo-web/issues/1131)
- /v5/\#/settings/plugins [\#1130](https://github.com/vatesfr/xo-web/issues/1130)
- /v5/\#/new/sr: layout issue [\#1129](https://github.com/vatesfr/xo-web/issues/1129)
- v5 /v5/\#/vms/new: layout issue [\#1128](https://github.com/vatesfr/xo-web/issues/1128)
- v5 user page missing style [\#1127](https://github.com/vatesfr/xo-web/issues/1127)
- Remote helper/tester [\#1075](https://github.com/vatesfr/xo-web/issues/1075)
- Generate uiSchema from custom schema properties [\#951](https://github.com/vatesfr/xo-web/issues/951)
- Customizing VM names generation during batch creation [\#949](https://github.com/vatesfr/xo-web/issues/949)
### Bug fixes
- Plugins: Don't use `default` attributes in presets list [\#1288](https://github.com/vatesfr/xo-web/issues/1288)
- CPU weight must be an integer [\#1286](https://github.com/vatesfr/xo-web/issues/1286)
- Overview of self service is always empty [\#1282](https://github.com/vatesfr/xo-web/issues/1282)
- SR attach/creation issue [\#1281](https://github.com/vatesfr/xo-web/issues/1281)
- Self service resources not modified after a VM deletion [\#1276](https://github.com/vatesfr/xo-web/issues/1276)
- Scheduled jobs seems use GMT since 5.0 [\#1258](https://github.com/vatesfr/xo-web/issues/1258)
- Can't create a VM with disks on 2 different SRs [\#1257](https://github.com/vatesfr/xo-web/issues/1257)
- Graph display bug [\#1247](https://github.com/vatesfr/xo-web/issues/1247)
- /v5/#/hosts/__UUID__: Patch list not limited to the current pool [\#1244](https://github.com/vatesfr/xo-web/issues/1244)
- Replication issues [\#1233](https://github.com/vatesfr/xo-web/issues/1233)
- VM creation install method disabled fields [\#1198](https://github.com/vatesfr/xo-web/issues/1198)
- Update icon shouldn't be displayed when menu is collapsed [\#1188](https://github.com/vatesfr/xo-web/issues/1188)
- /v5/ : Load average graph axis issue [\#1167](https://github.com/vatesfr/xo-web/issues/1167)
- Some remote can't be opened [\#1164](https://github.com/vatesfr/xo-web/issues/1164)
- Bulk action for hosts in home and pool view [\#1153](https://github.com/vatesfr/xo-web/issues/1153)
- New Vif [\#1138](https://github.com/vatesfr/xo-web/issues/1138)
- Missing SRs [\#1123](https://github.com/vatesfr/xo-web/issues/1123)
- Continuous replication email alert does not obey per job setting [\#1121](https://github.com/vatesfr/xo-web/issues/1121)
- Safari XO5 issue [\#1120](https://github.com/vatesfr/xo-web/issues/1120)
- ACLs shoud be available in Enterprise Edition [\#1118](https://github.com/vatesfr/xo-web/issues/1118)
- SR edit name or description doesn't work [\#1116](https://github.com/vatesfr/xo-web/issues/1116)
- Bad RRD parsing for VIFs [\#969](https://github.com/vatesfr/xo-web/issues/969)
## **5.0.0** (2016-06-24)
### Enhancements

View File

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

View File

@@ -1,7 +1,7 @@
{
"private": true,
"private": false,
"name": "xo-web",
"version": "5.0.1",
"version": "5.1.1",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -58,7 +58,7 @@
"font-awesome": "^4.5.0",
"font-mfizz": "github:fizzed/font-mfizz",
"ghooks": "^1.1.1",
"globby": "^5.0.0",
"globby": "^6.0.0",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-autoprefixer": "^3.1.0",
"gulp-csso": "^2.0.0",
@@ -75,8 +75,9 @@
"lodash": "^4.6.1",
"loose-envify": "^1.1.0",
"marked": "^0.3.5",
"modular-css": "^0.22.1",
"modular-css": "^0.23.2",
"moment": "^2.13.0",
"moment-timezone": "^0.5.4",
"notifyjs": "^2.0.1",
"novnc-node": "^0.5.3",
"promise-toolbox": "^0.4.0",
@@ -89,6 +90,7 @@
"react-debounce-input": "^2.4.0",
"react-dnd": "^2.1.4",
"react-dnd-html5-backend": "^2.1.2",
"react-document-title": "^2.0.2",
"react-dom": "^15.0.0",
"react-dropzone": "^3.5.0",
"react-intl": "^2.0.1",
@@ -111,8 +113,8 @@
"superagent": "^2.0.0",
"vinyl": "^1.1.1",
"watchify": "^3.7.0",
"xo-acl-resolver": "^0.2.0",
"xo-lib": "^0.8.0-1",
"xo-acl-resolver": "^0.2.1",
"xo-lib": "^0.8.0",
"xo-remote-parser": "^0.3"
},
"scripts": {

View File

@@ -22,6 +22,11 @@ $ct-series-colors: (
@import "../node_modules/chartist/dist/scss/settings/_chartist-settings";
@import "../node_modules/chartist/dist/scss/chartist";
.ct-chart {
display: flex;
flex-direction: column-reverse;
}
// Line in charts with only 2px in width
.ct-line {
stroke-width: 2px;
@@ -55,7 +60,6 @@ $ct-series-colors: (
// Arrow!
&:before {
position: absolute;
bottom: -14px;
top: 100%;
left: 50%;
@@ -80,28 +84,27 @@ $ct-series-colors: (
// CHARTIST LEGEND =============================================================
.ct-legend {
position: absolute;
bottom: 0;
margin-bottom: -1em;
li {
position: relative;
padding-left: 1.4em;
padding-left: 0.5em;
list-style-type: none;
display: inline;
display: inline-block;
margin-right: 0.5em;
font-size: 0.8em;
}
li:before {
display: inline-block;
width: 1em;
height: 1em;
position: absolute;
left: 0;
content: '';
border: 3px solid transparent;
border-radius: 2px;
margin-top: 0.5em;
margin-right: 0.2em;
}
li.inactive:before {

View File

@@ -1,8 +1,8 @@
// import _ from 'intl' TODO: fix tooltip
import _ from 'intl'
import ActionButton from 'action-button'
import map from 'lodash/map'
import React from 'react'
// import Tooltip from 'tooltip' TODO: fix tooltip
import Tooltip from 'tooltip'
import {
ButtonGroup
} from 'react-bootstrap-4/lib'
@@ -10,17 +10,17 @@ import {
const ActionBar = ({ actions, param }) => (
<ButtonGroup>
{map(actions, ({ handler, handlerParam = param, label, icon }, index) => (
/* <Tooltip key={index} content={_(label)}> TODO: fix tooltip */
<ActionButton
key={index}
btnStyle='secondary'
handler={handler}
handlerParam={handlerParam}
icon={icon}
size='large'
/>
/* </Tooltip> */
))}
<Tooltip key={index} content={_(label)}>
<ActionButton
key={index}
btnStyle='secondary'
handler={handler}
handlerParam={handlerParam}
icon={icon}
size='large'
/>
</Tooltip>
))}
</ButtonGroup>
)
ActionBar.propTypes = {

View File

@@ -5,7 +5,7 @@ import { Button } from 'react-bootstrap-4/lib'
import Component from './base-component'
import logError from './log-error'
import { autobind, propTypes } from './utils'
import propTypes from './prop-types'
@propTypes({
btnStyle: propTypes.string,
@@ -28,7 +28,6 @@ export default class ActionButton extends Component {
router: React.PropTypes.object
}
@autobind
async _execute () {
if (this.state.working) {
return
@@ -66,6 +65,7 @@ export default class ActionButton extends Component {
logError(error)
}
}
_execute = ::this._execute
_eventListener = event => {
event.preventDefault()

View File

@@ -1,6 +1,7 @@
import ActionButton from 'action-button'
import React from 'react'
import { propTypes } from 'utils'
import ActionButton from './action-button'
import propTypes from './prop-types'
const ActionToggle = ({ className, value, ...props }) =>
<ActionButton

View File

@@ -1,6 +1,7 @@
import forEach from 'lodash/forEach'
import { Component } from 'react'
import getEventValue from './get-event-value'
import invoke from './invoke'
import shallowEqual from './shallow-equal'
@@ -11,6 +12,8 @@ export default class BaseComponent extends Component {
// It really should have been done in React.Component!
this.state = {}
this._linkedState = null
if (process.env.NODE_ENV !== 'production') {
this.render = invoke(this.render, render => () => {
console.log('render', this.constructor.name)
@@ -20,6 +23,23 @@ export default class BaseComponent extends Component {
}
}
// See https://preactjs.com/guide/linked-state
linkState (name) {
let linkedState = this._linkedState
let cb
if (!linkedState) {
linkedState = this._linkedState = {}
} else if ((cb = linkedState[name])) {
return cb
}
return (linkedState[name] = event => {
this.setState({
[name]: getEventValue(event)
})
})
}
shouldComponentUpdate (newProps, newState) {
return !(
shallowEqual(this.props, newProps) &&

View File

@@ -1,5 +1,6 @@
import React from 'react'
import { propTypes } from 'utils'
import propTypes from './prop-types'
const CARD_STYLE = {
minHeight: '100%'
@@ -33,9 +34,9 @@ export const CardHeader = propTypes({
children,
className
}) => (
<h3 className={`card-header ${className || ''}`} style={CARD_HEADER_STYLE}>
<h4 className={`card-header ${className || ''}`} style={CARD_HEADER_STYLE}>
{children}
</h3>
</h4>
))
export const CardBlock = propTypes({

View File

@@ -1,7 +1,8 @@
import Component from 'base-component'
import Icon from 'icon'
import React from 'react'
import { propTypes } from 'utils'
import Component from './base-component'
import Icon from './icon'
import propTypes from './prop-types'
@propTypes({
children: propTypes.any.isRequired,

View File

@@ -0,0 +1,3 @@
.button {
border-radius: 0px;
};

View File

@@ -0,0 +1,101 @@
import map from 'lodash/map'
import React from 'react'
import size from 'lodash/size'
import Component from '../base-component'
import propTypes from '../prop-types'
import { ensureArray } from '../utils'
import {
DropdownButton,
MenuItem
} from 'react-bootstrap-4/lib'
import styles from './index.css'
@propTypes({
defaultValue: propTypes.any,
disabled: propTypes.bool,
options: propTypes.oneOfType([
propTypes.arrayOf(propTypes.string),
propTypes.number,
propTypes.objectOf(propTypes.string),
propTypes.string
]),
onChange: propTypes.func,
placeholder: propTypes.string,
required: propTypes.bool,
step: propTypes.any,
type: propTypes.string,
value: propTypes.any
})
export default class Combobox extends Component {
static defaultProps = {
type: 'text'
}
get value () {
return this.refs.input.value
}
set value (value) {
this.refs.input.value = value
}
_handleChange = event => {
const { onChange } = this.props
if (onChange) {
onChange(event.target.value)
}
}
_setText (value) {
this.refs.input.value = value
}
render () {
const { props } = this
const options = ensureArray(props.options)
const Input = (
<input
className='form-control'
defaultValue={props.defaultValue}
disabled={props.disabled}
options={options}
onChange={this._handleChange}
placeholder={props.placeholder}
ref='input'
required={props.required}
step={props.step}
type={props.type}
value={props.value}
/>
)
if (!size(options)) {
return Input
}
return (
<div className='input-group'>
<div className='input-group-btn'>
<DropdownButton
bsStyle='secondary'
className={styles.button}
disabled={props.disabled}
id='selectInput'
title=''
>
{map(options, option => (
<MenuItem key={option} onClick={() => this._setText(option)}>
{option}
</MenuItem>
))}
</DropdownButton>
</div>
{Input}
</div>
)
}
}

View File

@@ -3,7 +3,7 @@ import classNames from 'classnames'
import React, { createElement } from 'react'
import Icon from '../icon'
import { propTypes } from '../utils'
import propTypes from '../prop-types'
import styles from './index.css'

9
src/common/d3-utils.js vendored Normal file
View File

@@ -0,0 +1,9 @@
import forEach from 'lodash/forEach'
export function setStyles (style) {
forEach(style, (value, key) => {
this.style(key, value)
})
return this
}

View File

@@ -7,10 +7,11 @@ import React from 'react'
import _ from './intl'
import Component from './base-component'
import logError from './log-error'
import Icon from './icon'
import logError from './log-error'
import propTypes from './prop-types'
import Tooltip from './tooltip'
import { formatSize, propTypes } from './utils'
import { formatSize } from './utils'
import { SizeInput } from './form'
import {
SelectHost,
@@ -27,7 +28,13 @@ import {
const LONG_CLICK = 400
const SELECT_STYLE = { padding: '0px' }
const SIZE_STYLE = { width: '10rem' }
const EDITABLE_STYLE = { borderBottom: '1px dashed #ccc' }
const EDITABLE_STYLE = {
borderBottom: '1px dashed #ccc',
cursor: 'context-menu'
}
const LONG_EDITABLE_STYLE = {
cursor: 'context-menu'
}
@propTypes({
alt: propTypes.node.isRequired
@@ -156,7 +163,7 @@ class Editable extends Component {
const { useLongClick } = props
const success = <Icon icon='success' />
return <span style={useLongClick ? null : EDITABLE_STYLE}>
return <span style={useLongClick ? LONG_EDITABLE_STYLE : EDITABLE_STYLE}>
<span
onClick={!useLongClick && this._openEdition}
onMouseDown={useLongClick && this.__startTimer}
@@ -270,20 +277,34 @@ export class Password extends Text {
}
@propTypes({
value: propTypes.number.isRequired
nullable: propTypes.bool,
value: propTypes.number
})
export class Number extends Component {
get value () {
return +this.refs.input.value
}
_onChange = value => this.props.onChange(+value)
_onChange = value => {
if (value === '') {
if (this.props.nullable) {
value = null
} else {
return
}
} else {
value = +value
}
this.props.onChange(value)
}
render () {
const { value } = this.props
return <Text
{...this.props}
onChange={this._onChange}
value={String(this.props.value)}
value={value === null ? '' : String(value)}
/>
}
}

View File

@@ -20,7 +20,7 @@ export { Ellipsis as default }
export const EllipsisContainer = ({ children }) => (
<div style={ellipsisContainerStyle}>
{React.Children.map(children, child =>
child.type === Ellipsis ? child : <span>{child}</span>
child == null || child.type === Ellipsis ? child : <span>{child}</span>
)}
</div>
)

View File

@@ -1,6 +1,7 @@
import React from 'react'
import * as Grid from 'grid'
import { propTypes } from 'utils'
import * as Grid from './grid'
import propTypes from './prop-types'
export const LabelCol = propTypes({
children: propTypes.any.isRequired

View File

@@ -1,19 +1,21 @@
import BaseComponent from 'base-component'
import classNames from 'classnames'
import Icon from 'icon'
import map from 'lodash/map'
import randomPassword from 'random-password'
import React from 'react'
import round from 'lodash/round'
import {
DropdownButton,
MenuItem
} from 'react-bootstrap-4/lib'
import Component from '../base-component'
import propTypes from '../prop-types'
import {
autobind,
firstDefined,
formatSizeRaw,
parseSize,
propTypes
parseSize
} from '../utils'
export Select from './select'
@@ -33,16 +35,14 @@ export class Password extends Component {
this.refs.field.value = value
}
@autobind
_generate () {
_generate = () => {
this.refs.field.value = randomPassword(8)
this.setState({
visible: true
})
}
@autobind
_toggleVisibility () {
_toggleVisibility = () => {
this.setState({
visible: !this.state.visible
})
@@ -107,8 +107,7 @@ export class Range extends Component {
}
}
@autobind
_handleChange (event) {
_handleChange = event => {
const { onChange } = this.props
const { value } = event.target
@@ -162,38 +161,88 @@ const DEFAULT_UNIT = 'GiB'
placeholder: propTypes.string,
readOnly: propTypes.bool,
required: propTypes.bool,
style: propTypes.object
style: propTypes.object,
value: propTypes.number
})
export class SizeInput extends Component {
export class SizeInput extends BaseComponent {
constructor (props) {
super(props)
const humanSize = props.defaultValue && formatSizeRaw(props.defaultValue)
this._defaultValue = humanSize && humanSize.value
this.state = { unit: humanSize ? humanSize.prefix + 'B' : props.defaultUnit || DEFAULT_UNIT }
this.state = this._createStateFromBytes(firstDefined(props.value, props.defaultValue, 0))
}
componentWillReceiveProps (newProps) {
this.value = newProps.value
const { value } = newProps
if (value == null && value === this.props.value) {
return
}
const { _bytes, _unit, _value } = this
this._bytes = this._unit = this._value = null
if (value === _bytes) {
// Update input value
this.setState({
unit: _unit,
value: _value
})
} else {
this.setState(this._createStateFromBytes(value))
}
}
_createStateFromBytes = bytes => {
const humanSize = bytes && formatSizeRaw(bytes)
return {
unit: humanSize && humanSize.value ? humanSize.prefix + 'B' : this.props.defaultUnit || DEFAULT_UNIT,
value: humanSize ? round(humanSize.value, 3) : ''
}
}
get value () {
const value = this.refs.value.value
return value ? parseSize(value + ' ' + this.state.unit) : undefined
const { unit, value } = this.state
return parseSize(value + ' ' + unit)
}
set value (newValue) {
const humanSize = newValue && formatSizeRaw(newValue)
this.refs.value.value = humanSize ? humanSize.value : ''
this.setState({ unit: humanSize ? humanSize.prefix + 'B' : DEFAULT_UNIT })
if (
process.env.NODE_ENV !== 'production' &&
this.props.value != null
) {
throw new Error('cannot set value of controlled SizeInput')
}
this.setState(this._createStateFromBytes(newValue))
}
_onChange = () =>
this.props.onChange && this.props.onChange(this.value)
_onChange = value =>
this.props.onChange && this.props.onChange(value)
_updateValue = event => {
const { value } = event.target
if (this.props.value != null) {
this._value = value
this._unit = this.state.unit
this._bytes = parseSize((value || 0) + ' ' + this.state.unit)
this._onChange(this._bytes)
} else {
this.setState({ value }, () => {
this._onChange(this.value)
})
}
}
_updateUnit = unit => {
this.setState({ unit })
this._onChange()
if (this.props.value != null) {
this._value = this.state.value
this._unit = unit
this._bytes = parseSize((this.state.value || 0) + ' ' + unit)
this._onChange(this._bytes)
} else {
this.setState({ unit }, () => {
this._onChange(this.value)
})
}
}
render () {
@@ -206,6 +255,11 @@ export class SizeInput extends Component {
style
} = this.props
const {
value,
unit
} = this.state
return <span
className={classNames(className, 'input-group')}
style={style}
@@ -213,14 +267,13 @@ export class SizeInput extends Component {
<input
autoFocus={autoFocus}
className='form-control'
defaultValue={this._defaultValue}
min={0}
onChange={this._onChange}
onChange={this._updateValue}
placeholder={placeholder}
readOnly={readOnly}
required={required}
ref='value'
type='number'
value={value}
/>
<span className='input-group-btn'>
<DropdownButton
@@ -228,7 +281,7 @@ export class SizeInput extends Component {
disabled={readOnly}
id='size'
pullRight
title={this.state.unit}
title={unit}
>
{map(UNITS, unit =>
<MenuItem

View File

@@ -1,8 +1,10 @@
import find from 'lodash/find'
import map from 'lodash/map'
import React, { Component } from 'react'
import { Select } from 'form'
import { propTypes } from 'utils'
import propTypes from '../prop-types'
import Select from './select'
@propTypes({
autoFocus: propTypes.bool,

View File

@@ -1,11 +1,12 @@
import React, { Component } from 'react'
import ReactSelect from 'react-select'
import { propTypes } from 'utils'
import {
AutoSizer,
VirtualScroll
} from 'react-virtualized'
import propTypes from '../prop-types'
const SELECT_MENU_STYLE = {
overflow: 'hidden'
}

View File

@@ -0,0 +1,17 @@
// If the param is an event, returns the value of it's target,
// otherwise returns the param.
const getEventValue = event => {
let target
if (!event || !(target = event.target)) {
return event
}
return (
target.nodeName.toLowerCase() === 'input' &&
target.type.toLowerCase() === 'checkbox'
)
? target.checked
: target.value
}
export { getEventValue as default }

View File

@@ -1,6 +1,7 @@
import classNames from 'classnames'
import React from 'react'
import { propTypes } from 'utils'
import propTypes from './prop-types'
export const Col = propTypes({
className: propTypes.string,

View File

@@ -0,0 +1,16 @@
export const VM = {
homeFilterPendingVms: 'current_operations:"" ',
homeFilterNonRunningVms: '!power_state:running ',
homeFilterHvmGuests: 'virtualizationMode:hvm ',
homeFilterRunningVms: 'power_state:running ',
homeFilterTags: 'tags:'
}
export const host = {
homeFilterRunningHosts: 'power_state:running ',
homeFilterTags: 'tags:'
}
export const pool = {
homeFilterTags: 'tags:'
}

View File

@@ -0,0 +1,216 @@
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import React from 'react'
import { Portal } from 'react-overlays'
import _ from './intl'
import ActionButton from './action-button'
import Component from './base-component'
import forEach from 'lodash/forEach'
import Link from './link'
import propTypes from './prop-types'
import SortedTable from './sorted-table'
import TabButton from './tab-button'
import { connectStore } from './utils'
import {
createGetObjectsOfType,
createFilter,
createSelector
} from './selectors'
import {
getHostMissingPatches,
installAllHostPatches
} from './xo'
// ===================================================================
const MISSING_PATCHES_COLUMNS = [
{
name: _('srHost'),
itemRenderer: host => <Link to={`/hosts/${host.id}`}>{host.name_label}</Link>,
sortCriteria: host => host.name_label
},
{
name: _('hostDescription'),
itemRenderer: host => host.name_description,
sortCriteria: host => host.name_description
},
{
name: _('hostMissingPatches'),
itemRenderer: (host, { missingPatches }) => <Link to={`/hosts/${host.id}/patches`}>{missingPatches[host.id]}</Link>,
sortCriteria: (host, { missingPatches }) => missingPatches[host.id]
},
{
name: _('patchUpdateButton'),
itemRenderer: (host, { installAllHostPatches }) => (
<ActionButton
btnStyle='primary'
handler={installAllHostPatches}
handlerParam={host}
icon='host-patch-update'
/>
)
}
]
const POOLS_MISSING_PATCHES_COLUMNS = [{
name: _('srPool'),
itemRenderer: (host, { pools }) => {
const pool = pools[host.$pool]
return <Link to={`/pools/${pool.id}`}>{pool.name_label}</Link>
},
sortCriteria: (host, { pools }) => pools[host.$pool].name_label
}].concat(MISSING_PATCHES_COLUMNS)
// ===================================================================
class HostsPatchesTable extends Component {
constructor (props) {
super(props)
this.state.missingPatches = {}
}
_getHosts = createFilter(
() => this.props.hosts,
createSelector(
() => this.state.missingPatches,
missingPatches => host => missingPatches[host.id]
)
)
_refreshMissingPatches = () => (
Promise.all(
map(this.props.hosts, this._refreshHostMissingPatches)
)
)
_installAllMissingPatches = () => (
Promise.all(map(this._getHosts(), this._installAllHostPatches))
)
_refreshHostMissingPatches = host => (
getHostMissingPatches(host).then(patches => {
this.setState({
missingPatches: {
...this.state.missingPatches,
[host.id]: patches.length
}
})
})
)
_installAllHostPatches = host => (
installAllHostPatches(host).then(() =>
this._refreshHostMissingPatches(host)
)
)
componentWillMount () {
this._refreshMissingPatches()
}
componentDidMount () {
// Force one Portal refresh.
// Because Portal cannot see the container reference at first rendering.
this.forceUpdate()
}
componentWillReceiveProps (nextProps) {
forEach(nextProps.hosts, host => {
const { id } = host
if (this.state.missingPatches[id] !== undefined) {
return
}
this.setState({
missingPatches: {
...this.state.missingPatches,
[id]: 0
}
})
this._refreshHostMissingPatches(host)
})
}
render () {
const hosts = this._getHosts()
const noPatches = isEmpty(hosts)
const { props } = this
const Container = props.container || 'div'
const Button = props.useTabButton ? TabButton : ActionButton
const Buttons = (
<Container>
<Button
btnStyle='secondary'
handler={this._refreshMissingPatches}
icon='refresh'
labelId='refreshPatches'
/>
<Button
btnStyle='primary'
disabled={noPatches}
handler={this._installAllMissingPatches}
icon='host-patch-update'
labelId='installPoolPatches'
/>
</Container>
)
return (
<div>
{!noPatches
? (
<SortedTable
collection={hosts}
columns={props.displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
userData={{
installAllHostPatches: this._installAllHostPatches,
missingPatches: this.state.missingPatches,
pools: props.pools
}}
/>
) : <p>{_('patchNothing')}</p>
}
<Portal container={() => props.buttonsGroupContainer()}>
{Buttons}
</Portal>
</div>
)
}
}
// ===================================================================
@connectStore(() => {
const getPools = createGetObjectsOfType('pool')
return {
pools: getPools
}
})
class HostsPatchesTableByPool extends Component {
render () {
const { props } = this
return <HostsPatchesTable {...props} pools={props.pools} />
}
}
// ===================================================================
export default propTypes({
buttonsGroupContainer: propTypes.func.isRequired,
container: propTypes.any,
displayPools: propTypes.bool,
hosts: propTypes.oneOfType([
propTypes.arrayOf(propTypes.object),
propTypes.objectOf(propTypes.object)
]).isRequired,
useTabButton: propTypes.bool
})(props => props.displayPools
? <HostsPatchesTableByPool {...props} />
: <HostsPatchesTable {...props} />
)

File diff suppressed because it is too large Load Diff

View File

@@ -44,8 +44,8 @@ export default {
newSrPage: 'Stockage',
newImport: 'Importer',
// ----- Home view -----
homeDisplayedVms: '{displayed}x {vmIcon} (sur {total})',
homeSelectedVms: '{selected}x {vmIcon} sélectionnée{selected, plural, zero {} one {} other {s}} (sur {total})',
homeDisplayedItems: '{displayed}x {icon} (sur {total})',
homeSelectedItems: '{selected}x {icon} sélectionnée{selected, plural, zero {} one {} other {s}} (sur {total})',
homeMigrateTo: 'Migrer sur…',
// ----- General Stuff -----
homePage: 'Accueil',
@@ -82,20 +82,15 @@ export default {
fillOptionalInformations: 'Remplir informations (optionnel)',
selectTableReset: 'Réinitialiser',
schedulingMonth: 'Mois',
schedulingEveryMonth: 'Tous les mois',
schedulingEachSelectedMonth: 'Chaque mois sélectionné',
schedulingMonthDay: 'Jour du mois',
schedulingEveryMonthDay: 'Tous les jours',
schedulingEachSelectedMonthDay: 'Chaque jour sélectionné',
schedulingWeekDay: 'Jour de la semaine',
schedulingEveryWeekDay: 'Tous les jours',
schedulingEachSelectedWeekDay: 'Chaque jour sélectionné',
schedulingHour: 'Heure',
schedulingEveryHour: 'Toutes les heures',
schedulingEveryNHour: 'Toutes les N heures',
schedulingEachSelectedHour: 'Chaque heure sélectionnée',
schedulingMinute: 'Minute',
schedulingEveryMinute: 'Toutes les minutes',
schedulingEveryNMinute: 'Toutes les N minutes',
schedulingEachSelectedMinute: 'Chaque minute sélectionnée',
schedulingReset: 'Reset',
@@ -279,7 +274,7 @@ export default {
srStatePanel: 'État du stockage',
taskStatePanel: 'Tâches en cours',
usersStatePanel: 'Utilisateurs',
ofUsage: 'sur',
ofUsage: '{usage} (sur {total})',
noSrs: 'Aucun stockage',
srName: 'Nom',
srPool: 'Pool',
@@ -310,7 +305,8 @@ export default {
alarmPool: 'Pool',
alarmRemoveAll: 'Supprimer toutes les alarmes',
// ----- New VM -----
newVmCreateNewVmOn: 'Créer une nouvelle VM sur {pool}',
newVmCreateNewVmOn: 'Créer une nouvelle VM sur {select}',
newVmCreateNewVmOn2: 'Créer une nouvelle VM sur {select1} ou {select2}',
newVmInfoPanel: 'Informations',
newVmNameLabel: 'Nom',
newVmTemplateLabel: 'Modèle',
@@ -360,14 +356,21 @@ export default {
startVmImport: 'Lancement de l\'import…',
startVmExport: 'Lancement de l\'export…',
// ----- Modals -----
stopHostModalTitle: 'Arrêter l\'hôte',
stopHostModalMessage: 'Cette action va arrêter l\'hôte. Êtes vous sûr de vouloir continuer ?',
restartHostModalTitle: 'Redémarrer l\'hôte',
restartHostModalMessage: 'Cette action va redémarrer l\'hôte. Êtes vous sûr de vouloir continuer ?',
startVmsModalTitle: 'Démarrer {vms, plural, one {la} other {les}} VM{vms, plural, one {} other {s}}',
startVmsModalMessage: 'Voulez-vous vraiment démarrer {vms} VM{vms, plural, one {} other {s}} ?',
stopVmsModalTitle: 'Arrêter {vms, plural, one {la} other {les}} VM{vms, plural, one {} other {s}}',
restartVmModalTitle: 'Redémarrer la VM',
restartVmModalMessage: 'Voulez-vous vraiment redémarrer {name} ?',
stopVmModalTitle: 'Arrêter la VM',
stopVmModalMessage: 'Voulez-vous vraiment arrêter {name} ?',
stopVmsModalMessage: 'Voulez-vous vraiment arrêter {vms} VM{vms, plural, one {} other {s}} ?',
restartVmsModalTitle: 'Redémarrer {vms, plural, one {la} other {les}} VM{vms, plural, one {} other {s}}',
restartVmsModalMessage: 'Voulez-vous vraiment redémarrer {vms} VM{vms, plural, one {} other {s}} ?',
migrateVmModalTitle: 'Migrer la VM',
migrateVmModalBody: 'Voulez-vous vraiment migrer cette VM sur {hostName} ?',
migrateVmAdvancedModalSelectHost: 'Sélectionnez un hôte de destination:',
migrateVmAdvancedModalSelectNetwork: 'Sélectionnez un réseau pour la migration:',
migrateVmAdvancedModalSelectSrs: 'Pour chaque VDI, sélectionnez un SR:',

View File

@@ -272,11 +272,11 @@ export default {
// Original text: "vCPUs"
homeSortByvCPUs: 'כמות המאבדים',
// Original text: "{displayed, number}x {vmIcon} (on {total, number})"
homeDisplayedVms: undefined,
// Original text: "{displayed, number}x {icon} (on {total, number})"
homeDisplayedItems: undefined,
// Original text: "{selected, number}x {vmIcon} selected (on {total, number})"
homeSelectedVms: undefined,
// Original text: "{selected, number}x {icon} selected (on {total, number})"
homeSelectedItems: undefined,
// Original text: "More"
homeMore: 'עוד',
@@ -1676,6 +1676,30 @@ export default {
// Original text: "No backups available"
noBackup: undefined,
// Original text: "Shutdown host"
stopHostModalTitle: undefined,
// Original text: "This will shutdown your host. Do you want to continue?"
stopHostModalMessage: undefined,
// Original text: "Restart host"
restartHostModalTitle: undefined,
// Original text: "This will restart your host. Do you want to continue?"
restartHostModalMessage: undefined,
// Original text: "Restart VM"
restartVmModalTitle: undefined,
// Original text: "Are you sure you want to restart {name}?"
restartVmModalMessage: undefined,
// Original text: "Stop VM"
stopVmModalTitle: undefined,
// Original text: "Are you sure you want to stop {name}?"
stopVmModalMessage: undefined,
// Original text: "Start VM{vms, plural, one {} other {s}}"
startVmsModalTitle: undefined,
@@ -1715,9 +1739,6 @@ export default {
// Original text: "Migrate VM"
migrateVmModalTitle: undefined,
// Original text: "Are you sure you want to migrate this VM to {hostName}?"
migrateVmModalBody: undefined,
// Original text: "Select a destination host:"
migrateVmAdvancedModalSelectHost: undefined,

View File

@@ -1,4 +1,4 @@
// See http://momentjs.com/docs/#/use-it/browserify/
// See http://momentjs.com/docs/#/use-it/browserify/
import 'moment/locale/pt'
import reactIntlData from 'react-intl/locale-data/pt'
@@ -84,7 +84,7 @@ export default {
newMenu: 'Novo(a)',
// Original text: "Tasks"
taskMenu: 'Atividades',
taskMenu: 'Tarefas',
// Original text: "VM"
newVmPage: 'VM',
@@ -146,6 +146,12 @@ export default {
// Original text: "Custom Job"
customJob: 'Personalização do Trabalho',
// Original text: "EN"
enLang: 'Inglês',
// Original text: "FR"
frLang: 'Francês',
// Original text: "Username:"
usernameLabel: 'Usuário',
@@ -240,7 +246,7 @@ export default {
homeFilterDisabledHosts: 'Hosts Desativados',
// Original text: "Running VMs"
homeFilterRunningVms: 'Vms Ativas',
homeFilterRunningVms: 'VMs Ativas',
// Original text: "Non running VMs"
homeFilterNonRunningVms: 'VMs Paradas',
@@ -270,10 +276,10 @@ export default {
homeSortByvCPUs: 'vCPUs',
// Original text: "{displayed, number}x {vmIcon} (on {total, number})"
homeDisplayedVms: '{displayed, number}x {vmIcon} (sobre {total, number})',
homeDisplayedVms: '{displayed, number}x {vmIcon} (de {total, number})',
// Original text: "{selected, number}x {vmIcon} selected (on {total, number})"
homeSelectedVms: '{selected, number}x {vmIcon} selected (sobre {total, number})',
homeSelectedVms: '{selected, number}x {vmIcon} selected (de {total, number})',
// Original text: "More"
homeMore: 'Mais',
@@ -405,7 +411,7 @@ export default {
unknownSchedule: 'Desconhecido',
// Original text: "Job"
job: 'tarefa',
job: 'Tarefa',
// Original text: "Job ID"
jobId: 'ID tarefa',
@@ -414,10 +420,10 @@ export default {
jobName: 'Nome',
// Original text: "Start"
jobStart: 'Iniciar',
jobStart: 'Inicia',
// Original text: "End"
jobEnd: 'Terminar',
jobEnd: 'Termina',
// Original text: "Duration"
jobDuration: 'Duração',
@@ -507,7 +513,7 @@ export default {
newSrTypeSelection: 'Selecionar o tipo de armazenamento (storage)',
// Original text: "Settings"
newSrSettings: 'Configuraçõesé',
newSrSettings: 'Configurações',
// Original text: "Storage Usage"
newSrUsage: 'Uso de armazenamento (storage)',
@@ -1308,7 +1314,7 @@ export default {
vmPanel: 'VM{vms, plural, one {} other {s}}',
// Original text: "RAM Usage"
memoryStatePanel: 'Utilização da RAM',
memoryStatePanel: 'Utilização RAM',
// Original text: "CPUs Usage"
cpuStatePanel: 'Utilização de CPU',
@@ -1392,10 +1398,10 @@ export default {
orphanedVms: 'VMs órfãs',
// Original text: "No orphans"
noOrphanedObject: 'Sem órfãos',
noOrphanedObject: 'Sem órfãs',
// Original text: "Remove all orphaned VDIs"
removeAllOrphanedObject: 'Remover todos os VDIs órfãos',
removeAllOrphanedObject: 'Remover todos as VDIs órfãs',
// Original text: "Name"
vmNameLabel: 'Nome',
@@ -1536,16 +1542,16 @@ export default {
newVmCloudConfig: 'Configuração do Cloud',
// Origingal text: "Create VMs"
newVmCreateVms: undefined,
newVmCreateVms: 'Criar VMs',
// Original text : "Are you sure you want to create {nbVms} VMs?"
newVmCreateVmsConfirm: undefined,
newVmCreateVmsConfirm: 'Você tem certeza que deseja criar {nbVms} VMs?',
// Original text : "Multiple VMs"
newVmMultipleVms: undefined,
newVmMultipleVms: 'Multiplas VMs',
// Original text: "Resource sets"
resourceSets: 'Ajustes dos recursos',
resourceSets: 'Ajustes de recursos',
// Original text: "Resource set name"
resourceSetName: 'Ajuste de nome do recurso',
@@ -1569,7 +1575,7 @@ export default {
deleteResourceSetWarning: 'Deletar grupo de recurso',
// Original text: "Are you sure you want to delete this resource set?"
deleteResourceSetQuestion: 'Você tem certeza que deseja deletar este grupo de recurso?',
deleteResourceSetQuestion: 'Você tem certeza que deseja deletar este ajuste?',
// Original text: "Missing objects:"
resourceSetMissingObjects: 'Objetos faltando',
@@ -1596,7 +1602,7 @@ export default {
noHostsAvailable: 'Sem hosts disponiveis',
// Original text: "VMs created from this resource set shall run on the following hosts."
availableHostsDescription: 'VMs criadas a partir desse conjunto de recursos deve ser executado nos seguintes hosts.',
availableHostsDescription: 'VMs criadas a partir desse conjunto de recursos deve ser executado nos hosts indicados.',
// Original text: "Maximum CPUs"
maxCpus: 'Limite de CPUs',
@@ -1605,7 +1611,7 @@ export default {
maxRam: 'Limite de RAM (GiB)',
// Original text: "Maximum disk space"
maxDiskSpace: 'Limite de espaço do disco',
maxDiskSpace: 'Limite de espaço de disco',
// Original text: "No limits."
noResourceSetLimits: 'Sem limites',
@@ -1620,7 +1626,7 @@ export default {
usedResource: 'Usado:',
// Original text: "Try dropping some backups here, or click to select backups to upload. Accept only .xva files."
importVmsList: 'Tente soltar alguns backups aqui, ou clique para selecionar backups que seja feito o upload. Apenas arquivos .xva são aceitos.',
importVmsList: 'Tente soltar alguns backups aqui, ou clique para selecionar os backups para que seja feito o upload. Apenas arquivos .xva são aceitos.',
// Original text: "No selected VMs."
noSelectedVms: 'Nenhuma VM selecionada',
@@ -1673,6 +1679,30 @@ export default {
// Original text: "No backups available"
noBackup: 'Nenhum backup disponível',
// Original text: "Shutdown host"
stopHostModalTitle: 'Desligar host',
// Original text: "This will shutdown your host. Do you want to continue?"
stopHostModalMessage: 'O host será desligado. Você tem certeza que deseja continuar?',
// Original text: "Restart host"
restartHostModalTitle: 'Reiniciar host',
// Original text: "This will restart your host. Do you want to continue?"
restartHostModalMessage: 'O host será reiniciado. Você tem certeza que deseja continuar?',
// Original text: "Restart VM"
restartVmModalTitle: 'Reiniciar VM',
// Original text: "Are you sure you want to restart {name}?"
restartVmModalMessage: 'Você tem certeza que deseja reiniciar {name}?',
// Original text: "Stop VM"
stopVmModalTitle: 'Parar VM',
// Original text: "Are you sure you want to stop {name}?"
stopVmModalMessage: 'Você tem certeza que deseja parar {name}?',
// Original text: "Start VM{vms, plural, one {} other {s}}"
startVmsModalTitle: 'Iniciar VM{vms, plural, one {} other {s}}',
@@ -1749,7 +1779,7 @@ export default {
importBackupModalSelectBackup: 'Selecionar backup...',
// Original text: "Are you sure you want to remove all orphaned VDIs?"
removeAllOrphanedModalWarning: 'Você tem certeza que deseja remover todos os VDIs orfãos?',
removeAllOrphanedModalWarning: 'Você tem certeza que deseja remover todos as VDIs orfãs?',
// Original text: "Remove all logs"
removeAllLogsModalTitle: 'Remover todos os logs',

File diff suppressed because it is too large Load Diff

View File

@@ -13,8 +13,15 @@ var messages = {
confirmOk: 'OK',
confirmCancel: 'Cancel',
// ----- Filters -----
onError: 'On error',
successful: 'Successful',
// ----- Titles -----
homePage: 'Home',
homeVmPage: 'VMs',
homeHostPage: 'Hosts',
homePoolPage: 'Pools',
dashboardPage: 'Dashboard',
overviewDashboardPage: 'Overview',
overviewVisualizationDashboardPage: 'Visualizations',
@@ -35,6 +42,7 @@ var messages = {
aboutPage: 'About',
newMenu: 'New',
taskMenu: 'Tasks',
taskPage: 'Tasks',
newVmPage: 'VM',
newSrPage: 'Storage',
newServerPage: 'Server',
@@ -55,11 +63,9 @@ var messages = {
jobsNewPage: 'New',
jobsSchedulingPage: 'Scheduling',
customJob: 'Custom Job',
userPage: 'User',
// ----- Sign in/out -----
usernameLabel: 'Username:',
passwordLabel: 'Password:',
signInButton: 'Sign in',
// ----- Sign out -----
signOut: 'Sign out',
// ----- Home view ------
@@ -100,10 +106,13 @@ var messages = {
homeSortByPowerstate: 'Power state',
homeSortByRAM: 'RAM',
homeSortByvCPUs: 'vCPUs',
homeDisplayedVms: '{displayed, number}x {vmIcon} (on {total, number})',
homeSelectedVms: '{selected, number}x {vmIcon} selected (on {total, number})',
homeSortByCpus: 'CPUs',
homeDisplayedItems: '{displayed, number}x {icon} (on {total, number})',
homeSelectedItems: '{selected, number}x {icon} selected (on {total, number})',
homeMore: 'More',
homeMigrateTo: 'Migrate to…',
homeMissingPaths: 'Missing patches',
highAvailability: 'High Availability',
// ----- Forms -----
add: 'Add',
@@ -120,11 +129,17 @@ var messages = {
selectPifs: 'Select PIF(s)…',
selectPools: 'Select Pool(s)…',
selectRemotes: 'Select Remote(s)…',
selectResourceSets: 'Select resource set(s)…',
selectResourceSetsVmTemplate: 'Select template(s)…',
selectResourceSetsSr: 'Select SR(s)…',
selectResourceSetsNetwork: 'Select network(s)…',
selectResourceSetsVdi: 'Select disk(s)…',
selectSrs: 'Select SR(s)…',
selectVms: 'Select VM(s)…',
selectVmTemplates: 'Select VM template(s)…',
selectTags: 'Select tag(s)…',
selectVdis: 'Select disk(s)…',
selectTimezone: 'Select timezone…',
fillRequiredInformations: 'Fill required informations.',
fillOptionalInformations: 'Fill informations (optional)',
selectTableReset: 'Reset',
@@ -132,24 +147,24 @@ var messages = {
// --- Dates/Scheduler ---
schedulingMonth: 'Month',
schedulingEveryMonth: 'Every month',
schedulingEachSelectedMonth: 'Each selected month',
schedulingMonthDay: 'Day of the month',
schedulingEveryMonthDay: 'Every day',
schedulingEachSelectedMonthDay: 'Each selected day',
schedulingWeekDay: 'Day of the week',
schedulingEveryWeekDay: 'Every day',
schedulingEachSelectedWeekDay: 'Each selected day',
schedulingHour: 'Hour',
schedulingEveryHour: 'Every hour',
schedulingEveryNHour: 'Every N hour',
schedulingEachSelectedHour: 'Each selected hour',
schedulingMinute: 'Minute',
schedulingEveryMinute: 'Every minute',
schedulingEveryNMinute: 'Every N minute',
schedulingEachSelectedMinute: 'Each selected minute',
schedulingReset: 'Reset',
unknownSchedule: 'Unknown',
timezonePickerServerValue: 'Xo-server timezone:',
timezonePickerUseLocalTime: 'Web browser timezone',
timezonePickerUseServerTime: 'Xo-server timezone',
serverTimezoneOption: 'Server timezone ({value})',
cronPattern: 'Cron Pattern:',
backupEditNotFoundTitle: 'Cannot edit backup',
backupEditNotFoundMessage: 'Missing required info for edition',
job: 'Job',
@@ -163,6 +178,8 @@ var messages = {
jobTag: 'Tag',
jobScheduling: 'Scheduling',
jobState: 'State',
jobTimezone: 'Timezone',
jobServerTimezone: 'xo-server',
runJob: 'Run job',
runJobVerbose: 'One shot running started. See overview for logs.',
jobStarted: 'Started',
@@ -188,8 +205,17 @@ var messages = {
remoteTypeNfs: 'NFS',
remoteTypeSmb: 'SMB',
remoteType: 'Type',
remoteTestTip: 'Test your remote',
testRemote: 'Test Remote',
remoteTestFailure: 'Test failed for {name}',
remoteTestSuccess: 'Test passed for {name}',
remoteTestError: 'Error',
remoteTestStep: 'Test Step',
remoteTestFile: 'Test file',
remoteTestSuccessMessage: 'The remote appears to work correctly',
// ------ New Storage -----
newSrTitle: 'Create a new SR',
newSrGeneral: 'General',
newSrTypeSelection: 'Select Strorage Type:',
newSrSettings: 'Settings',
@@ -253,6 +279,25 @@ var messages = {
cancelPluginEdition: 'Cancel',
pluginConfigurationSuccess: 'Plugin configuration',
pluginConfigurationChanges: 'Plugin configuration successfully saved!',
pluginConfigurationPresetTitle: 'Predefined configuration',
pluginConfigurationChoosePreset: 'Choose a predefined configuration.',
applyPluginPreset: 'Apply',
// ----- User preferences -----
saveNewUserFilterErrorTitle: 'Save filter error',
saveNewUserFilterErrorBody: 'Bad parameter: name must be given.',
filterName: 'Name:',
filterValue: 'Value:',
saveNewFilterTitle: 'Save new filter',
setUserFiltersTitle: 'Set custom filters',
setUserFiltersBody: 'Are you sure you want to set custom filters?',
removeUserFilterTitle: 'Remove custom filter',
removeUserFilterBody: 'Are you sure you want to remove custom filter?',
defaultFilter: 'Default filter',
defaultFilters: 'Default filters',
customFilters: 'Custom filters',
customizeFilters: 'Customize filters',
saveCustomFilters: 'Save custom filters',
// ----- VM actions ------
startVmLabel: 'Start',
@@ -366,7 +411,7 @@ var messages = {
patchNameLabel: 'Name',
patchUpdateButton: 'Install all patches',
patchDescription: 'Description',
patchApplied: 'Release date',
patchApplied: 'Applied date',
patchSize: 'Size',
patchStatus: 'Status',
patchStatusApplied: 'Applied',
@@ -375,9 +420,12 @@ var messages = {
patchReleaseDate: 'Release date',
patchGuidance: 'Guidance',
patchAction: 'Action',
hostInstalledPatches: 'Downloaded patches',
hostAppliedPatches: 'Applied patches',
hostMissingPatches: 'Missing patches',
hostUpToDate: 'Host up-to-date!',
// ----- Pool patch tabs -----
refreshPatches: 'Refresh patches',
installPoolPatches: 'Install pool patches',
// ----- VM tabs -----
generalTabName: 'General',
@@ -413,6 +461,7 @@ var messages = {
statsCpu: 'CPU usage',
statsMemory: 'Memory usage',
statsNetwork: 'Network throughput',
useStackedValuesOnStats: 'Stacked values',
statDisk: 'Disk throughput',
statLastTenMinutes: 'Last 10 minutes',
statLastTwoHours: 'Last 2 hours',
@@ -435,6 +484,7 @@ var messages = {
vdiTags: 'Tags',
vdiSize: 'Size',
vdiSr: 'SR',
vdiVm: 'VM',
vdbBootableStatus: 'Boot flag',
vdbStatus: 'Status',
vbdStatusConnected: 'Connected',
@@ -452,6 +502,7 @@ var messages = {
vifStatusConnected: 'Connected',
vifStatusDisconnected: 'Disconnected',
vifIpAddresses: 'IP addresses',
vifMacAutoGenerate: 'Auto-generated if empty',
// ----- VM snapshot tab -----
noSnapshots: 'No snapshots',
@@ -478,7 +529,9 @@ var messages = {
uuid: 'UUID',
virtualizationMode: 'Virtualization mode',
cpuWeightLabel: 'CPU weight',
defaultCpuWeight: 'Default',
defaultCpuWeight: 'Default ({value, number})',
cpuCapLabel: 'CPU cap',
defaultCpuCap: 'Default ({value, number})',
pvArgsLabel: 'PV args',
xenToolsStatus: 'Xen tools status',
xenToolsStatusValue: {
@@ -516,7 +569,7 @@ var messages = {
taskStatePanel: 'Pending tasks',
usersStatePanel: 'Users',
srStatePanel: 'Storage state',
ofUsage: 'of',
ofUsage: '{usage} (of {total})',
noSrs: 'No storage',
srName: 'Name',
srPool: 'Pool',
@@ -528,6 +581,7 @@ var messages = {
srFree: 'free',
srUsageStatePanel: 'Storage Usage',
srTopUsageStatePanel: 'Top 5 SR Usage (in %)',
vmsStates: '{running} running ({halted} halted)',
// --- Stats board --
weekHeatmapData: '{value} {date, date, medium}',
@@ -561,7 +615,9 @@ var messages = {
alarmRemoveAll: 'Remove all alarms',
// ----- New VM -----
newVmCreateNewVmOn: 'Create a new VM on {pool}',
newVmCreateNewVmOn: 'Create a new VM on {select}',
newVmCreateNewVmOn2: 'Create a new VM on {select1} or {select2}',
newVmCreateNewVmNoPermission: 'You have no permission to create a VM',
newVmInfoPanel: 'Infos',
newVmNameLabel: 'Name',
newVmTemplateLabel: 'Template',
@@ -592,20 +648,24 @@ var messages = {
newVmBootAfterCreate: 'Boot VM after creation',
newVmMacPlaceholder: 'Auto-generated if empty',
newVmCpuWeightLabel: 'CPU weight',
newVmCpuWeightQuarter: 'Quarter (1/4)',
newVmCpuWeightHalf: 'Half (1/2)',
newVmCpuWeightNormal: 'Normal',
newVmCpuWeightDouble: 'Double (x2)',
newVmDefaultCpuWeight: 'Default: {value, number}',
newVmCpuCapLabel: 'CPU cap',
newVmDefaultCpuCap: 'Default: {value, number}',
newVmCloudConfig: 'Cloud config',
newVmCreateVms: 'Create VMs',
newVmCreateVmsConfirm: 'Are you sure you want to create {nbVms} VMs?',
newVmMultipleVms: 'Multiple VMs:',
newVmSelectResourceSet: 'Select a resource set:',
newVmMultipleVmsPattern: 'Name pattern:',
newVmMultipleVmsPatternPlaceholder: 'e.g.: \\{name\\}_%',
newVmFirstIndex: 'First index:',
// ----- Self -----
resourceSets: 'Resource sets',
noResourceSets: 'No resource sets.',
resourceSetName: 'Resource set name',
resourceSetCreation: 'Creation and edition',
recomputeResourceSets: 'Recompute all limits',
saveResourceSet: 'Save',
resetResourceSet: 'Reset',
editResourceSet: 'Edit',
@@ -664,10 +724,26 @@ var messages = {
importBackupMessage: 'Starting your backup import',
// ----- Modals -----
emergencyShutdownHostsModalTitle: 'Emergency shutdown Host{nHosts, plural, one {} other {s}}',
emergencyShutdownHostsModalMessage: 'Are you sure you want to shutdown {nHosts} Host{nHosts, plural, one {} other {s}}?',
stopHostModalTitle: 'Shutdown host',
stopHostModalMessage: 'This will shutdown your host. Do you want to continue?',
restartHostModalTitle: 'Restart host',
restartHostModalMessage: 'This will restart your host. Do you want to continue?',
restartHostsAgentsModalTitle: 'Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}',
restartHostsAgentsModalMessage: 'Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?',
restartHostsModalTitle: 'Restart Host{nHosts, plural, one {} other {s}}',
restartHostsModalMessage: 'Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}}?',
startVmsModalTitle: 'Start VM{vms, plural, one {} other {s}}',
startVmsModalMessage: 'Are you sure you want to start {vms} VM{vms, plural, one {} other {s}}?',
stopHostsModalTitle: 'Stop Host{nHosts, plural, one {} other {s}}',
stopHostsModalMessage: 'Are you sure you want to stop {nHosts} Host{nHosts, plural, one {} other {s}}?',
stopVmsModalTitle: 'Stop VM{vms, plural, one {} other {s}}',
stopVmsModalMessage: 'Are you sure you want to stop {vms} VM{vms, plural, one {} other {s}}?',
restartVmModalTitle: 'Restart VM',
restartVmModalMessage: 'Are you sure you want to restart {name}?',
stopVmModalTitle: 'Stop VM',
stopVmModalMessage: 'Are you sure you want to stop {name}?',
restartVmsModalTitle: 'Restart VM{vms, plural, one {} other {s}}',
restartVmsModalMessage: 'Are you sure you want to restart {vms} VM{vms, plural, one {} other {s}}?',
snapshotVmsModalTitle: 'Snapshot VM{vms, plural, one {} other {s}}',
@@ -677,15 +753,20 @@ var messages = {
deleteVmModalMessage: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
deleteVmsModalMessage: 'Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
migrateVmModalTitle: 'Migrate VM',
migrateVmModalBody: 'Are you sure you want to migrate this VM to {hostName}?',
migrateVmAdvancedModalSelectHost: 'Select a destination host:',
migrateVmAdvancedModalSelectNetwork: 'Select a migration network:',
migrateVmAdvancedModalSelectSrs: 'For each VDI, select an SR:',
migrateVmAdvancedModalSelectNetworks: 'For each VIF, select a network:',
migrateVmAdvancedModalName: 'Name',
migrateVmAdvancedModalSr: 'SR',
migrateVmAdvancedModalVif: 'VIF',
migrateVmAdvancedModalNetwork: 'Network',
migrateVmSelectHost: 'Select a destination host:',
migrateVmSelectMigrationNetwork: 'Select a migration network:',
migrateVmSelectSrs: 'For each VDI, select an SR:',
migrateVmSelectNetworks: 'For each VIF, select a network:',
migrateVmsSelectSr: 'Select a destination SR:',
migrateVmsSelectSrIntraPool: 'Select a destination SR for local disks:',
migrateVmsSelectNetwork: 'Select a network on which to connect each VIF:',
migrateVmsSmartMapping: 'Smart mapping',
migrateVmName: 'Name',
migrateVmSr: 'SR',
migrateVmVif: 'VIF',
migrateVmNetwork: 'Network',
migrateVmNoTargetHost: 'No target host',
migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',
importBackupModalTitle: 'Import a {name} Backup',
importBackupModalStart: 'Start VM after restore',
importBackupModalSelectBackup: 'Select your backup…',
@@ -713,9 +794,13 @@ var messages = {
copyVm: 'Copy VM',
copyVmConfirm: 'Are you sure you want to copy this VM to {SR}?',
copyVmName: 'Name',
copyVmNamePattern: 'Name pattern',
copyVmNamePlaceholder: 'If empty: name of the copied VM',
copyVmNamePatternPlaceholder: 'e.g.: "\\{name\\}_COPY"',
copyVmSelectSr: 'Select SR',
copyVmCompress: 'Use compression',
copyVmsNoTargetSr: 'No target SR',
copyVmsNoTargetSrMessage: 'A target SR is required to copy a VM',
// ----- Network -----
newNetworkCreate: 'Create network',
@@ -759,6 +844,7 @@ var messages = {
availableIn: 'This feature is available starting from {plan} Edition',
// ----- Updates View -----
updateTitle: 'Updates',
registration: 'Registration',
trial: 'Trial',
settings: 'Settings',
@@ -780,6 +866,8 @@ var messages = {
mustUpgrade: 'You need to update your XOA (new version is available)',
registerNeeded: 'Your XOA is not registered for updates',
updaterError: 'Can\'t fetch update information',
promptUpgradeReloadTitle: 'Upgrade successful',
promptUpgradeReloadMessage: 'Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?',
// ----- OS Disclaimer -----
disclaimerTitle: 'Xen Orchestra from the sources',
@@ -793,7 +881,36 @@ var messages = {
disconnectPif: 'Disconnect PIF',
disconnectPifConfirm: 'Are you sure you want to disconnect this PIF?',
deletePif: 'Delete PIF',
deletePifConfirm: 'Are you sure you want to delete this PIF?'
deletePifConfirm: 'Are you sure you want to delete this PIF?',
// ----- User -----
username: 'Username',
password: 'Password',
language: 'Language',
oldPasswordPlaceholder: 'Old password',
newPasswordPlaceholder: 'New password',
confirmPasswordPlaceholder: 'Confirm new password',
confirmationPasswordError: 'Confirmation password incorrect',
confirmationPasswordErrorBody: 'Password does not match the confirm password.',
pwdChangeSuccess: 'Password changed',
pwdChangeSuccessBody: 'Your password has been successfully changed.',
pwdChangeError: 'Incorrect password',
pwdChangeErrorBody: 'The old password provided is incorrect. Your password has not been changed.',
changePasswordOk: 'OK',
sshKeys: 'SSH keys',
newSshKey: 'New SSH key',
deleteSshKey: 'Delete',
noSshKeys: 'No SSH keys',
newSshKeyModalTitle: 'New SSH key',
sshKeyErrorTitle: 'Invalid key',
sshKeyErrorMessage: 'An SSH key requires both a title and a key.',
title: 'Title',
key: 'Key',
deleteSshKeyConfirm: 'Delete SSH key',
deleteSshKeyConfirmMessage: 'Are you sure you want to delete the SSH key {title}?',
// ----- Usage -----
others: 'Others'
}
forEach(messages, function (message, id) {
if (isString(message)) {

View File

@@ -2,6 +2,8 @@ import React from 'react'
import ActionButton from './action-button'
import Component from './base-component'
import propTypes from './prop-types'
import { connectStore } from './utils'
import { SelectVdi } from './select-objects'
import {
createGetObjectsOfType,
@@ -9,10 +11,6 @@ import {
createGetObject,
createSelector
} from './selectors'
import {
connectStore,
propTypes
} from './utils'
import {
ejectCd,
insertCd

View File

@@ -1,7 +1,6 @@
import { Component } from 'react'
import {
propTypes
} from 'utils'
import propTypes from '../prop-types'
// ===================================================================

View File

@@ -1,16 +1,12 @@
import _ from 'intl'
import React, { Component, cloneElement } from 'react'
import map from 'lodash/map'
import filter from 'lodash/filter'
import {
autobind,
propsEqual,
propTypes
} from 'utils'
import _ from '../intl'
import propTypes from '../prop-types'
import { propsEqual } from '../utils'
import GenericInput from './generic-input'
import {
descriptionRender,
forceDisplayOptionalAttr
@@ -76,15 +72,13 @@ export default class ArrayInput extends Component {
})
}
@autobind
_handleOptionalChange (event) {
_handleOptionalChange = event => {
this.setState({
use: event.target.checked
})
}
@autobind
_handleAdd () {
_handleAdd = () => {
const { children } = this.state
this.setState({
children: children.concat(this._makeChild(this.props))

View File

@@ -23,7 +23,7 @@ export default class BooleanInput extends AbstractInput {
<PrimitiveInputWrapper {...props}>
<div className='checkbox form-control'>
<Toggle
defaultValue={props.defaultValue || props.schema.default}
defaultValue={props.defaultValue}
disabled={props.disabled}
onChange={props.onChange}
ref='input'

View File

@@ -1,10 +1,7 @@
import React, { Component } from 'react'
import includes from 'lodash/includes'
import {
EMPTY_OBJECT,
propTypes
} from 'utils'
import propTypes from '../prop-types'
import { EMPTY_OBJECT } from '../utils'
import ArrayInput from './array-input'
import BooleanInput from './boolean-input'
@@ -13,55 +10,18 @@ import IntegerInput from './integer-input'
import NumberInput from './number-input'
import ObjectInput from './object-input'
import StringInput from './string-input'
import XoHighLevelObjectInput from './xo-highlevel-object-input'
import XoHostInput from './xo-host-input'
import XoPoolInput from './xo-pool-input'
import XoRemoteInput from './xo-remote-input'
import XoRoleInput from './xo-role-input'
import XoSrInput from './xo-sr-input'
import XoSubjectInput from './xo-subject-input'
import XoVmInput from './xo-vm-input'
import { getType } from './helpers'
// ===================================================================
const getType = (schema, attr = 'type') => {
if (!schema) {
return
}
const type = schema[attr]
if (Array.isArray(type)) {
if (includes(type, 'integer')) {
return 'integer'
}
if (includes(type, 'number')) {
return 'number'
}
return 'string'
}
return type
}
const getXoType = schema => getType(schema, 'xo:type')
const InputByType = {
array: ArrayInput,
boolean: BooleanInput,
host: XoHostInput,
integer: IntegerInput,
number: NumberInput,
object: ObjectInput,
pool: XoPoolInput,
remote: XoRemoteInput,
sr: XoSrInput,
string: StringInput,
vm: XoVmInput,
xoobject: XoHighLevelObjectInput,
role: XoRoleInput,
subject: XoSubjectInput
string: StringInput
}
// ===================================================================
@@ -88,12 +48,14 @@ export default class GenericInput extends Component {
render () {
const {
schema,
defaultValue = schema.default,
uiSchema = EMPTY_OBJECT,
...opts
} = this.props
const props = {
...opts,
defaultValue,
schema,
uiSchema,
ref: 'input'
@@ -104,14 +66,13 @@ export default class GenericInput extends Component {
return <EnumInput {...props} />
}
// $type = Job Creation Schemas && Old XO plugins.
const type = getXoType(schema) || getType(schema, '$type') || getType(schema)
const type = getType(schema)
const Input = uiSchema.widget || InputByType[type.toLowerCase()]
if (!Input) {
throw new Error(`Unsupported type: ${type}.`)
}
return <Input {...props} />
return <Input {...props} {...uiSchema.config} />
}
}

View File

@@ -1,10 +1,43 @@
import React from 'react'
import includes from 'lodash/includes'
import isArray from 'lodash/isArray'
import marked from 'marked'
import { Col, Row } from 'grid'
// ===================================================================
export const getType = schema => {
if (!schema) {
return
}
const type = schema.type
if (isArray(type)) {
if (includes(type, 'integer')) {
return 'integer'
}
if (includes(type, 'number')) {
return 'number'
}
return 'string'
}
return type
}
export const getXoType = schema => {
const type = schema && (schema['xo:type'] || schema.$type)
if (type) {
return type.toLowerCase()
}
}
// ===================================================================
export const descriptionRender = description =>
<span className='text-muted' dangerouslySetInnerHTML={{__html: marked(description || '')}} />

View File

@@ -1,2 +1 @@
import GenericInput from './generic-input'
export default GenericInput
export default from './generic-input'

View File

@@ -1,6 +1,7 @@
import React from 'react'
import AbstractInput from './abstract-input'
import Combobox from '../combobox'
import { PrimitiveInputWrapper } from './helpers'
// ===================================================================
@@ -20,16 +21,16 @@ export default class IntegerInput extends AbstractInput {
render () {
const { props } = this
const { onChange } = props
const { schema } = props
return (
<PrimitiveInputWrapper {...props}>
<input
className='form-control'
defaultValue={props.defaultValue || ''}
<Combobox
defaultValue={props.defaultValue}
disabled={props.disabled}
onChange={onChange && (event => onChange(event.target.value))}
placeholder={props.placeholder}
onChange={props.onChange}
options={schema.defaults}
placeholder={props.placeholder || schema.default}
ref='input'
required={props.required}
step={1}

View File

@@ -1,6 +1,7 @@
import React from 'react'
import AbstractInput from './abstract-input'
import Combobox from '../combobox'
import { PrimitiveInputWrapper } from './helpers'
// ===================================================================
@@ -20,16 +21,16 @@ export default class NumberInput extends AbstractInput {
render () {
const { props } = this
const { onChange } = props
const { schema } = props
return (
<PrimitiveInputWrapper {...props}>
<input
className='form-control'
defaultValue={props.defaultValue || ''}
<Combobox
defaultValue={props.defaultValue}
disabled={props.disabled}
onChange={onChange && (event => onChange(event.target.value))}
placeholder={props.placeholder}
onChange={props.onChange}
options={schema.defaults}
placeholder={props.placeholder || schema.default}
ref='input'
required={props.required}
step='any'

View File

@@ -4,11 +4,8 @@ import forEach from 'lodash/forEach'
import includes from 'lodash/includes'
import map from 'lodash/map'
import {
autobind,
propsEqual,
propTypes
} from 'utils'
import propTypes from '../prop-types'
import { propsEqual } from '../utils'
import GenericInput from './generic-input'
@@ -81,8 +78,7 @@ export default class ObjectInput extends Component {
})
}
@autobind
_handleOptionalChange (event) {
_handleOptionalChange = event => {
const { checked } = event.target
this.setState({
@@ -98,6 +94,7 @@ export default class ObjectInput extends Component {
defaultValue = {}
} = props
const obj = {}
const { properties } = uiSchema
forEach(schema.properties, (childSchema, key) => {
obj[key] = (
@@ -108,7 +105,7 @@ export default class ObjectInput extends Component {
label={childSchema.title || key}
required={includes(schema.required, key)}
schema={childSchema}
uiSchema={uiSchema.properties}
uiSchema={properties && properties[key]}
defaultValue={defaultValue[key]}
/>
</ObjectItem>

View File

@@ -1,26 +1,31 @@
import React from 'react'
import AbstractInput from './abstract-input'
import Combobox from '../combobox'
import propTypes from '../prop-types'
import { PrimitiveInputWrapper } from './helpers'
// ===================================================================
@propTypes({
password: propTypes.bool
})
export default class StringInput extends AbstractInput {
render () {
const { props } = this
const { onChange } = props
const { schema } = props
return (
<PrimitiveInputWrapper {...props}>
<input
className='form-control'
defaultValue={props.defaultValue || ''}
<Combobox
defaultValue={props.defaultValue}
disabled={props.disabled}
onChange={onChange && (event => onChange(event.target.value))}
placeholder={props.placeholder}
onChange={props.onChange}
options={schema.defaults}
placeholder={props.placeholder || schema.default}
ref='input'
required={props.required}
type={props.schema['xo:type'] === 'password' ? 'password' : 'text'}
type={props.password && 'password'}
/>
</PrimitiveInputWrapper>
)

59
src/common/link.js Normal file
View File

@@ -0,0 +1,59 @@
import Link from 'react-router/lib/Link'
import React from 'react'
import { routerShape } from 'react-router/lib/PropTypes'
import Component from './base-component'
import propTypes from './prop-types'
// ===================================================================
export { Link as default }
// -------------------------------------------------------------------
const _IGNORED_TAGNAMES = {
A: true,
BUTTON: true,
INPUT: true,
SELECT: true
}
@propTypes({
tagName: propTypes.string
})
export class BlockLink extends Component {
static contextTypes = {
router: routerShape
}
_style = { cursor: 'pointer' }
_onClickCapture = event => {
const { currentTarget } = event
let element = event.target
while (element !== currentTarget) {
if (_IGNORED_TAGNAMES[element.tagName]) {
return
}
element = element.parentNode
}
event.stopPropagation()
if (event.ctrlKey || event.button === 1) {
window.open(this.context.router.createHref(this.props.to))
} else {
this.context.router.push(this.props.to)
}
}
render () {
const { children, tagName = 'div' } = this.props
const Component = tagName
return (
<Component
style={this._style}
onClickCapture={this._onClickCapture}
>
{children}
</Component>
)
}
}

View File

@@ -4,7 +4,8 @@ import isArray from 'lodash/isArray'
import isString from 'lodash/isString'
import React, { Component, cloneElement } from 'react'
import { Button, Modal as ReactModal } from 'react-bootstrap-4/lib'
import { propTypes } from './utils'
import propTypes from './prop-types'
let instance

View File

@@ -1,7 +1,8 @@
import classNames from 'classnames'
import Link from 'react-router/lib/Link'
import React from 'react'
import Link from './link'
export const NavLink = ({ children, to }) => (
<li className='nav-item' role='tab'>
<Link className='nav-link' activeClassName='active' to={to}>

View File

@@ -1,6 +1,5 @@
import React, { Component } from 'react'
import { createBackoff } from 'jsonrpc-websocket-client'
import { propTypes } from 'utils'
import { RFB } from 'novnc-node'
import {
format as formatUrl,
@@ -8,6 +7,8 @@ import {
resolve as resolveUrl
} from 'url'
import propTypes from './prop-types'
const parseRelativeUrl = url => parseUrl(resolveUrl(String(window.location), url))
const PROTOCOL_ALIASES = {

View File

@@ -1,12 +1,13 @@
import Icon from 'icon'
import React, { Component } from 'react'
import { createGetObject } from 'selectors'
import Icon from './icon'
import propTypes from './prop-types'
import { createGetObject } from './selectors'
import { isSrWritable } from './xo'
import {
connectStore,
formatSize,
propTypes
} from 'utils'
formatSize
} from './utils'
// ===================================================================
@@ -53,7 +54,7 @@ export const SrItem = propTypes({
})(({ sr, container }) => {
let label = `${sr.name_label || sr.id}`
if (sr.content_type === 'user') {
if (isSrWritable(sr)) {
label += ` (${formatSize(sr.size)})`
}
@@ -107,6 +108,11 @@ const xoItemToRender = {
<Icon icon='user' /> {user.email}
</span>
),
resourceSet: resourceSet => (
<span>
<Icon icon='resource-set' /> {resourceSet.name}
</span>
),
// XO objects.
pool: pool => (
@@ -132,6 +138,13 @@ const xoItemToRender = {
// VM.
VM: vm => <VmItem vm={vm} />,
'VM-snapshot': vm => <VmItem vm={vm} />,
'VM-controller': vm => (
<span>
<Icon icon='host' />
{' '}
<VmItem vm={vm} />
</span>
),
// PIF.
PIF: pif => (

View File

@@ -1,30 +1,32 @@
import Component from 'base-component'
import React from 'react'
import _ from 'intl'
import forEach from 'lodash/forEach'
import includes from 'lodash/includes'
import join from 'lodash/join'
import later from 'later'
import map from 'lodash/map'
import React from 'react'
import sortedIndex from 'lodash/sortedIndex'
import { Range } from 'form'
import { FormattedTime } from 'react-intl'
import { Col, Row } from 'grid'
import {
Panel,
Tab,
Tabs
} from 'react-bootstrap-4/lib'
import {
propTypes
} from 'utils'
import _ from './intl'
import Component from './base-component'
import propTypes from './prop-types'
import TimezonePicker from './timezone-picker'
import { Card, CardHeader, CardBlock } from './card'
import { Col, Row } from './grid'
import { Range } from './form'
// ===================================================================
const NAV_EVERY = 1
const NAV_EACH_SELECTED = 2
const NAV_EVERY_N = 3
// By default later use UTC but we use this line for futures versions.
later.date.UTC()
// ===================================================================
const NAV_EACH_SELECTED = 1
const NAV_EVERY_N = 2
const MIN_PREVIEWS = 5
const MAX_PREVIEWS = 20
@@ -80,13 +82,29 @@ const MINS = (() => {
return minutes
})()
const PICKTIME_TO_ID = {
minute: 0,
hour: 1,
monthDay: 2,
month: 3,
weekDay: 4
}
const TIME_FORMAT = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric'
minute: 'numeric',
// The timezone is not significant for displaying the date previews
// as long as it is the same used to generate the next occurrences
// from the cron patterns.
// Therefore we can use UTC everywhere and say to the user that the
// previews are in the configured timezone.
timeZone: 'UTC'
}
// ===================================================================
@@ -103,7 +121,7 @@ const getDayName = (dayNum) =>
// ===================================================================
@propTypes({
cron: propTypes.string.isRequired
cronPattern: propTypes.string.isRequired
})
export class SchedulePreview extends Component {
_handleChange = value => {
@@ -113,12 +131,15 @@ export class SchedulePreview extends Component {
}
render () {
const { props } = this
const cronSched = later.parse.cron(props.cron)
const { cronPattern } = this.props
const cronSched = later.parse.cron(cronPattern)
const dates = later.schedule(cronSched).next(this.state.value || MIN_PREVIEWS)
return (
<div>
<div className='alert alert-info' role='alert'>
{_('cronPattern')} <strong>{cronPattern}</strong>
</div>
<div className='form-inline p-b-1'>
<Range min={MIN_PREVIEWS} max={MAX_PREVIEWS} onChange={this._handleChange} />
</div>
@@ -139,34 +160,21 @@ export class SchedulePreview extends Component {
@propTypes({
children: propTypes.any.isRequired,
onChange: propTypes.func
onChange: propTypes.func.isRequired,
tdId: propTypes.number.isRequired,
value: propTypes.bool.isRequired
})
class ToggleTd extends Component {
get value () {
return this.state.value
}
set value (value) {
const { onChange } = this.props
this.setState({
value
}, onChange && (() => onChange(value)))
}
_onClick = () => {
const { onChange } = this.props
const value = !this.state.value
this.setState({
value
}, onChange && (() => onChange(value)))
const { props } = this
props.onChange(props.tdId, !props.value)
}
render () {
const { props } = this
return (
<td style={{ cursor: 'pointer' }} className={this.state.value ? 'table-success' : ''} onClick={this._onClick}>
{this.props.children}
<td style={{ cursor: 'pointer' }} className={props.value ? 'table-success' : ''} onClick={this._onClick}>
{props.children}
</td>
)
}
@@ -175,79 +183,64 @@ class ToggleTd extends Component {
// ===================================================================
@propTypes({
data: propTypes.array.isRequired,
dataRender: propTypes.func,
onChange: propTypes.func
options: propTypes.array.isRequired,
optionsRenderer: propTypes.func,
onChange: propTypes.func.isRequired,
value: propTypes.array.isRequired
})
class TableSelect extends Component {
constructor () {
super()
this.state = {
value: []
}
}
get value () {
return this.state.value
}
set value (value) {
const { onChange } = this.props
forEach(this.refs, (ref, id) => {
// Don't call ref.input directly because onChange of each ToggleTd is called else!
ref.setState({
value: includes(value, +id)
})
})
this.setState({
value
}, onChange && (() => onChange(value)))
static defaultProps = {
optionsRenderer: value => value
}
_reset = () => {
this.value = []
this.props.onChange([])
}
_handleChange = (id, value) => {
const { onChange } = this.props
const newValue = this.state.value.slice()
_handleChange = (tdId, tdValue) => {
const { props } = this
if (value) {
newValue.splice(sortedIndex(newValue, id), 0, id)
const newValue = props.value.slice()
const index = sortedIndex(newValue, tdId)
if (tdValue) {
// Add
if (newValue[index] !== tdId) {
newValue.splice(index, 0, tdId)
}
} else {
newValue.splice(sortedIndex(newValue, id), 1)
// Remove
if (newValue[index] === tdId) {
newValue.splice(index, 1)
}
}
this.setState({
value: newValue
}, onChange && (() => onChange(newValue)))
props.onChange(newValue)
}
render () {
const dataRender = this.props.dataRender || ((value) => value)
const {
props: {
data
}
} = this
const { length } = data[0]
options,
optionsRenderer,
value
} = this.props
const { length } = options[0]
return (
<div>
<table className='table table-bordered table-sm'>
<tbody>
{map(data, (line, i) => (
{map(options, (line, i) => (
<tr key={i}>
{map(line, (value, j) => {
const id = length * i + j
{map(line, (tdOption, j) => {
const tdId = length * i + j
return (
<ToggleTd
key={id}
ref={id}
children={dataRender(value)}
onChange={(value) => { this._handleChange(id, value) }}
children={optionsRenderer(tdOption)}
tdId={tdId}
key={tdId}
onChange={this._handleChange}
value={includes(value, tdId)}
/>
)
})}
@@ -266,177 +259,148 @@ class TableSelect extends Component {
// ===================================================================
@propTypes({
dataRender: propTypes.func,
onChange: propTypes.func,
optionsRenderer: propTypes.func,
onChange: propTypes.func.isRequired,
range: propTypes.array,
type: propTypes.string.isRequired
labelId: propTypes.string.isRequired,
value: propTypes.any.isRequired
})
class TimePicker extends Component {
constructor () {
super()
this.state = {
activeKey: NAV_EVERY
activeKey: NAV_EACH_SELECTED,
tableValue: []
}
}
get value () {
const { activeKey } = this.state
_update (props) {
const { refs } = this
const { value } = props
if (activeKey === NAV_EVERY) {
return 'all'
}
if (activeKey === NAV_EACH_SELECTED) {
return refs.select.value
}
return refs.range.value
}
set value (value) {
const { refs } = this
const { onChange } = this.props
if (value === 'all') {
this.setState({
activeKey: NAV_EVERY
}, onChange && (() => onChange(value)))
} else if (Array.isArray(value)) {
this.setState({
activeKey: NAV_EACH_SELECTED
})
refs.select.value = value
} else {
if (value.indexOf('/') === 1) {
this.setState({
activeKey: NAV_EVERY_N
})
refs.range.value = value
refs.range.value = value.split('/')[1]
} else {
this.setState({
activeKey: NAV_EACH_SELECTED,
tableValue: value === '*'
? []
: map(value.split(','), e => +e)
})
}
}
_updateOpen = () => {
this.setState({
open: !this.state.open
})
componentWillMount () {
this._update(this.props)
}
componentWillReceiveProps (props) {
this._update(props)
}
_selectTab = activeKey => {
const { onChange } = this.props
this.setState({
activeKey
}, onChange && (() => onChange(this.value)))
}, () => {
const { activeKey, tableValue } = this.state
const { onChange } = this.props
const { refs } = this
if (activeKey === NAV_EACH_SELECTED) {
onChange(tableValue)
} else {
onChange(refs.range.value)
}
})
}
_handleTableValue = tableValue => {
this.setState({
tableValue
}, () => this.props.onChange(tableValue))
}
render () {
const {
props,
state
} = this
const {
onChange,
options,
optionsRenderer,
range,
type
} = props
labelId
} = this.props
const { tableValue } = this.state
const tableSelect = (
<TableSelect
onChange={this._handleTableValue}
options={options}
optionsRenderer={optionsRenderer}
value={tableValue}
/>
)
return (
<div className='card'>
<button className='card-header btn btn-lg btn-block' onClick={this._updateOpen}>
{_(`scheduling${type}`)}
</button>
<Panel collapsible expanded={state.open}>
<div className='card-block'>
<Tabs bsStyle='tabs' activeKey={state.activeKey} onSelect={this._selectTab}>
<Tab tabClassName='nav-item' eventKey={NAV_EVERY} title={_(`schedulingEvery${type}`)} />
<Tab tabClassName='nav-item' eventKey={NAV_EACH_SELECTED} title={_(`schedulingEachSelected${type}`)}>
<TableSelect ref='select' data={props.data} dataRender={props.dataRender} onChange={onChange} />
<Card>
<CardHeader>
{_(`scheduling${labelId}`)}
</CardHeader>
<CardBlock>
{range
? (
<Tabs bsStyle='tabs' activeKey={this.state.activeKey} onSelect={this._selectTab}>
<Tab tabClassName='nav-item' eventKey={NAV_EACH_SELECTED} title={_(`schedulingEachSelected${labelId}`)}>
{tableSelect}
</Tab>
<Tab tabClassName='nav-item' eventKey={NAV_EVERY_N} title={_(`schedulingEveryN${labelId}`)}>
<Range ref='range' min={range[0]} max={range[1]} onChange={onChange} />
</Tab>
{range &&
<Tab tabClassName='nav-item' eventKey={NAV_EVERY_N} title={_(`schedulingEveryN${type}`)}>
<Range ref='range' min={range[0]} max={range[1]} onChange={onChange} />
</Tab>}
</Tabs>
</div>
</Panel>
</div>
) : tableSelect
}
</CardBlock>
</Card>
)
}
}
// ===================================================================
const ID_TO_PICKTIME = [
'minute',
'hour',
'monthDay',
'month',
'weekDay'
]
const HOURS_RANGE = [2, 12]
const MINUTES_RANGE = [2, 30]
@propTypes({
onChange: propTypes.func
cronPattern: propTypes.string.isRequired,
onChange: propTypes.func,
timezone: propTypes.string
})
export default class Scheduler extends Component {
constructor () {
super()
this.cron = {
minute: '*',
hour: '*',
monthDay: '*',
month: '*',
weekDay: '*'
}
}
get value () {
const { cron } = this
return `${cron.minute} ${cron.hour} ${cron.monthDay} ${cron.month} ${cron.weekDay}`
}
set value (value) {
if (!value) {
value = '* * * * *'
}
forEach(value.split(' '), (t, id) => {
const ref = this.refs[ID_TO_PICKTIME[id]]
if (t === '*') {
ref.value = 'all'
} else if (t.indexOf('/') === 1) {
ref.value = t.split('/')[1]
} else {
ref.value = map(t.split(','), e => +e)
}
})
}
_update (type, value) {
const { cron } = this
const { onChange } = this.props
if (value === 'all') {
cron[type] = '*'
} else if (Array.isArray(value)) {
if (Array.isArray(value)) {
if (!value.length) {
cron[type] = '*'
value = '*'
} else {
cron[type] = join(
value = join(
(type === 'monthDay' || type === 'month')
? map(value, (n) => n + 1)
? map(value, n => n + 1)
: value,
','
)
}
} else {
cron[type] = `*/${value}`
value = `*/${value}`
}
if (onChange) {
onChange(this.value)
}
const { props } = this
const cronPattern = props.cronPattern.split(' ')
cronPattern[PICKTIME_TO_ID[type]] = value
this.props.onChange({
cronPattern: cronPattern.join(' '),
timezone: props.timezone
})
}
_onHourChange = value => this._update('hour', value)
@@ -445,49 +409,69 @@ export default class Scheduler extends Component {
_onMonthDayChange = value => this._update('monthDay', value)
_onWeekDayChange = value => this._update('weekDay', value)
_onTimezoneChange = timezone => {
const { props } = this
props.onChange({
cronPattern: props.cronPattern,
timezone
})
}
render () {
const {
cronPattern,
timezone
} = this.props
const cronPatternArr = cronPattern.split(' ')
return (
<div className='card-block'>
<Row>
<Col mediumSize={6}>
<TimePicker
ref='month'
type='Month'
dataRender={getMonthName}
data={MONTHS}
labelId='Month'
optionsRenderer={getMonthName}
options={MONTHS}
onChange={this._onMonthChange}
value={cronPatternArr[PICKTIME_TO_ID['month']]}
/>
<TimePicker
ref='monthDay'
type='MonthDay'
data={DAYS}
labelId='MonthDay'
options={DAYS}
onChange={this._onMonthDayChange}
value={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
/>
<TimePicker
ref='weekDay'
type='WeekDay'
dataRender={getDayName}
data={WEEK_DAYS}
labelId='WeekDay'
optionsRenderer={getDayName}
options={WEEK_DAYS}
onChange={this._onWeekDayChange}
value={cronPatternArr[PICKTIME_TO_ID['weekDay']]}
/>
</Col>
<Col mediumSize={6}>
<TimePicker
ref='hour'
type='Hour'
data={HOURS}
range={[2, 12]}
labelId='Hour'
options={HOURS}
range={HOURS_RANGE}
onChange={this._onHourChange}
value={cronPatternArr[PICKTIME_TO_ID['hour']]}
/>
<TimePicker
ref='minute'
type='Minute'
data={MINS}
range={[2, 30]}
labelId='Minute'
options={MINS}
range={MINUTES_RANGE}
onChange={this._onMinuteChange}
value={cronPatternArr[PICKTIME_TO_ID['minute']]}
/>
</Col>
</Row>
<Row>
<Col>
<hr />
<TimezonePicker value={timezone} onChange={this._onTimezoneChange} />
</Col>
</Row>
</div>
)
}

View File

@@ -1,38 +1,42 @@
import Component from 'base-component'
import React from 'react'
import _ from 'intl'
import assign from 'lodash/assign'
import classNames from 'classnames'
import filter from 'lodash/filter'
import flatten from 'lodash/flatten'
import forEach from 'lodash/forEach'
import groupBy from 'lodash/groupBy'
import keyBy from 'lodash/keyBy'
import keys from 'lodash/keys'
import map from 'lodash/map'
import renderXoItem from 'render-xo-item'
import sortBy from 'lodash/sortBy'
import store from 'store'
import { parse as parseRemote } from 'xo-remote-parser'
import { Select } from 'form'
import _ from './intl'
import Component from './base-component'
import propTypes from './prop-types'
import renderXoItem from './render-xo-item'
import { Select } from './form'
import {
createFilter,
createGetObjectsOfType,
createGetTags,
createSelector
} from 'selectors'
createSelector,
getObject
} from './selectors'
import {
connectStore,
mapPlus,
propTypes
} from 'utils'
resolveResourceSets
} from './utils'
import {
isSrWritable,
subscribeGroups,
subscribeRemotes,
subscribeResourceSets,
subscribeRoles,
subscribeUsers
} from 'xo'
} from './xo'
// ===================================================================
@@ -325,11 +329,9 @@ export const SelectPool = makeStoreSelect(() => ({
// ===================================================================
const userSrPredicate = sr => sr.content_type === 'user'
export const SelectSr = makeStoreSelect(() => {
const getSrsByContainer = createGetObjectsOfType('SR').filter(
(_, { predicate }) => predicate || userSrPredicate
(_, { predicate }) => predicate || isSrWritable
).sort().groupBy('$container')
const getContainerIds = createSelector(
@@ -583,3 +585,184 @@ export const SelectRemote = makeSubscriptionSelect(subscriber => {
return unsubscribeRemotes
}, { placeholder: _('selectRemotes') })
// ===================================================================
export const SelectResourceSet = makeSubscriptionSelect(subscriber => {
const unsubscribeResourceSets = subscribeResourceSets(resourceSets => {
const xoObjects = map(sortBy(resolveResourceSets(resourceSets), 'name'), resourceSet => ({...resourceSet, type: 'resourceSet'}))
subscriber({xoObjects})
})
return unsubscribeResourceSets
}, { placeholder: _('selectResourceSets') })
// ===================================================================
export class SelectResourceSetsVmTemplate extends Component {
get value () {
return this.refs.select.value
}
set value (value) {
this.refs.select.value = value
}
componentWillMount () {
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
this.setState({
resourceSets: resolveResourceSets(resourceSets)
})
})
}
_getTemplates = createSelector(
() => this.props.resourceSet,
({ objectsByType }) => {
const { predicate } = this.props
const templates = objectsByType['VM-template']
return sortBy(predicate ? filter(templates, predicate) : templates, 'name_label')
}
)
render () {
return (
<GenericSelect
ref='select'
placeholder={_('selectResourceSetsVmTemplate')}
{...this.props}
xoObjects={this._getTemplates()}
/>
)
}
}
// ===================================================================
export class SelectResourceSetsSr extends Component {
get value () {
return this.refs.select.value
}
set value (value) {
this.refs.select.value = value
}
componentWillMount () {
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
this.setState({
resourceSets: resolveResourceSets(resourceSets)
})
})
}
_getSrs = createSelector(
() => this.props.resourceSet,
({ objectsByType }) => {
const { predicate } = this.props
const srs = objectsByType['SR']
return sortBy(predicate ? filter(srs, predicate) : srs, 'name_label')
}
)
render () {
return (
<GenericSelect
ref='select'
placeholder={_('selectResourceSetsSr')}
{...this.props}
xoObjects={this._getSrs()}
/>
)
}
}
// ===================================================================
export class SelectResourceSetsVdi extends Component {
get value () {
return this.refs.select.value
}
set value (value) {
this.refs.select.value = value
}
componentWillMount () {
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
this.setState({
resourceSets: resolveResourceSets(resourceSets)
})
})
}
_getObject (id) {
return getObject(store.getState(), id, true)
}
_getSrs = createSelector(
() => this.props.resourceSet,
({ objectsByType }) => {
const { srPredicate } = this.props
const srs = objectsByType['SR']
return srPredicate ? filter(srs, srPredicate) : srs
}
)
_getVdis = createSelector(
this._getSrs,
srs => sortBy(map(flatten(map(srs, sr => sr.VDIs)), this._getObject), 'name_label')
)
render () {
return (
<GenericSelect
ref='select'
placeholder={_('selectResourceSetsVdi')}
{...this.props}
xoObjects={this._getVdis()}
/>
)
}
}
// ===================================================================
export class SelectResourceSetsNetwork extends Component {
get value () {
return this.refs.select.value
}
set value (value) {
this.refs.select.value = value
}
componentWillMount () {
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
this.setState({
resourceSets: resolveResourceSets(resourceSets)
})
})
}
_getNetworks = createSelector(
() => this.props.resourceSet,
({ objectsByType }) => {
const { predicate } = this.props
const networks = objectsByType['network']
return sortBy(predicate ? filter(networks, predicate) : networks, 'name_label')
}
)
render () {
return (
<GenericSelect
ref='select'
placeholder={_('selectResourceSetsNetwork')}
{...this.props}
xoObjects={this._getNetworks()}
/>
)
}
}

View File

@@ -13,8 +13,9 @@ import size from 'lodash/size'
import slice from 'lodash/slice'
import { createSelector as create } from 'reselect'
import invoke from './invoke'
import shallowEqual from './shallow-equal'
import { EMPTY_ARRAY, EMPTY_OBJECT, invoke } from './utils'
import { EMPTY_ARRAY, EMPTY_OBJECT } from './utils'
// ===================================================================
@@ -244,13 +245,18 @@ const _getPermissionsPredicate = invoke(() => {
// Creates an object selector from an id selector.
export const createGetObject = (idSelector = _getId) =>
(state, props) => {
(state, props, useResourceSet) => {
const object = state.objects.all[idSelector(state, props)]
if (!object) {
return
}
if (useResourceSet) {
return object
}
const predicate = _getPermissionsPredicate(state)
if (!predicate) {
if (predicate == null) {
return object // no filtering
@@ -324,14 +330,18 @@ const _extendCollectionSelector = (selector, objectsType) => {
}
_addSort(selector)
// groupBy and sort can be chained.
selector.pick = idsSelector => _addGroupBy(_addSort(
createPicker(selector, idsSelector)
))
// count, groupBy and sort can be chained.
selector.filter = predicate => _addCount(_addGroupBy(_addSort(
createFilter(selector, predicate)
const _addFilter = selector => {
selector.filter = predicate => _addCount(_addGroupBy(_addSort(
createFilter(selector, predicate)
)))
return selector
}
_addFilter(selector)
// filter, groupBy and sort can be chained.
selector.pick = idsSelector => _addFilter(_addGroupBy(_addSort(
createPicker(selector, idsSelector)
)))
return selector
@@ -350,7 +360,7 @@ const _extendCollectionSelector = (selector, objectsType) => {
// - groupBy: returns a selector which returns the objects grouped by
// a value determined by a getter selector
// - pick: returns a selector which returns only the objects with given
// ids (groupBy and sort can be chained)
// ids (filter, groupBy and sort can be chained)
// - sort: returns a selector which returns the objects appropriately
// sorted (groupBy can be chained)
export const createGetObjectsOfType = type => {
@@ -403,3 +413,24 @@ export const createGetObjectMessages = objectSelector =>
// const object = getObject(store.getState(), objectId)
// ...
export const getObject = createGetObject((_, id) => id)
export const createGetHostMetrics = hostSelector => _createCollectionWrapper(
create(
hostSelector,
hosts => {
const metrics = {
count: 0,
cpus: 0,
memoryTotal: 0,
memoryUsage: 0
}
forEach(hosts, host => {
metrics.count++
metrics.cpus += host.cpus.cores
metrics.memoryTotal += host.memory.size
metrics.memoryUsage += host.memory.usage
})
return metrics
}
)
)

View File

@@ -1,8 +1,9 @@
import React, { cloneElement } from 'react'
import { propTypes } from 'utils'
import propTypes from './prop-types'
const SINGLE_LINE_STYLE = { display: 'flex' }
const COL_STYLE = { margin: 'auto' }
const COL_STYLE = { marginTop: 'auto', marginBottom: 'auto' }
const SingleLineRow = propTypes({
className: propTypes.string

View File

@@ -1,17 +1,24 @@
import Component from 'base-component'
import Icon from 'icon'
import React from 'react'
import SingleLineRow from 'single-line-row'
import _ from 'intl'
import ceil from 'lodash/ceil'
import debounce from 'lodash/debounce'
import isEmpty from 'lodash/isEmpty'
import isFunction from 'lodash/isFunction'
import map from 'lodash/map'
import { Pagination } from 'react-bootstrap-4/lib'
import React from 'react'
import { Dropdown, MenuItem, Pagination } from 'react-bootstrap-4/lib'
import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabricator.babeljs.io/T6662 so Dropdown.Menu won't work like https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
import DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
import { Portal } from 'react-overlays'
import { Container, Col } from 'grid'
import { create as createMatcher } from 'complex-matcher'
import { propTypes } from 'utils'
import Component from '../base-component'
import Icon from '../icon'
import propTypes from '../prop-types'
import SingleLineRow from '../single-line-row'
import { BlockLink } from '../link'
import { Container, Col } from '../grid'
import { create as createMatcher } from '../complex-matcher'
import {
createCounter,
createFilter,
createPager,
createSelector,
@@ -23,14 +30,19 @@ import styles from './index.css'
// ===================================================================
@propTypes({
filters: propTypes.object,
nFilteredItems: propTypes.number.isRequired,
nItems: propTypes.number.isRequired,
onChange: propTypes.func.isRequired
})
class TableFilter extends Component {
_cleanFilter = () => {
_cleanFilter = () => this._setFilter('')
_setFilter = filterValue => {
const { filter } = this.refs
filter.value = ''
filter.value = filterValue
filter.focus()
this.props.onChange('')
this.props.onChange(filterValue)
}
_onChange = event => {
@@ -38,9 +50,27 @@ class TableFilter extends Component {
}
render () {
const { props } = this
return (
<div className='input-group'>
<span className='input-group-addon'><Icon icon='search' /></span>
<span className='input-group-addon'>{props.nFilteredItems} / {props.nItems}</span>
{isEmpty(props.filters)
? <span className='input-group-addon'><Icon icon='search' /></span>
: <div className='input-group-btn'>
<Dropdown id='filter'>
<DropdownToggle bsStyle='info'>
<Icon icon='search' />
</DropdownToggle>
<DropdownMenu>
{map(props.filters, (filter, label) =>
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
{_(label)}
</MenuItem>
)}
</DropdownMenu>
</Dropdown>
</div>}
<input
type='text'
ref='filter'
@@ -118,8 +148,13 @@ const DEFAULT_ITEMS_PER_PAGE = 10
sortOrder: propTypes.string
})).isRequired,
filterContainer: propTypes.func,
filters: propTypes.object,
itemsPerPage: propTypes.number,
paginationContainer: propTypes.func,
rowLink: propTypes.oneOfType([
propTypes.func,
propTypes.string
]),
userData: propTypes.any
})
export default class SortedTable extends Component {
@@ -134,6 +169,10 @@ export default class SortedTable extends Component {
this._getSelectedColumn = () =>
this.props.columns[this.state.selectedColumn]
this._getTotalNumberOfItems = createCounter(
() => this.props.collection
)
this._getAllItems = createSort(
createFilter(
() => this.props.collection,
@@ -142,7 +181,14 @@ export default class SortedTable extends Component {
createMatcher
)
),
() => this._getSelectedColumn().sortCriteria,
createSelector(
() => this._getSelectedColumn().sortCriteria,
() => this.props.userData,
(sortCriteria, userData) =>
(typeof sortCriteria === 'function')
? object => sortCriteria(object, userData)
: sortCriteria
),
() => this.state.sortOrder
)
@@ -156,7 +202,9 @@ export default class SortedTable extends Component {
}
componentWillMount () {
this._sort(this.state.selectedColumn)
this.setState({
sortOrder: this.props.columns[this.state.selectedColumn].sortOrder === 'desc' ? 'desc' : 'asc'
})
}
componentDidMount () {
@@ -203,9 +251,13 @@ export default class SortedTable extends Component {
const {
paginationContainer,
filterContainer,
filters,
rowLink,
userData
} = props
const nFilteredItems = this._getAllItems().length
const paginationInstance = (
<Pagination
first
@@ -215,14 +267,19 @@ export default class SortedTable extends Component {
ellipsis
boundaryLinks
maxButtons={10}
items={ceil(this._getAllItems().length / state.itemsPerPage)}
items={ceil(nFilteredItems / state.itemsPerPage)}
activePage={this.state.activePage}
onSelect={this._onPageSelection}
/>
)
const filterInstance = (
<TableFilter onChange={this._onFilterChange} />
<TableFilter
filters={filters}
nFilteredItems={nFilteredItems}
nItems={this._getTotalNumberOfItems()}
onChange={this._onFilterChange}
/>
)
return (
@@ -242,15 +299,23 @@ export default class SortedTable extends Component {
</tr>
</thead>
<tbody>
{map(this._getVisibleItems(), (item, key) => (
<tr key={key}>
{map(props.columns, (column, key) => (
<td key={key}>
{column.itemRenderer(item, userData)}
</td>
))}
</tr>
))}
{map(this._getVisibleItems(), (item, i) => {
const colums = map(props.columns, (column, key) => (
<td key={key}>
{column.itemRenderer(item, userData)}
</td>
))
const { id = i } = item
return rowLink
? <BlockLink
key={id}
tagName='tr'
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
>{colums}</BlockLink>
: <tr key={id}>{colums}</tr>
})}
</tbody>
</table>
{(!paginationContainer || !filterContainer) && (

View File

@@ -1,8 +1,9 @@
import _ from 'intl'
import ActionButton from 'action-button'
import Icon from 'icon'
import React from 'react'
import { Link } from 'react-router'
import _ from './intl'
import ActionButton from './action-button'
import Icon from './icon'
import Link from './link'
const STYLE = {
marginBottom: '1em',

View File

@@ -1,9 +1,9 @@
import React from 'react'
import Icon from 'icon'
import map from 'lodash/map'
import Component from './base-component'
import { propTypes } from './utils'
import Icon from './icon'
import propTypes from './prop-types'
@propTypes({
labels: propTypes.arrayOf(React.PropTypes.string).isRequired,

View File

@@ -0,0 +1,106 @@
import ActionButton from 'action-button'
import map from 'lodash/map'
import moment from 'moment-timezone'
import React from 'react'
import _ from './intl'
import Component from './base-component'
import propTypes from './prop-types'
import { getXoServerTimezone } from './xo'
import { Select } from './form'
const XO_SERVER_TIMEZONE = 'xo-server'
@propTypes({
defaultValue: propTypes.string,
onChange: propTypes.func.isRequired,
value: propTypes.string
})
export default class TimezonePicker extends Component {
constructor (props) {
super(props)
this.state.options = map(moment.tz.names(), value => ({ label: value, value }))
}
get value () {
const value = this.refs.select.value
return (value === XO_SERVER_TIMEZONE) ? null : value
}
set value (value) {
this.refs.select.value = value || XO_SERVER_TIMEZONE
}
_updateTimezone (value) {
this.props.onChange(value)
}
_handleChange = option => {
return this._updateTimezone(
!option || option.value === XO_SERVER_TIMEZONE
? null
: option.value
)
}
_useServerTime = () => {
this._updateTimezone(null)
}
_useLocalTime = () => {
this._updateTimezone(moment.tz.guess())
}
componentWillMount () {
// Use local timezone (Web browser) if no default value.
if (this.props.value === undefined) {
this._useLocalTime()
}
getXoServerTimezone.then(serverTimezone => {
this.setState({
options: [{
label: _('serverTimezoneOption', {
value: serverTimezone
}),
value: XO_SERVER_TIMEZONE
}].concat(this.state.options),
serverTimezone
})
})
}
render () {
const { props, state } = this
return (
<div>
<div className='alert alert-info' role='alert'>
{_('timezonePickerServerValue')} <strong>{state.serverTimezone}</strong>
</div>
<Select
className='m-b-1'
defaultValue={props.defaultValue}
onChange={this._handleChange}
options={state.options}
placeholder={_('selectTimezone')}
ref='select'
value={props.value || XO_SERVER_TIMEZONE}
/>
<div className='pull-right'>
<ActionButton
btnStyle='primary'
className='m-r-1'
handler={this._useServerTime}
icon='time'
>
{_('timezonePickerUseServerTime')}
</ActionButton>
<ActionButton
btnStyle='secondary'
handler={this._useLocalTime}
icon='time'
>
{_('timezonePickerUseLocalTime')}
</ActionButton>
</div>
</div>
)
}
}

View File

@@ -0,0 +1,287 @@
// Source: https://github.com/wwayne/react-tooltip/blob/master/src/utils/getPosition.js
/**
* Calculate the position of tooltip
*
* @params
* - `e` {Event} the event of current mouse
* - `target` {Element} the currentTarget of the event
* - `node` {DOM} the react-tooltip object
* - `place` {String} top / right / bottom / left
* - `effect` {String} float / solid
* - `offset` {Object} the offset to default position
*
* @return {Object
* - `isNewState` {Bool} required
* - `newState` {Object}
* - `position` {OBject} {left: {Number}, top: {Number}}
*/
export default function (e, target, node, place, effect, offset) {
const tipWidth = node.clientWidth
const tipHeight = node.clientHeight
const {mouseX, mouseY} = getCurrentOffset(e, target, effect)
const defaultOffset = getDefaultPosition(effect, target.clientWidth, target.clientHeight, tipWidth, tipHeight)
const {extraOffsetX, extraOffsetY} = calculateOffset(offset)
const windowWidth = window.innerWidth
const windowHeight = window.innerHeight
const {parentTop, parentLeft} = getParent(target)
// Get the edge offset of the tooltip
const getTipOffsetLeft = (place) => {
const offsetX = defaultOffset[place].l
return mouseX + offsetX + extraOffsetX
}
const getTipOffsetRight = (place) => {
const offsetX = defaultOffset[place].r
return mouseX + offsetX + extraOffsetX
}
const getTipOffsetTop = (place) => {
const offsetY = defaultOffset[place].t
return mouseY + offsetY + extraOffsetY
}
const getTipOffsetBottom = (place) => {
const offsetY = defaultOffset[place].b
return mouseY + offsetY + extraOffsetY
}
// Judge if the tooltip has over the window(screen)
const outsideVertical = () => {
let result = false
let newPlace
if (getTipOffsetTop('left') < 0 &&
getTipOffsetBottom('left') <= windowHeight &&
getTipOffsetBottom('bottom') <= windowHeight) {
result = true
newPlace = 'bottom'
} else if (getTipOffsetBottom('left') > windowHeight &&
getTipOffsetTop('left') >= 0 &&
getTipOffsetTop('top') >= 0) {
result = true
newPlace = 'top'
}
return {result, newPlace}
}
const outsideLeft = () => {
let {result, newPlace} = outsideVertical() // Deal with vertical as first priority
if (result && outsideHorizontal().result) {
return {result: false} // No need to change, if change to vertical will out of space
}
if (!result && getTipOffsetLeft('left') < 0 && getTipOffsetRight('right') <= windowWidth) {
result = true // If vertical ok, but let out of side and right won't out of side
newPlace = 'right'
}
return {result, newPlace}
}
const outsideRight = () => {
let {result, newPlace} = outsideVertical()
if (result && outsideHorizontal().result) {
return {result: false} // No need to change, if change to vertical will out of space
}
if (!result && getTipOffsetRight('right') > windowWidth && getTipOffsetLeft('left') >= 0) {
result = true
newPlace = 'left'
}
return {result, newPlace}
}
const outsideHorizontal = () => {
let result = false
let newPlace
if (getTipOffsetLeft('top') < 0 &&
getTipOffsetRight('top') <= windowWidth &&
getTipOffsetRight('right') <= windowWidth) {
result = true
newPlace = 'right'
} else if (getTipOffsetRight('top') > windowWidth &&
getTipOffsetLeft('top') >= 0 &&
getTipOffsetLeft('left') >= 0) {
result = true
newPlace = 'left'
}
return {result, newPlace}
}
const outsideTop = () => {
let {result, newPlace} = outsideHorizontal()
if (result && outsideVertical().result) {
return {result: false}
}
if (!result && getTipOffsetTop('top') < 0 && getTipOffsetBottom('bottom') <= windowHeight) {
result = true
newPlace = 'bottom'
}
return {result, newPlace}
}
const outsideBottom = () => {
let {result, newPlace} = outsideHorizontal()
if (result && outsideVertical().result) {
return {result: false}
}
if (!result && getTipOffsetBottom('bottom') > windowHeight && getTipOffsetTop('top') >= 0) {
result = true
newPlace = 'top'
}
return {result, newPlace}
}
// Return new state to change the placement to the reverse if possible
const outsideLeftResult = outsideLeft()
const outsideRightResult = outsideRight()
const outsideTopResult = outsideTop()
const outsideBottomResult = outsideBottom()
if (place === 'left' && outsideLeftResult.result) {
return {
isNewState: true,
newState: {place: outsideLeftResult.newPlace}
}
} else if (place === 'right' && outsideRightResult.result) {
return {
isNewState: true,
newState: {place: outsideRightResult.newPlace}
}
} else if (place === 'top' && outsideTopResult.result) {
return {
isNewState: true,
newState: {place: outsideTopResult.newPlace}
}
} else if (place === 'bottom' && outsideBottomResult.result) {
return {
isNewState: true,
newState: {place: outsideBottomResult.newPlace}
}
}
// Return tooltip offset position
return {
isNewState: false,
position: {
left: getTipOffsetLeft(place) - parentLeft,
top: getTipOffsetTop(place) - parentTop
}
}
}
// Get current mouse offset
const getCurrentOffset = (e, currentTarget, effect) => {
const boundingClientRect = currentTarget.getBoundingClientRect()
const targetTop = boundingClientRect.top
const targetLeft = boundingClientRect.left
const targetWidth = currentTarget.clientWidth
const targetHeight = currentTarget.clientHeight
if (effect === 'float') {
return {
mouseX: e.clientX,
mouseY: e.clientY
}
}
return {
mouseX: targetLeft + (targetWidth / 2),
mouseY: targetTop + (targetHeight / 2)
}
}
// List all possibility of tooltip final offset
// This is useful in judging if it is necessary for tooltip to switch position when out of window
const getDefaultPosition = (effect, targetWidth, targetHeight, tipWidth, tipHeight) => {
let top
let right
let bottom
let left
const disToMouse = 3
const triangleHeight = 2
const cursorHeight = 12 // Optimize for float bottom only, cause the cursor will hide the tooltip
if (effect === 'float') {
top = {
l: -(tipWidth / 2),
r: tipWidth / 2,
t: -(tipHeight + disToMouse + triangleHeight),
b: -disToMouse
}
bottom = {
l: -(tipWidth / 2),
r: tipWidth / 2,
t: disToMouse + cursorHeight,
b: tipHeight + disToMouse + triangleHeight + cursorHeight
}
left = {
l: -(tipWidth + disToMouse + triangleHeight),
r: -disToMouse,
t: -(tipHeight / 2),
b: tipHeight / 2
}
right = {
l: disToMouse,
r: tipWidth + disToMouse + triangleHeight,
t: -(tipHeight / 2),
b: tipHeight / 2
}
} else if (effect === 'solid') {
top = {
l: -(tipWidth / 2),
r: tipWidth / 2,
t: -(targetHeight / 2 + tipHeight + triangleHeight),
b: -(targetHeight / 2)
}
bottom = {
l: -(tipWidth / 2),
r: tipWidth / 2,
t: targetHeight / 2,
b: targetHeight / 2 + tipHeight + triangleHeight
}
left = {
l: -(tipWidth + targetWidth / 2 + triangleHeight),
r: -(targetWidth / 2),
t: -(tipHeight / 2),
b: tipHeight / 2
}
right = {
l: targetWidth / 2,
r: tipWidth + targetWidth / 2 + triangleHeight,
t: -(tipHeight / 2),
b: tipHeight / 2
}
}
return {top, bottom, left, right}
}
// Consider additional offset into position calculation
const calculateOffset = (offset) => {
let extraOffsetX = 0
let extraOffsetY = 0
if (Object.prototype.toString.apply(offset) === '[object String]') {
offset = JSON.parse(offset.toString().replace(/'/g, '"'))
}
for (let key in offset) {
if (key === 'top') {
extraOffsetY -= parseInt(offset[key], 10)
} else if (key === 'bottom') {
extraOffsetY += parseInt(offset[key], 10)
} else if (key === 'left') {
extraOffsetX -= parseInt(offset[key], 10)
} else if (key === 'right') {
extraOffsetX += parseInt(offset[key], 10)
}
}
return {extraOffsetX, extraOffsetY}
}
// Get the offset of the parent elements
const getParent = (currentTarget) => {
let currentParent = currentTarget
while (currentParent) {
if (currentParent.style.transform.length > 0) break
currentParent = currentParent.parentElement
}
const parentTop = currentParent && currentParent.getBoundingClientRect().top || 0
const parentLeft = currentParent && currentParent.getBoundingClientRect().left || 0
return {parentTop, parentLeft}
}

View File

@@ -1,45 +1,20 @@
.container {
position: relative;
}
.common {
opacity: 0;
transition: opacity .3s;
visibility: hidden;
}
.container:hover .common {
visibility: visible;
opacity: 1;
z-index: 9999;
}
.arrow {
composes: common;
border-bottom: .5em solid rgba(0, 0, 0, .8);
border-left: .5em solid transparent;
border-right: .5em solid transparent;
font-size: 1rem;
left: 25%;
margin-left: 1em;
position: absolute;
top: 100%;
}
.tooltip {
composes: common;
background: #333;
background: rgba(0, 0, 0, .8);
border-radius: .25em;
.tooltipEnabled {
background-color: #222;
border-radius: 3px;
border: 1px solid $fff;
color: #fff;
font-size: 1rem;
left: 25%;
margin-top: .5em;
padding: .5em;
position: absolute;
top: 100%;
min-width: fit-content;
max-width: 20em;
display: inline-block;
font-size: 13px;
margin-left: 0px;
margin-top: 0px;
opacity: 0.9;
padding: 8px 21px;
pointer-events: none;
position: fixed;
transition: opacity 0.3s ease-out, margin-top 0.3s ease-out, margin-left 0.3s ease-out;
z-index: 999;
}
.tooltipDisabled {
display: none;
}

View File

@@ -1,30 +1,147 @@
import classNames from 'classnames'
import React, { PropTypes } from 'react'
import isString from 'lodash/isString'
import React from 'react'
import ReactDOM from 'react-dom'
import Component from '../base-component'
import getPosition from './get-position'
import propTypes from '../prop-types'
import styles from './index.css'
const Tooltip = ({
children,
className,
content,
style,
tagName: Component = 'span'
}) => (
<Component className={classNames(className, styles.container)} style={style}>
<div className={styles.arrow} />
<div className={styles.tooltip}>
{content}
</div>
{children}
</Component>
)
// ===================================================================
Tooltip.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
content: PropTypes.any.isRequired,
style: PropTypes.object,
tagName: PropTypes.string
let instance
export class TooltipViewer extends Component {
constructor () {
super()
if (instance) {
throw new Error('Tooltip viewer is a singleton!')
}
instance = this
this.state.place = 'top'
}
render () {
const {
className,
content,
place,
show,
style
} = this.state
return (
<div
className={classNames(show ? styles.tooltipEnabled : styles.tooltipDisabled, className)}
style={{
marginTop: (place === 'top' && '-10px') || (place === 'bottom' && '10px'),
marginLeft: (place === 'left' && '-10px') || (place === 'right' && '10px'),
...style
}}
>
{content}
</div>
)
}
}
export { Tooltip as default }
// ===================================================================
@propTypes({
children: propTypes.oneOfType([
propTypes.element,
propTypes.string
]),
className: propTypes.string,
content: propTypes.node,
style: propTypes.object,
tagName: propTypes.string
})
export default class Tooltip extends Component {
componentDidMount () {
this._addListeners()
}
componentWillUnmount () {
this._removeListeners()
}
componentWillReceiveProps (props) {
if (props.children !== this.props.children) {
this._removeListeners()
}
}
componentDidUpdate (prevProps) {
if (prevProps.children !== this.props.children) {
this._addListeners()
}
}
_addListeners () {
const node = this._node = ReactDOM.findDOMNode(this)
node.addEventListener('mouseenter', this._showTooltip)
node.addEventListener('mouseleave', this._hideTooltip)
node.addEventListener('mousemove', this._updateTooltip)
}
_removeListeners () {
const node = this._node
if (!node) {
return
}
node.removeEventListener('mouseenter', this._showTooltip)
node.removeEventListener('mouseleave', this._hideTooltip)
node.removeEventListener('mousemove', this._updateTooltip)
this._node = null
}
_showTooltip = () => {
const { props } = this
instance.setState({
className: props.className,
content: props.content,
show: true,
style: props.style
})
}
_hideTooltip = () => {
instance.setState({ show: false })
}
_updateTooltip = event => {
const node = ReactDOM.findDOMNode(instance)
const result = getPosition(event, event.currentTarget, node, instance.state.place, 'solid', {})
if (result.isNewState) {
return instance.setState(result.newState, () => this._updateTooltip(event))
}
const { position } = result
node.style.left = `${position.left}px`
node.style.top = `${position.top}px`
}
render () {
const { children } = this.props
if (!children) {
return <span />
}
if (isString(children)) {
return <span>{children}</span>
}
return children
}
}

52
src/common/usage/index.js Normal file
View File

@@ -0,0 +1,52 @@
import _ from 'intl'
import classNames from 'classnames'
import React, { PropTypes, cloneElement } from 'react'
import sum from 'lodash/sum'
import Tooltip from '../tooltip'
const Usage = ({ total, children }) => {
const limit = total / 400
const othersValues = React.Children.map(children, child => {
const { value } = child.props
return value < limit && value
})
const othersTotal = sum(othersValues)
return <span className='usage'>
{React.Children.map(children, (child, index) =>
child.props.value > limit && cloneElement(child, { total })
)}
<Element
others
tooltip={_('others')}
total={total}
value={othersTotal}
/>
</span>
}
Usage.propTypes = {
total: PropTypes.number.isRequired
}
export { Usage as default }
const Element = ({ highlight, href, others, tooltip, total, value }) => (
<Tooltip content={tooltip}>
<a
href={href}
className={classNames(
'usage-element',
highlight && 'usage-element-highlight',
others && 'usage-element-others'
)}
style={{ width: (value / total) * 100 + '%' }}
/>
</Tooltip>
)
Element.propTypes = {
highlight: PropTypes.bool,
href: PropTypes.string,
others: PropTypes.bool,
tooltip: PropTypes.node,
value: PropTypes.number.isRequired
}
export { Element as UsageElement }

View File

@@ -1,18 +1,22 @@
import * as actions from 'store/actions'
import escapeRegExp from 'lodash/escapeRegExp'
import every from 'lodash/every'
import forEach from 'lodash/forEach'
import humanFormat from 'human-format'
import includes from 'lodash/includes'
import isArray from 'lodash/isArray'
import isEmpty from 'lodash/isEmpty'
import isFunction from 'lodash/isFunction'
import isPlainObject from 'lodash/isPlainObject'
import isString from 'lodash/isString'
import join from 'lodash/join'
import keys from 'lodash/keys'
import map from 'lodash/map'
import mapValues from 'lodash/mapValues'
import propTypes from 'prop-types'
import React, { cloneElement } from 'react'
import React from 'react'
import replace from 'lodash/replace'
import store from 'store'
import { connect } from 'react-redux'
import { getObject } from 'selectors'
import BaseComponent from './base-component'
import invoke from './invoke'
@@ -20,8 +24,6 @@ import invoke from './invoke'
export const EMPTY_ARRAY = Object.freeze([ ])
export const EMPTY_OBJECT = Object.freeze({ })
export { propTypes }
// ===================================================================
export const ensureArray = (value) => {
@@ -78,92 +80,6 @@ export const addSubscriptions = subscriptions => Component => {
// -------------------------------------------------------------------
const _bind = (fn, thisArg) => function bound () {
return fn.apply(thisArg, arguments)
}
const _defineProperty = Object.defineProperty
export const autobind = (target, key, {
configurable,
enumerable,
value: fn,
writable
}) => ({
configurable,
enumerable,
get () {
if (this === target) {
return fn
}
const bound = _bind(fn, this)
_defineProperty(this, key, {
configurable: true,
enumerable: false,
value: bound,
writable: true
})
return bound
},
set (newValue) {
// Cannot use assignment because it will call the setter on
// the prototype.
_defineProperty(this, key, {
configurable: true,
enumerable: true,
value: newValue,
writable: true
})
}
})
// -------------------------------------------------------------------
@propTypes({
tagName: propTypes.string
})
export class BlockLink extends React.Component {
static contextTypes = {
router: React.PropTypes.object
}
_style = { cursor: 'pointer' }
_onClickCapture = event => {
const { currentTarget } = event
let element = event.target
while (element !== currentTarget) {
if (includes(['A', 'INPUT', 'BUTTON', 'SELECT'], element.tagName)) {
return
}
element = element.parentNode
}
event.stopPropagation()
if (event.ctrlKey || event.button === 1) {
window.open(this.context.router.createHref(this.props.to))
} else {
this.context.router.push(this.props.to)
}
}
render () {
const { children, tagName = 'div' } = this.props
const Component = tagName
return (
<Component
style={this._style}
onClickCapture={this._onClickCapture}
>
{children}
</Component>
)
}
}
// -------------------------------------------------------------------
export const checkPropsState = (propsNames, stateNames) => Component => {
const nProps = propsNames && propsNames.length
const nState = stateNames && stateNames.length
@@ -255,18 +171,6 @@ export const connectStore = (mapStateToProps, opts = {}) => {
// -------------------------------------------------------------------
// Simple matcher to use in object filtering.
export const createSimpleMatcher = (pattern, valueGetter) => {
if (!pattern) {
return
}
pattern = pattern.toLowerCase()
return item => valueGetter(item).toLowerCase().indexOf(pattern) !== -1
}
// -------------------------------------------------------------------
export { default as Debug } from './debug'
// -------------------------------------------------------------------
@@ -344,24 +248,6 @@ export const osFamily = invoke({
// -------------------------------------------------------------------
// Experimental!
//
// ```js
// <If cond={user}>
// <p>user.name</p>
// <p>user.email</p>
// </If>
// ```
export const If = ({ cond, children }) => cond && children
? map(children, (child, key) => cloneElement(child, { key }))
: null
// -------------------------------------------------------------------
export { invoke }
// -------------------------------------------------------------------
export const formatSize = bytes => humanFormat(bytes, { scale: 'binary', unit: 'B' })
export const formatSizeRaw = bytes => humanFormat.raw(bytes, { scale: 'binary', unit: 'B' })
@@ -471,10 +357,63 @@ export function rethrow (cb) {
)
}
// ===================================================================
export const resolveResourceSets = resourceSets => (
map(resourceSets, resourceSet => {
const { objects, ...attrs } = resourceSet
const resolvedObjects = {}
const resolvedSet = {
...attrs,
missingObjects: [],
objectsByType: resolvedObjects
}
const state = store.getState()
forEach(objects, id => {
const object = getObject(state, id, true) // true: useResourceSet to bypass permissions
// Error, missing resource.
if (!object) {
resolvedSet.missingObjects.push(id)
return
}
const { type } = object
if (!resolvedObjects[type]) {
resolvedObjects[type] = [ object ]
} else {
resolvedObjects[type].push(object)
}
})
return resolvedSet
})
)
// -------------------------------------------------------------------
// If param is an event: returns the value associated to it
// Otherwise: returns param
export function getEventValue (param) {
return param && param.target ? param.target.value : param
// Creates a string replacer based on a pattern and a list of rules
//
// ```js
// const myReplacer = buildTemplate('{name}_COPY_{name}_{id}_%', {
// '{name}': vm => vm.name_label,
// '{id}': vm => vm.id,
// '%': (_, i) => i
// })
//
// const newString = myReplacer({
// name_label: 'foo',
// id: 42,
// }, 32)
//
// newString === 'foo_COPY_foo_42_32'
// ```
export function buildTemplate (pattern, rules) {
const regExp = new RegExp(join(map(keys(rules), escapeRegExp), '|'), 'g')
return (...params) => replace(pattern, regExp, match => {
const rule = rules[match]
return isFunction(rule) ? rule(...params) : rule
})
}

View File

@@ -1,11 +1,12 @@
import _ from 'intl'
import classNames from 'classnames'
import every from 'lodash/every'
import Icon from 'icon'
import map from 'lodash/map'
import { propTypes } from 'utils'
import React, { Component, cloneElement } from 'react'
import _ from '../intl'
import Icon from '../icon'
import propTypes from '../prop-types'
import styles from './index.css'
const Wizard = ({ children }) => {

View File

@@ -0,0 +1,67 @@
import forEach from 'lodash/forEach'
import XoHighLevelObjectInput from './xo-highlevel-object-input'
import XoHostInput from './xo-host-input'
import XoPoolInput from './xo-pool-input'
import XoRemoteInput from './xo-remote-input'
import XoRoleInput from './xo-role-input'
import XoSrInput from './xo-sr-input'
import XoSubjectInput from './xo-subject-input'
import XoVmInput from './xo-vm-input'
import { getType, getXoType } from '../json-schema-input/helpers'
// ===================================================================
const XO_TYPE_TO_COMPONENT = {
host: XoHostInput,
xoobject: XoHighLevelObjectInput,
pool: XoPoolInput,
remote: XoRemoteInput,
role: XoRoleInput,
sr: XoSrInput,
subject: XoSubjectInput,
vm: XoVmInput
}
// ===================================================================
const buildStringInput = (uiSchema, key, xoType) => {
if (key === 'password') {
uiSchema.config = { password: true }
}
uiSchema.widget = XO_TYPE_TO_COMPONENT[xoType]
}
// ===================================================================
const _generateUiSchema = (schema, uiSchema, key) => {
const type = getType(schema)
if (type === 'object') {
const properties = uiSchema.properties = {}
forEach(schema.properties, (schema, key) => {
const subUiSchema = properties[key] = {}
_generateUiSchema(schema, subUiSchema, key)
})
} else if (type === 'array') {
const widget = XO_TYPE_TO_COMPONENT[getXoType(schema.items)]
if (widget) {
uiSchema.widget = widget
uiSchema.config = { multi: true }
} else {
const subUiSchema = uiSchema.items = {}
_generateUiSchema(schema.items, subUiSchema, key)
}
} else if (type === 'string') {
buildStringInput(uiSchema, key, getXoType(schema))
}
}
export const generateUiSchema = schema => {
const uiSchema = {}
_generateUiSchema(schema, uiSchema, '')
return uiSchema
}

View File

@@ -1,5 +1,5 @@
import map from 'lodash/map'
import AbstractInput from './abstract-input'
import AbstractInput from '../json-schema-input/abstract-input'
// ===================================================================

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { SelectHighLevelObject } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from './helpers'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
@@ -14,7 +14,7 @@ export default class HighLevelObjectInput extends XoAbstractInput {
<PrimitiveInputWrapper {...props}>
<SelectHighLevelObject
disabled={props.disabled}
multi={props.schema.type === 'array'}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { SelectHost } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from './helpers'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
@@ -14,7 +14,7 @@ export default class HostInput extends XoAbstractInput {
<PrimitiveInputWrapper {...props}>
<SelectHost
disabled={props.disabled}
multi={props.schema.type === 'array'}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { SelectPool } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from './helpers'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
@@ -14,7 +14,7 @@ export default class PoolInput extends XoAbstractInput {
<PrimitiveInputWrapper {...props}>
<SelectPool
disabled={props.disabled}
multi={props.schema.type === 'array'}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { SelectRemote } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from './helpers'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
@@ -14,7 +14,7 @@ export default class RemoteInput extends XoAbstractInput {
<PrimitiveInputWrapper {...props}>
<SelectRemote
disabled={props.disabled}
multi={props.schema.type === 'array'}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { SelectRole } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from './helpers'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
@@ -14,7 +14,7 @@ export default class RoleInput extends XoAbstractInput {
<PrimitiveInputWrapper {...props}>
<SelectRole
disabled={props.disabled}
multi={props.schema.type === 'array'}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { SelectSr } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from './helpers'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
@@ -14,7 +14,7 @@ export default class SrInput extends XoAbstractInput {
<PrimitiveInputWrapper {...props}>
<SelectSr
disabled={props.disabled}
multi={props.schema.type === 'array'}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { SelectSubject } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from './helpers'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
@@ -14,7 +14,7 @@ export default class SubjectInput extends XoAbstractInput {
<PrimitiveInputWrapper {...props}>
<SelectSubject
disabled={props.disabled}
multi={props.schema.type === 'array'}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}

View File

@@ -2,7 +2,7 @@ import React from 'react'
import { SelectVm } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from './helpers'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
@@ -14,7 +14,7 @@ export default class VmInput extends XoAbstractInput {
<PrimitiveInputWrapper {...props}>
<SelectVm
disabled={props.disabled}
multi={props.schema.type === 'array'}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}

View File

@@ -0,0 +1,4 @@
.dashedLine {
stroke: black;
stroke-dasharray: 4px 5px;
}

View File

@@ -1,18 +1,32 @@
import ChartistGraph from 'react-chartist'
import ChartistLegend from 'chartist-plugin-legend'
import ChartistTooltip from 'chartist-plugin-tooltip'
import map from 'lodash/map'
import React from 'react'
import size from 'lodash/size'
import values from 'lodash/values'
import { injectIntl } from 'react-intl'
import {
formatSize,
propTypes
} from 'utils'
import find from 'lodash/find'
import propTypes from '../prop-types'
import { computeArraysSum } from '../xo-stats'
import { formatSize } from '../utils'
import styles from './index.css'
// Number of labels on axis X.
const N_LABELS_X = 5
const LABEL_OFFSET_X = 40
const LABEL_OFFSET_Y = 75
const LABEL_OFFSET_Y = 85
// ===================================================================
// See xo-stats.js, data can be null.
// Return the size of the first non-null object.
const getStatsLength = stats => size(find(stats, stats => stats != null))
// ===================================================================
const makeOptions = ({ intl, nValues, endTimestamp, interval, valueTransform }) => ({
showPoint: true,
@@ -62,15 +76,29 @@ const makeLabelInterpolationFnc = (intl, nValues, endTimestamp, interval) => {
: null
}
const makeObjectSeries = (data, prefix) => {
// Supported series: xvds, vifs, pifs.
const buildSeries = ({ stats, label, addSumSeries }) => {
const series = []
for (const io in data) {
const ioData = data[io]
for (const io in stats) {
const ioData = stats[io]
for (const letter in ioData) {
const data = ioData[letter]
// See xo-stats.js, data can be null.
if (data) {
series.push({
name: `${label}${letter} (${io})`,
data
})
}
}
if (addSumSeries) {
series.push({
name: `${prefix}${letter} (${io})`,
data: ioData[letter]
name: `All ${io}`,
data: computeArraysSum(values(ioData)),
className: styles.dashedLine
})
}
}
@@ -86,22 +114,27 @@ const templateError =
// ===================================================================
export const CpuLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ data, options = {}, intl }) => {
})(({ addSumSeries, data, options = {}, intl }) => {
const stats = data.stats.cpus
const { length } = (stats && stats[0]) || {}
const length = getStatsLength(stats)
if (!length) {
return templateError
}
const series = []
const series = map(stats, (data, id) => ({
name: `Cpu${id}`,
data
}))
for (const id in stats) {
if (addSumSeries) {
series.push({
name: `Cpu${id}`,
data: stats[id]
name: 'All Cpus',
data: computeArraysSum(stats),
className: styles.dashedLine
})
}
@@ -119,7 +152,7 @@ export const CpuLineChart = injectIntl(propTypes({
interval: data.interval,
valueTransform: value => `${value}%`
}),
high: 100,
high: !addSumSeries ? 100 : stats.length * 100,
...options
}}
/>
@@ -164,11 +197,12 @@ export const MemoryLineChart = injectIntl(propTypes({
}))
export const XvdLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ data, options = {}, intl }) => {
})(({ addSumSeries, data, options = {}, intl }) => {
const stats = data.stats.xvds
const { length } = (stats && stats.r.a) || {}
const length = stats && getStatsLength(stats.r)
if (!length) {
return templateError
@@ -178,7 +212,7 @@ export const XvdLineChart = injectIntl(propTypes({
<ChartistGraph
type='Line'
data={{
series: makeObjectSeries(stats, 'Xvd')
series: buildSeries({ addSumSeries, stats, label: 'Xvd' })
}}
options={{
...makeOptions({
@@ -195,11 +229,12 @@ export const XvdLineChart = injectIntl(propTypes({
}))
export const VifLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ data, options = {}, intl }) => {
})(({ addSumSeries, data, options = {}, intl }) => {
const stats = data.stats.vifs
const { length } = (stats && stats.rx[0]) || {}
const length = stats && getStatsLength(stats.rx)
if (!length) {
return templateError
@@ -209,7 +244,7 @@ export const VifLineChart = injectIntl(propTypes({
<ChartistGraph
type='Line'
data={{
series: makeObjectSeries(stats, 'Vif')
series: buildSeries({ addSumSeries, stats, label: 'Vif' })
}}
options={{
...makeOptions({
@@ -226,11 +261,12 @@ export const VifLineChart = injectIntl(propTypes({
}))
export const PifLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ data, options = {}, intl }) => {
})(({ addSumSeries, data, options = {}, intl }) => {
const stats = data.stats.pifs
const { length } = (stats && stats.rx[0]) || {}
const length = stats && getStatsLength(stats.rx)
if (!length) {
return templateError
@@ -240,7 +276,7 @@ export const PifLineChart = injectIntl(propTypes({
<ChartistGraph
type='Line'
data={{
series: makeObjectSeries(stats, 'Pif')
series: buildSeries({ addSumSeries, stats, label: 'Pif' })
}}
options={{
...makeOptions({
@@ -282,7 +318,7 @@ export const LoadLineChart = injectIntl(propTypes({
nValues: length,
endTimestamp: data.endTimestamp,
interval: data.interval,
valueTransform: value => `${value}`
valueTransform: value => `${value.toPrecision(3)}`
}),
...options
}}

View File

@@ -0,0 +1,294 @@
import * as d3 from 'd3'
import React from 'react'
import forEach from 'lodash/forEach'
import keys from 'lodash/keys'
import map from 'lodash/map'
import times from 'lodash/times'
import Component from './base-component'
import propTypes from './prop-types'
import { setStyles } from './d3-utils'
// ===================================================================
const CHART_WIDTH = 2000
const CHART_HEIGHT = 800
const TICK_SIZE = CHART_WIDTH / 100
const N_TICKS = 4
const TOOLTIP_PADDING = 10
const DEFAULT_STROKE_WIDTH_FACTOR = 500
const HIGHLIGHT_STROKE_WIDTH_FACTOR = 200
const BRUSH_SELECTION_WIDTH = 2 * CHART_WIDTH / 100
// ===================================================================
const SVG_STYLE = {
display: 'block',
height: '100%',
left: 0,
position: 'absolute',
top: 0,
width: '100%'
}
const SVG_CONTAINER_STYLE = {
'padding-bottom': '50%',
'vertical-align': 'middle',
overflow: 'hidden',
position: 'relative',
width: '100%'
}
const SVG_CONTENT = {
'font-size': `${CHART_WIDTH / 100}px`
}
const COLUMN_TITLE_STYLE = {
'font-size': '100%',
'font-weight': 'bold',
'text-anchor': 'middle'
}
const COLUMN_VALUES_STYLE = {
'font-size': '100%'
}
const LINES_CONTAINER_STYLE = {
'stroke-opacity': 0.5,
'stroke-width': CHART_WIDTH / DEFAULT_STROKE_WIDTH_FACTOR,
fill: 'none',
stroke: 'red'
}
const TOOLTIP_STYLE = {
'fill': 'white',
'font-size': '125%',
'font-weight': 'bold'
}
// ===================================================================
@propTypes({
dataSet: propTypes.arrayOf(
propTypes.shape({
data: propTypes.object.isRequired,
label: propTypes.string.isRequired,
objectId: propTypes.string.isRequired
})
).isRequired,
labels: propTypes.object.isRequired,
renderers: propTypes.object
})
export default class XoParallelChart extends Component {
_line = d3.line()
_color = d3.scaleOrdinal(d3.schemeCategory10)
_handleBrush = () => {
// 1. Get selected brushes.
const brushes = []
this._svg.selectAll('.chartColumn')
.selectAll('.brush')
.each((_1, _2, [ brush ]) => {
if (d3.brushSelection(brush) != null) {
brushes.push(brush)
}
})
// 2. Change stroke of selected lines.
const lines = this._svg.select('.linesContainer')
.selectAll('path')
lines.each((elem, lineId, lines) => {
const { data } = elem
const res = brushes.every(brush => {
const selection = d3.brushSelection(brush)
const columnId = brush.__data__
const { invert } = this._y[columnId] // Range to domain.
return invert(selection[1]) <= data[columnId] && data[columnId] <= invert(selection[0])
})
const line = d3.select(lines[lineId])
if (!res) {
line.attr('stroke-opacity', 1.0).attr('stroke', '#e6e6e6')
} else {
line.attr('stroke-opacity', 0.5).attr('stroke', this._color(elem.label))
}
})
}
_brush = d3.brushY()
// Brush area: (x0, y0), (x1, y1)
.extent([[-BRUSH_SELECTION_WIDTH / 2, 0], [BRUSH_SELECTION_WIDTH / 2, CHART_HEIGHT]])
.on('brush', this._handleBrush)
.on('end', this._handleBrush)
_highlight (elem, position) {
const svg = this._svg
// Reset tooltip.
svg
.selectAll('.objectTooltip')
.remove()
// Reset all lines.
svg
.selectAll('.chartLine')
.attr('stroke-width', CHART_WIDTH / DEFAULT_STROKE_WIDTH_FACTOR)
if (!position) {
return
}
// Set stroke on selected line.
svg
.select('#chartLine-' + elem.objectId)
.attr('stroke-width', CHART_WIDTH / HIGHLIGHT_STROKE_WIDTH_FACTOR)
const { label } = elem
const tooltip = svg.append('g')
.attr('class', 'objectTooltip')
const bbox = tooltip.append('text')
.text(label)
.attr('x', position[0])
.attr('y', position[1] - 30)
::setStyles(TOOLTIP_STYLE)
.node().getBBox()
tooltip.insert('rect', '*')
.attr('x', bbox.x - TOOLTIP_PADDING)
.attr('y', bbox.y - TOOLTIP_PADDING)
.attr('width', bbox.width + TOOLTIP_PADDING * 2)
.attr('height', bbox.height + TOOLTIP_PADDING * 2)
.style('fill', this._color(label))
}
_handleMouseOver = (elem, pathId, paths) => {
this._highlight(elem, d3.mouse(paths[pathId]))
}
_handleMouseOut = (elem) => {
this._highlight()
}
_draw (props = this.props) {
const svg = this._svg
const { labels, dataSet } = props
const columnsIds = keys(labels)
const spacing = (CHART_WIDTH - 200) / (columnsIds.length - 1)
const x = d3.scaleOrdinal()
.domain(columnsIds).range(
times(columnsIds.length, n => n * spacing)
)
// 1. Remove old nodes.
svg
.selectAll('.chartColumn')
.remove()
svg
.selectAll('.linesContainer')
.remove()
// 2. Build Ys.
const y = this._y = {}
forEach(columnsIds, (columnId, index) => {
const max = d3.max(dataSet, elem => elem.data[columnId])
y[columnId] = d3.scaleLinear()
.domain([0, max])
.range([CHART_HEIGHT, 0])
})
// 3. Build columns.
const columns = svg.selectAll('.chartColumn')
.data(columnsIds)
.enter().append('g')
.attr('class', 'chartColumn')
.attr('transform', d => `translate(${x(d)})`)
// 4. Draw titles.
columns.append('text')
.text(columnId => labels[columnId])
.attr('y', -50)
::setStyles(COLUMN_TITLE_STYLE)
// 5. Draw axis.
columns.append('g')
.each((columnId, axisId, axes) => {
const axis = d3.axisLeft()
.ticks(N_TICKS, ',f')
.tickSize(TICK_SIZE)
.scale(y[columnId])
const renderer = props.renderers[columnId]
// Add optional renderer like formatSize.
if (renderer) {
axis.tickFormat(renderer)
}
d3.select(axes[axisId]).call(axis)
})
::setStyles(COLUMN_VALUES_STYLE)
// 6. Draw lines.
const path = elem => this._line(map(columnsIds.map(
columnId => [x(columnId), y[columnId](elem.data[columnId])]
)))
svg.append('g')
.attr('class', 'linesContainer')
::setStyles(LINES_CONTAINER_STYLE)
.selectAll('path')
.data(dataSet)
.enter().append('path')
.attr('d', path)
.attr('class', 'chartLine')
.attr('id', elem => 'chartLine-' + elem.objectId)
.attr('stroke', elem => this._color(elem.label))
.attr('shape-rendering', 'optimizeQuality')
.attr('stroke-linecap', 'round')
.attr('stroke-linejoin', 'round')
.on('mouseover', this._handleMouseOver)
.on('mouseout', this._handleMouseOut)
// 7. Brushes.
columns.append('g')
.attr('class', 'brush')
.each((_, brushId, brushes) => { d3.select(brushes[brushId]).call(this._brush) })
}
componentDidMount () {
this._svg = d3.select(this.refs.chart)
.append('div')
::setStyles(SVG_CONTAINER_STYLE)
.append('svg')
::setStyles(SVG_STYLE)
.attr('preserveAspectRatio', 'xMinYMin meet')
.attr('viewBox', `0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`)
.append('g')
.attr('transform', `translate(${100}, ${100})`)
::setStyles(SVG_CONTENT)
this._draw()
}
componentWillReceiveProps (nextProps) {
this._draw(nextProps)
}
render () {
return <div ref='chart' />
}
}

View File

@@ -1,11 +1,15 @@
import map from 'lodash/map'
import React from 'react'
import {
Sparklines,
SparklinesLine,
SparklinesSpots
} from 'react-sparklines'
import { propTypes } from 'utils'
import propTypes from './prop-types'
import {
computeArraysAvg,
computeObjectsAvg
} from './xo-stats'
const STYLE = {}
@@ -14,37 +18,6 @@ const HEIGHT = 40
// ===================================================================
function computeArraysAvg (arrays) {
if (!arrays || !arrays.length || !arrays[0].length) {
return []
}
const n = arrays[0].length
const m = arrays.length
const result = new Array(n)
for (let i = 0; i < n; i++) {
result[i] = 0
for (let j = 0; j < m; j++) {
result[i] += arrays[j][i]
}
result[i] /= m
}
return result
}
function computeObjectsAvg (objects) {
return computeArraysAvg(
map(objects, object =>
computeArraysAvg(map(object, arr => arr))
)
)
}
const templateError =
<div>
No stats.

79
src/common/xo-stats.js Normal file
View File

@@ -0,0 +1,79 @@
// ===================================================================
// Tools to manipulate rrd stats
// ===================================================================
import map from 'lodash/map'
import values from 'lodash/values'
import { mapPlus } from 'utils'
// Returns a new array with arrays sums.
// Example: computeArraysSum([[1, 2], [3, 4], [5, 0]) = [9, 6]
const _computeArraysSum = arrays => {
if (!arrays || !arrays.length || !arrays[0].length) {
return []
}
const n = arrays[0].length // N items in each array
const m = arrays.length // M arrays
const result = new Array(n)
for (let i = 0; i < n; i++) {
result[i] = 0
for (let j = 0; j < m; j++) {
result[i] += arrays[j][i]
}
}
return result
}
// Returns a new array with arrays avgs.
// Example: computeArraysAvg([[1, 2], [3, 4], [5, 0]) = [4.5, 2]
const _computeArraysAvg = arrays => {
const sums = _computeArraysSum(arrays)
if (!arrays[0]) {
return []
}
const n = arrays && arrays[0].length
const m = arrays.length
for (let i = 0; i < n; i++) {
sums[i] /= m
}
return sums
}
// Arrays can be null.
// See: https://github.com/vatesfr/xo-web/issues/969
//
// It's a fix to avoid error like `Uncaught TypeError: Cannot read property 'length' of null`.
// FIXME: Repare this bug in xo-server. (Warning: Can break the stats of xo-web v4.)
const removeUndefinedArrays = arrays => mapPlus(arrays, (array, push) => {
if (array != null) {
push(array)
}
})
export const computeArraysSum = arrays => _computeArraysSum(removeUndefinedArrays(arrays))
export const computeArraysAvg = arrays => _computeArraysAvg(removeUndefinedArrays(arrays))
// More complex than computeArraysAvg.
//
// Take in parameter one object like:
// { x: { a: [...], b: [...], c: [...] },
// y: { d: [...], e: [...], f: [...] } }
// and returns the avgs between a, b, c, d, e and f.
// Useful for vifs, pifs, xvds.
//
// Note: The parameter can be also an 3D array.
export const computeObjectsAvg = objects => {
return _computeArraysAvg(
map(objects, object =>
computeArraysAvg(values(object))
)
)
}

View File

@@ -1,19 +1,20 @@
import React from 'react'
import _ from 'intl'
import * as d3 from 'd3'
import forEach from 'lodash/forEach'
import map from 'lodash/map'
import { Toggle } from 'form'
import Component from '../base-component'
import _ from '../intl'
import propTypes from '../prop-types'
import { Toggle } from '../form'
import { setStyles } from '../d3-utils'
import {
createGetObject,
createSelector
} from '../selectors'
import {
connectStore,
propsEqual,
propTypes
propsEqual
} from '../utils'
import styles from './index.css'
@@ -63,16 +64,6 @@ const HORIZON_AREA_PATH_STYLE = {
// ===================================================================
function setStyles (style) {
forEach(style, (value, key) => {
this.style(key, value)
})
return this
}
// ===================================================================
@propTypes({
chartHeight: propTypes.number,
chartWidth: propTypes.number,

View File

@@ -1,5 +1,4 @@
import React from 'react'
import _ from 'intl'
import forEach from 'lodash/forEach'
import map from 'lodash/map'
import moment from 'moment'
@@ -12,9 +11,10 @@ import {
} from 'd3'
import { FormattedTime } from 'react-intl'
import _ from '../intl'
import Component from '../base-component'
import propTypes from '../prop-types'
import Tooltip from '../tooltip'
import { propTypes } from '../utils'
import styles from './index.css'
@@ -165,15 +165,17 @@ export default class XoWeekHeatmap extends Component {
<th><FormattedTime value={day.timestamp} {...DAY_TIME_FORMAT} /></th>
{map(day.hours, (hour, key) => (
<Tooltip
className={styles.cell}
key={key}
style={{ background: hour ? hour.color : '#ffffff' }}
tagName='td'
content={hour
? _('weekHeatmapData', { date: hour.date, value: this.props.cellRenderer(hour.value) })
: _('weekHeatmapNoData')
}
/>
key={key}
>
<td
className={styles.cell}
style={{ background: hour ? hour.color : '#ffffff' }}
/>
</Tooltip>
))}
</tr>
))}

View File

@@ -0,0 +1,61 @@
import keys from 'lodash/keys'
import React from 'react'
import * as FormGrid from '../../form-grid'
import _ from '../../intl'
import Combobox from '../../combobox'
import Component from '../../base-component'
import propTypes from '../../prop-types'
import { createSelector } from '../../selectors'
@propTypes({
type: propTypes.string.isRequired,
user: propTypes.object.isRequired,
value: propTypes.string.isRequired
})
export default class SaveNewUserFilterModalBody extends Component {
get value () {
return this.state.name || ''
}
_getFilterOptions = createSelector(
tmp => (
(tmp = this.props.user) &&
(tmp = tmp.preferences) &&
(tmp = tmp.filters) &&
tmp[this.props.type]
),
keys
)
render () {
const { value } = this.props
const options = this._getFilterOptions()
return (
<div>
<FormGrid.Row>
<FormGrid.LabelCol>{_('filterName')}</FormGrid.LabelCol>
<FormGrid.InputCol>
<Combobox
onChange={this.linkState('name')}
options={options}
value={this.state.name || ''}
/>
</FormGrid.InputCol>
</FormGrid.Row>
<FormGrid.Row>
<FormGrid.LabelCol>{_('filterValue')}</FormGrid.LabelCol>
<FormGrid.InputCol>
<input
className='form-control'
disabled
type='text'
value={value}
/>
</FormGrid.InputCol>
</FormGrid.Row>
</div>
)
}
}

View File

@@ -0,0 +1,103 @@
import _, { messages } from 'intl'
import map from 'lodash/map'
import React from 'react'
import { injectIntl } from 'react-intl'
import BaseComponent from 'base-component'
import SingleLineRow from 'single-line-row'
import Upgrade from 'xoa-upgrade'
import { Col } from 'grid'
import { createGetObjectsOfType } from 'selectors'
import { SelectSr } from 'select-objects'
import { Toggle } from 'form'
import {
buildTemplate,
connectStore
} from 'utils'
@connectStore(() => {
const getVms = createGetObjectsOfType('VM').pick(
(_, props) => props.vms
)
return {
vms: getVms
}
}, { withRef: true })
class CopyVmsModalBody extends BaseComponent {
get value () {
const { state } = this
if (!state || !state.sr) {
return {}
}
const { vms } = this.props
const { namePattern } = state
const names = namePattern
? map(vms, buildTemplate(namePattern, {
'{name}': vm => vm.name_label,
'{id}': vm => vm.id
}))
: map(vms, vm => vm.name_label)
return {
compress: state.compress,
names,
sr: state.sr.id
}
}
componentWillMount () {
this.setState({
compress: false,
namePattern: '{name}_COPY'
})
}
_onChangeSr = sr =>
this.setState({ sr })
_onChangeNamePattern = event =>
this.setState({ namePattern: event.target.value })
_onChangeCompress = compress =>
this.setState({ compress })
render () {
const { formatMessage } = this.props.intl
const { compress, namePattern, sr } = this.state
return process.env.XOA_PLAN > 2
? <div>
<SingleLineRow>
<Col size={6}>{_('copyVmSelectSr')}</Col>
<Col size={6}>
<SelectSr
onChange={this.linkState('sr')}
value={sr}
/>
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('copyVmName')}</Col>
<Col size={6}>
<input
className='form-control'
onChange={this.linkState('namePattern')}
placeholder={formatMessage(messages.copyVmNamePatternPlaceholder)}
type='text'
value={namePattern}
/>
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('copyVmCompress')}</Col>
<Col size={6}>
<Toggle
onChange={this.linkState('compress')}
value={compress}
/>
</Col>
</SingleLineRow>
</div>
: <div><Upgrade place='vmCopy' available={3} /></div>
}
}
export default injectIntl(CopyVmsModalBody, { withRef: true })

File diff suppressed because it is too large Load Diff

View File

@@ -7,11 +7,11 @@
margin-top: 2px;
}
.firstBlock {
.block {
padding-bottom: 1em;
}
.block {
.groupBlock {
padding-bottom: 1em;
padding-top: 1em;
border-top: 1px solid #e5e5e5;

View File

@@ -1,11 +1,15 @@
import BaseComponent from 'base-component'
import forEach from 'lodash/forEach'
import find from 'lodash/find'
import map from 'lodash/map'
import React, { Component } from 'react'
import mapValues from 'lodash/mapValues'
import React from 'react'
import _ from '../../intl'
import invoke from '../../invoke'
import SingleLineRow from '../../single-line-row'
import { Col } from '../../grid'
import { getDefaultNetworkForVif } from '../utils'
import {
SelectHost,
SelectNetwork,
@@ -21,6 +25,8 @@ import {
createSelector
} from '../../selectors'
import { isSrWritable } from '../'
import styles from './index.css'
@connectStore(() => {
@@ -47,19 +53,17 @@ import styles from './index.css'
const getPifs = createGetObjectsOfType('PIF')
const getNetworks = createGetObjectsOfType('network')
const getSrs = createGetObjectsOfType('SR')
const getPools = createGetObjectsOfType('pool')
return {
networks: getNetworks,
pifs: getPifs,
pools: getPools,
srs: getSrs,
vdis: getVdis,
vifs: getVifs
}
}, { withRef: true })
export default class MigrateVmModalBody extends Component {
export default class MigrateVmModalBody extends BaseComponent {
constructor (props) {
super(props)
@@ -76,8 +80,8 @@ export default class MigrateVmModalBody extends Component {
this._getSrPredicate = createSelector(
() => this.state.host,
host => (host
? sr => sr.content_type !== 'iso' && (sr.$container === host.id || sr.$container === host.$pool)
: () => false
? sr => isSrWritable(sr) && (sr.$container === host.id || sr.$container === host.$pool)
: false
)
)
@@ -88,7 +92,7 @@ export default class MigrateVmModalBody extends Component {
),
pifs => {
if (!pifs) {
return () => false
return false
}
const networks = {}
@@ -101,129 +105,145 @@ export default class MigrateVmModalBody extends Component {
)
}
componentDidMount () {
this._selectHost(this.props.host)
}
get value () {
return {
targetHost: this.state.host && this.state.host.id,
mapVdisSrs: this.state.mapVdisSrs,
mapVifsNetworks: this.state.mapVifsNetworks,
migrationNetwork: this.state.network && this.state.network.id
migrationNetwork: this.state.migrationNetworkId
}
}
_selectHost = host => {
if (!host) {
this.setState({ intraPool: undefined, targetHost: undefined })
this.setState({
host: undefined,
intraPool: undefined
})
return
}
const { networks, pools, pifs, srs, vdis, vifs } = this.props
const defaultMigrationNetwork = networks[find(pifs, pif => pif.$host === host.id && pif.management).$network]
const defaultSr = srs[pools[host.$pool].default_SR]
const defaultNetworks = {}
// Default network...
forEach(vifs, vif => {
// ...is the one which has the same name_label as the VIF's previous network (if it has an IP)...
const defaultPif = find(host.$PIFs, pifId => {
const pif = pifs[pifId]
return pif.ip && networks[vif.$network].name_label === networks[pif.$network].name_label
const intraPool = this.props.vm.$pool === host.$pool
if (intraPool) {
this.setState({
host,
intraPool,
mapVdisSrs: undefined,
mapVifsNetworks: undefined,
migrationNetwork: undefined
})
defaultNetworks[vif.id] = defaultPif && networks[defaultPif.$network]
// ...or the first network in the target host networks list that has an IP.
if (!defaultNetworks[vif.id]) {
defaultNetworks[vif.id] = networks[pifs[find(host.$PIFs, pif => pifs[pif].ip)].$network]
}
return
}
const { networks, pools, pifs, vdis, vifs } = this.props
const defaultMigrationNetworkId = find(pifs, pif => pif.$host === host.id && pif.management).$network
const defaultSr = pools[host.$pool].default_SR
const defaultNetwork = invoke(() => {
// First PIF with an IP.
const pifId = find(host.$PIFs, pif => pifs[pif].ip)
const pif = pifId && pifs[pifId]
return pif && pif.$network
})
const defaultNetworksForVif = {}
forEach(vifs, vif => {
defaultNetworksForVif[vif.id] = (
getDefaultNetworkForVif(vif, host, pifs, networks) ||
defaultNetwork
)
})
this.setState({
network: defaultMigrationNetwork,
defaultNetworks,
defaultSr,
host,
intraPool: this.props.vm.$pool === host.$pool
}, () => {
if (!this.state.intraPool) {
this.refs.network.value = defaultMigrationNetwork
forEach(vdis, vdi => {
this.refs['sr_' + vdi.id].value = defaultSr
})
forEach(vifs, vif => {
this.refs['network_' + vif.id].value = defaultNetworks[vif.id]
})
}
intraPool,
mapVdisSrs: mapValues(vdis, vdi => defaultSr),
mapVifsNetworks: defaultNetworksForVif,
migrationNetworkId: defaultMigrationNetworkId
})
}
_selectMigrationNetwork = network => this.setState({ network })
_selectMigrationNetwork = migrationNetwork => this.setState({ migrationNetworkId: migrationNetwork.id })
render () {
const { vdis, vifs, networks } = this.props
const {
host,
intraPool,
mapVdisSrs,
mapVifsNetworks,
migrationNetworkId
} = this.state
return <div>
<div className={styles.firstBlock}>
<div className={styles.block}>
<SingleLineRow>
<Col size={6}>{_('migrateVmAdvancedModalSelectHost')}</Col>
<Col size={6}>{_('migrateVmSelectHost')}</Col>
<Col size={6}>
<SelectHost
onChange={this._selectHost}
predicate={this._getHostPredicate()}
value={host}
/>
</Col>
</SingleLineRow>
</div>
{this.state.intraPool !== undefined &&
(!this.state.intraPool &&
{intraPool !== undefined &&
(!intraPool &&
<div>
<div className={styles.block}>
<div className={styles.groupBlock}>
<SingleLineRow>
<Col size={6}>{_('migrateVmAdvancedModalSelectNetwork')}</Col>
<Col size={6}>{_('migrateVmSelectMigrationNetwork')}</Col>
<Col size={6}>
<SelectNetwork
ref='network'
defaultValue={this.state.network}
onChange={this._selectMigrationNetwork}
predicate={this._getNetworkPredicate()}
value={migrationNetworkId}
/>
</Col>
</SingleLineRow>
</div>
<div className={styles.block}>
<div className={styles.groupBlock}>
<SingleLineRow>
<Col>{_('migrateVmAdvancedModalSelectSrs')}</Col>
<Col>{_('migrateVmSelectSrs')}</Col>
</SingleLineRow>
<br />
<SingleLineRow>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmAdvancedModalName')}</span></Col>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmAdvancedModalSr')}</span></Col>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmName')}</span></Col>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmSr')}</span></Col>
</SingleLineRow>
{map(vdis, vdi => <div className={styles.listItem} key={vdi.id}>
<SingleLineRow>
<Col size={6}>{vdi.name_label}</Col>
<Col size={6}>
<SelectSr
ref={'sr_' + vdi.id}
defaultValue={this.state.defaultSr}
onChange={sr => this.setState({ mapVdisSrs: { ...this.state.mapVdisSrs, [vdi.id]: sr.id } })}
onChange={sr => this.setState({ mapVdisSrs: { ...mapVdisSrs, [vdi.id]: sr.id } })}
predicate={this._getSrPredicate()}
value={mapVdisSrs[vdi.id]}
/>
</Col>
</SingleLineRow>
</div>)}
</div>
<div className={styles.block}>
<div className={styles.groupBlock}>
<SingleLineRow>
<Col>{_('migrateVmAdvancedModalSelectNetworks')}</Col>
<Col>{_('migrateVmSelectNetworks')}</Col>
</SingleLineRow>
<br />
<SingleLineRow>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmAdvancedModalVif')}</span></Col>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmAdvancedModalNetwork')}</span></Col>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmVif')}</span></Col>
<Col size={6}><span className={styles.listTitle}>{_('migrateVmNetwork')}</span></Col>
</SingleLineRow>
{map(vifs, vif => <div className={styles.listItem} key={vif.id}>
<SingleLineRow>
<Col size={6}>{vif.MAC} ({networks[vif.$network].name_label})</Col>
<Col size={6}>
<SelectNetwork
ref={'network_' + vif.id}
defaultValue={this.state.defaultNetworks[vif.id]}
onChange={network => this.setState({ mapVifsNetworks: { ...this.state.mapVifsNetworks, [vif.id]: network.id } })}
onChange={network => this.setState({ mapVifsNetworks: { ...mapVifsNetworks, [vif.id]: network.id } })}
predicate={this._getNetworkPredicate()}
value={mapVifsNetworks[vif.id]}
/>
</Col>
</SingleLineRow>

View File

@@ -0,0 +1,309 @@
import BaseComponent from 'base-component'
import every from 'lodash/every'
import flatten from 'lodash/flatten'
import forEach from 'lodash/forEach'
import filter from 'lodash/filter'
import find from 'lodash/find'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import mapValues from 'lodash/mapValues'
import React from 'react'
import some from 'lodash/some'
import store from 'store'
import _ from '../../intl'
import invoke from '../../invoke'
import SingleLineRow from '../../single-line-row'
import { Col } from '../../grid'
import { getDefaultNetworkForVif } from '../utils'
import {
SelectHost,
SelectNetwork,
SelectSr
} from '../../select-objects'
import {
connectStore
} from '../../utils'
import {
createGetObjectsOfType,
createPicker,
createSelector,
getObject
} from '../../selectors'
import {
isSrShared
} from 'xo'
import { isSrWritable } from '../'
const LINE_STYLE = { paddingBottom: '1em' }
@connectStore(() => {
const getNetworks = createGetObjectsOfType('network')
const getPifs = createGetObjectsOfType('PIF')
const getPools = createGetObjectsOfType('pool')
const getVms = createGetObjectsOfType('VM').pick(
(_, props) => props.vms
)
const getVbdsByVm = createGetObjectsOfType('VBD').pick(
createSelector(
getVms,
vms => flatten(map(vms, vm => vm.$VBDs))
)
).groupBy('VM')
const getVifsByVM = createGetObjectsOfType('VIF').pick(
createSelector(
getVms,
vms => flatten(map(vms, vm => vm.VIFs))
)
).groupBy('$VM')
return {
networks: getNetworks,
pifs: getPifs,
pools: getPools,
vbdsByVm: getVbdsByVm,
vifsByVm: getVifsByVM,
vms: getVms
}
}, { withRef: true })
export default class MigrateVmsModalBody extends BaseComponent {
constructor (props) {
super(props)
this._getHostPredicate = createSelector(
() => this.props.vms,
vms => host => some(vms, vm => host.id !== vm.$container)
)
this._getSrPredicate = createSelector(
() => this.state.host,
host => (host
? sr => isSrWritable(sr) && (sr.$container === host.id || sr.$container === host.$pool)
: false
)
)
this._getNetworkPredicate = createSelector(
createPicker(
() => this.props.pifs,
() => this.state.host.$PIFs
),
pifs => {
if (!pifs) {
return false
}
const networks = {}
forEach(pifs, pif => {
pif.ip && (networks[pif.$network] = true)
})
return network => networks[network.id]
}
)
}
componentDidMount () {
this._selectHost(this.props.host)
}
get value () {
const { host } = this.state
const vms = filter(this.props.vms, vm => vm.$container !== host.id)
if (!host || isEmpty(vms)) {
return { vms }
}
const {
networks,
pifs,
vbdsByVm,
vifsByVm
} = this.props
const {
intraPool,
doNotMigrateVdi,
doNotMigrateVmVdis,
migrationNetworkId,
networkId,
smartVifMapping,
srId
} = this.state
// Map VM --> ( Map VDI --> SR )
const mapVmsMapVdisSrs = {}
forEach(vbdsByVm, (vbds, vm) => {
if (doNotMigrateVmVdis[vm]) {
return
}
const mapVdisSrs = {}
forEach(vbds, vbd => {
const vdi = vbd.VDI
if (!vbd.is_cd_drive && vdi) {
mapVdisSrs[vdi] = intraPool && doNotMigrateVdi[vdi] ? this._getObject(vdi).SR : srId
}
})
mapVmsMapVdisSrs[vm] = mapVdisSrs
})
const defaultNetwork = smartVifMapping && invoke(() => {
// First PIF with an IP.
const pifId = find(host.$PIFs, pif => pifs[pif].ip)
const pif = pifId && pifs[pifId]
return pif && pif.$network
})
// Map VM --> ( Map VIF --> network )
const mapVmsMapVifsNetworks = {}
forEach(vms, vm => {
if (vm.$pool === host.$pool) {
return
}
const mapVifsNetworks = {}
forEach(vifsByVm[vm.id], vif => {
mapVifsNetworks[vif.id] = smartVifMapping
? getDefaultNetworkForVif(vif, host, pifs, networks) || defaultNetwork
: networkId
})
mapVmsMapVifsNetworks[vm.id] = mapVifsNetworks
})
// Map VM --> migration network
const mapVmsMigrationNetwork = mapValues(doNotMigrateVmVdis, doNotMigrateVdis =>
doNotMigrateVdis ? undefined : migrationNetworkId
)
return {
mapVmsMapVdisSrs,
mapVmsMapVifsNetworks,
mapVmsMigrationNetwork,
targetHost: host.id,
vms
}
}
_getObject (id) {
return getObject(store.getState(), id)
}
_selectHost = host => {
if (!host) {
this.setState({ targetHost: undefined })
return
}
const { pools, pifs } = this.props
const defaultMigrationNetworkId = find(pifs, pif => pif.$host === host.id && pif.management).$network
const defaultSrId = pools[host.$pool].default_SR
const doNotMigrateVmVdis = {}
const doNotMigrateVdi = {}
forEach(this.props.vbdsByVm, (vbds, vm) => {
if (this._getObject(vm).$container === host.id) {
doNotMigrateVmVdis[vm] = true
return
}
const _doNotMigrateVdi = {}
forEach(vbds, vbd => {
if (vbd.VDI != null) {
doNotMigrateVdi[vbd.VDI] = _doNotMigrateVdi[vbd.VDI] = isSrShared(this._getObject(this._getObject(vbd.VDI).$SR))
}
})
doNotMigrateVmVdis[vm] = every(_doNotMigrateVdi)
})
const noVdisMigration = every(doNotMigrateVmVdis)
this.setState({
host,
intraPool: every(this.props.vms, vm => vm.$pool === host.$pool),
doNotMigrateVdi,
doNotMigrateVmVdis,
migrationNetworkId: defaultMigrationNetworkId,
networkId: defaultMigrationNetworkId,
noVdisMigration,
smartVifMapping: true,
srId: defaultSrId
})
}
_selectMigrationNetwork = migrationNetwork => this.setState({ migrationNetworkId: migrationNetwork.id })
_selectNetwork = network => this.setState({ networkId: network.id })
_selectSr = sr => this.setState({ srId: sr.id })
_toggleSmartVifMapping = () => this.setState({ smartVifMapping: !this.state.smartVifMapping })
render () {
const {
host,
intraPool,
migrationNetworkId,
networkId,
noVdisMigration,
smartVifMapping,
srId
} = this.state
return <div>
<div style={LINE_STYLE}>
<SingleLineRow>
<Col size={6}>{_('migrateVmSelectHost')}</Col>
<Col size={6}>
<SelectHost
onChange={this._selectHost}
predicate={this._getHostPredicate()}
value={host}
/>
</Col>
</SingleLineRow>
</div>
{intraPool === false &&
<div style={LINE_STYLE}>
<SingleLineRow>
<Col size={6}>{_('migrateVmSelectMigrationNetwork')}</Col>
<Col size={6}>
<SelectNetwork
onChange={this._selectMigrationNetwork}
predicate={this._getNetworkPredicate()}
value={migrationNetworkId}
/>
</Col>
</SingleLineRow>
</div>
}
{host && (!intraPool || !noVdisMigration) &&
<div key='sr' style={LINE_STYLE}>
<SingleLineRow>
<Col size={6}>{!intraPool ? _('migrateVmsSelectSr') : _('migrateVmsSelectSrIntraPool')}</Col>
<Col size={6}>
<SelectSr
onChange={this._selectSr}
predicate={this._getSrPredicate()}
value={srId}
/>
</Col>
</SingleLineRow>
</div>
}
{host && !intraPool &&
<div key='network' style={LINE_STYLE}>
<SingleLineRow>
<Col size={6}>{_('migrateVmsSelectNetwork')}</Col>
<Col size={6}>
<SelectNetwork
disabled={smartVifMapping}
onChange={this._selectNetwork}
predicate={this._getNetworkPredicate()}
value={networkId}
/>
</Col>
</SingleLineRow>
<SingleLineRow>
<Col size={6} offset={6}>
<input type='checkbox' onChange={this._toggleSmartVifMapping} checked={smartVifMapping} />
{' '}
{_('migrateVmsSmartMapping')}
</Col>
</SingleLineRow>
</div>
}
</div>
}
}

View File

@@ -0,0 +1,58 @@
import BaseComponent from 'base-component'
import React from 'react'
import _ from '../../intl'
import SingleLineRow from '../../single-line-row'
import { Col } from '../../grid'
import getEventValue from '../../get-event-value'
export default class NewSshKeyModalBody extends BaseComponent {
get value () {
return this.state
}
_onKeyChange = event => {
const key = getEventValue(event)
const splitKey = key.split(' ')
if (!this.state.title && splitKey.length === 3) {
this.setState({ title: splitKey[2].split('\n')[0] })
}
this.setState({ key })
}
render () {
const {
key,
title
} = this.state
return <div>
<div className='p-b-1'>
<SingleLineRow>
<Col size={4}>{_('title')}</Col>
<Col size={8}>
<input
className='form-control'
onChange={this.linkState('title')}
type='text'
value={title || ''}
/>
</Col>
</SingleLineRow>
</div>
<div className='p-b-1'>
<SingleLineRow>
<Col size={4}>{_('key')}</Col>
<Col size={8}>
<textarea
className='form-control'
onChange={this._onKeyChange}
rows={10}
value={key || ''}
/>
</Col>
</SingleLineRow>
</div>
</div>
}
}

14
src/common/xo/utils.js Normal file
View File

@@ -0,0 +1,14 @@
import forEach from 'lodash/forEach'
export const getDefaultNetworkForVif = (vif, host, pifs, networks) => {
const nameLabel = networks[vif.$network].name_label
let defaultNetwork
forEach(host.$PIFs, pifId => {
const pif = pifs[pifId]
if (pif.ip && networks[pif.$network].name_label === nameLabel) {
defaultNetwork = pif.$network
return false
}
})
return defaultNetwork
}

View File

@@ -98,8 +98,8 @@ class XoaUpdater extends EventEmitter {
await this._update(true)
}
_promptForReload () {
this.emit('promptForReload')
_upgradeSuccessful () {
this.emit('upgradeSuccessful')
}
async _open () {
@@ -158,7 +158,7 @@ class XoaUpdater extends EventEmitter {
if (this._lowState.state === 'updater-upgraded' || this._lowState.state === 'installer-upgraded') {
this.update()
} else if (this._lowState.state === 'xoa-upgraded') {
this._promptForReload()
this._upgradeSuccessful()
}
this.xoaState()
})

View File

@@ -1,9 +1,11 @@
import _ from 'intl'
import Icon from 'icon'
import Link from 'react-router/lib/Link'
import React from 'react'
import { Card, CardHeader, CardBlock } from 'card'
import { getXoaPlan, propTypes } from 'utils'
import _ from './intl'
import Icon from './icon'
import Link from './link'
import propTypes from './prop-types'
import { Card, CardHeader, CardBlock } from './card'
import { getXoaPlan } from './utils'
const Upgrade = propTypes({
available: propTypes.number.isRequired,

View File

@@ -58,6 +58,10 @@
@extend .fa;
@extend .fa-wrench;
}
&-diagnosis {
@extend .fa;
@extend .fa-medkit;
}
&-chevron-up {
@extend .fa;
@extend .fa-chevron-up;
@@ -108,6 +112,10 @@
@extend .fa;
@extend .fa-play;
}
&-ssh-key {
@extend .fa;
@extend .fa-key;
}
&-shown {
@extend .fa;
@@ -140,11 +148,11 @@
&-asc {
@extend .fa;
@extend .fa-arrow-up;
@extend .fa-arrow-down;
}
&-desc {
@extend .fa;
@extend .fa-arrow-down;
@extend .fa-arrow-up;
}
&-sort {
@extend .fa;
@@ -343,6 +351,18 @@
@extend .xo-status-busy;
}
// Task
&-task {
&-cancel {
@extend .fa;
@extend .fa-ban;
}
&-destroy {
@extend .fa;
@extend .fa-trash;
}
}
// SR
&-sr, &-vdi {
&-reconnect-all {
@@ -521,6 +541,10 @@
@extend .fa;
@extend .fa-clock-o;
}
&-time {
@extend .fa;
@extend .fa-clock-o;
}
&-database {
@extend .fa;
@extend .fa-database;
@@ -827,6 +851,10 @@
@extend .fa;
@extend .fa-server;
}
&-connect {
@extend .fa;
@extend .fa-link;
}
&-disconnect {
@extend .fa;
@extend .fa-unlink;

View File

@@ -21,6 +21,8 @@ html.no-js(
//- .visible-js to display content only when JavaScript is ENABLED.
//- .hidden-js to display content only when JavaScript is DISABLED.
script !function(d){d.className=d.className.replace(/\bno-js\b/,'js')}(document.documentElement)
script(src = 'https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en')
style .no-js .visible-js,.js .hidden-js{display:none}
//- (TODO: confirm) For smartphones and tablets: sets the page

View File

@@ -202,27 +202,9 @@ $select-input-height: 40px; // Bootstrap input height
background: $gray-lighter;
}
// DASHBOARD STYLE =============================================================
.card-dashboard {
@extend .card;
}
.card-header-dashboard {
@extend .card-header;
font-size: 1.2em;
text-align: center;
}
.card-block-dashboard {
@extend .card-block;
font-size: 4em;
text-align: center;
}
// MEMORY/DISK BAR STYLE =======================================================
.progress-usage {
.usage {
@extend .progress;
background-color: #eee;
height: 2em;
@@ -231,22 +213,24 @@ $select-input-height: 40px; // Bootstrap input height
margin-bottom: 2em;
}
.progress-dom0 {
display: inline-block;
background-color: #337ab7;
height: 2em;
}
.progress-object {
.usage-element {
background-color: #5cb85c;
box-shadow: -1px 0 0 0 white;
height: 2em;
margin-right: 0px;
display: inline-block;
transition: all 0.3s ease 0s;
}
.progress-object:hover {
opacity: 0.5;
.usage-element-highlight {
background-color: $brand-primary;
}
.usage-element-others {
background-color: $brand-info;
}
.usage-element:hover {
opacity: 0.6;
}
// NOTIFICATIONS STYLE =========================================================

View File

@@ -2,7 +2,7 @@ import _ from 'intl'
import Component from 'base-component'
import Copiable from 'copiable'
import Icon from 'icon'
import Link from 'react-router/lib/Link'
import Link from 'link'
import Page from '../page'
import React from 'react'
import { getUser } from 'selectors'
@@ -33,7 +33,7 @@ export default class About extends Component {
const { user } = this.props
const isAdmin = user && user.permission === 'admin'
return <Page header={HEADER}>
return <Page header={HEADER} title='aboutPage' formatTitle>
<Container className='text-xs-center'>
{isAdmin && <Row>
<Col mediumSize={6}>

View File

@@ -32,7 +32,7 @@ const Backup = routes('overview', {
overview: Overview,
restore: Restore
})(
({ children }) => <Page header={HEADER}>{children}</Page>
({ children }) => <Page header={HEADER} title='backupPage' formatTitle>{children}</Page>
)
export default Backup

View File

@@ -11,13 +11,14 @@ import Upgrade from 'xoa-upgrade'
import Wizard, { Section } from 'wizard'
import { Container } from 'grid'
import { error } from 'notification'
import { generateUiSchema } from 'xo-json-schema-input'
import { injectIntl } from 'react-intl'
import {
createJob,
createSchedule,
setJob,
setSchedule
updateSchedule
} from 'xo'
import { getJobValues } from '../helpers'
@@ -34,7 +35,10 @@ const COMMON_SCHEMA = {
},
vms: {
type: 'array',
'xo:type': 'vm',
items: {
type: 'string',
'xo:type': 'vm'
},
title: 'VMs',
description: 'Choose VMs to backup.'
},
@@ -83,7 +87,7 @@ const BACKUP_SCHEMA = {
required: COMMON_SCHEMA.required.concat([ 'depth', 'remoteId' ])
}
const ROLLING_SNAPHOT_SCHEMA = {
const ROLLING_SNAPSHOT_SCHEMA = {
type: 'object',
properties: {
...COMMON_SCHEMA.properties,
@@ -143,13 +147,15 @@ if (process.env.XOA_PLAN < 4) {
const BACKUP_METHOD_TO_INFO = {
'vm.rollingBackup': {
schema: BACKUP_SCHEMA,
uiSchema: generateUiSchema(BACKUP_SCHEMA),
label: 'backup',
icon: 'backup',
jobKey: 'rollingBackup',
method: 'vm.rollingBackup'
},
'vm.rollingSnapshot': {
schema: ROLLING_SNAPHOT_SCHEMA,
schema: ROLLING_SNAPSHOT_SCHEMA,
uiSchema: generateUiSchema(ROLLING_SNAPSHOT_SCHEMA),
label: 'rollingSnapshot',
icon: 'rolling-snapshot',
jobKey: 'rollingSnapshot',
@@ -157,6 +163,7 @@ const BACKUP_METHOD_TO_INFO = {
},
'vm.rollingDeltaBackup': {
schema: DELTA_BACKUP_SCHEMA,
uiSchema: generateUiSchema(DELTA_BACKUP_SCHEMA),
label: 'deltaBackup',
icon: 'delta-backup',
jobKey: 'deltaBackup',
@@ -164,6 +171,7 @@ const BACKUP_METHOD_TO_INFO = {
},
'vm.rollingDrCopy': {
schema: DISASTER_RECOVERY_SCHEMA,
uiSchema: generateUiSchema(DISASTER_RECOVERY_SCHEMA),
label: 'disasterRecovery',
icon: 'disaster-recovery',
jobKey: 'disasterRecovery',
@@ -171,6 +179,7 @@ const BACKUP_METHOD_TO_INFO = {
},
'vm.deltaCopy': {
schema: CONTINUOUS_REPLICATION_SCHEMA,
uiSchema: generateUiSchema(CONTINUOUS_REPLICATION_SCHEMA),
label: 'continuousReplication',
icon: 'continuous-replication',
jobKey: 'continuousReplication',
@@ -180,13 +189,13 @@ const BACKUP_METHOD_TO_INFO = {
// ===================================================================
const DEFAULT_CRON_PATTERN = '0 0 * * *'
@injectIntl
export default class New extends Component {
constructor (props) {
super(props)
const { state } = this
state.cronPattern = '* * * * *'
this.state.cronPattern = DEFAULT_CRON_PATTERN
}
componentWillMount () {
@@ -199,7 +208,8 @@ export default class New extends Component {
}
this.setState({
backupInfo: BACKUP_METHOD_TO_INFO[job.method],
cronPattern: schedule.cron
cronPattern: schedule.cron,
timezone: schedule.timezone || null
}, () => delay(this._populateForm, 250, job)) // Work around.
// Without the delay, some selects are not always ready to load a value
// Values are displayed, but html5 compliant browsers say the value is required and empty on submit
@@ -231,7 +241,8 @@ export default class New extends Component {
...callArgs
} = backup
const { backupInfo } = this.state
const { backupInfo, timezone } = this.state
const job = {
type: 'call',
key: backupInfo.jobKey,
@@ -253,33 +264,33 @@ export default class New extends Component {
if (oldJob && oldSchedule) {
job.id = oldJob.id
oldSchedule.cron = this.state.cronPattern
return setJob(job).then(() => setSchedule(oldSchedule))
return setJob(job).then(() => updateSchedule({
...oldSchedule,
cron: this.state.cronPattern,
timezone
}))
}
// Create backup schedule.
return createJob(job).then(jobId => {
createSchedule(jobId, this.state.cronPattern, enabled)
createSchedule(jobId, { cron: this.state.cronPattern, enabled, timezone })
})
}
_handleReset = () => {
const {
backupInput,
scheduler
} = this.refs
const { backupInput } = this.refs
if (backupInput) {
backupInput.value = undefined
}
scheduler.value = '* * * * *'
this.setState({
cronPattern: DEFAULT_CRON_PATTERN
})
}
_updateCronPattern = value => {
this.setState({
cronPattern: value
})
this.setState(value)
}
_handleBackupSelection = event => {
@@ -289,7 +300,12 @@ export default class New extends Component {
}
render () {
const { backupInfo, defaultValue } = this.state
const {
backupInfo,
cronPattern,
defaultValue,
timezone
} = this.state
const { formatMessage } = this.props.intl
return process.env.XOA_PLAN > 1
@@ -318,17 +334,22 @@ export default class New extends Component {
label={<span><Icon icon={backupInfo.icon} /> {formatMessage(messages[backupInfo.label])}</span>}
required
schema={backupInfo.schema}
uiSchema={backupInfo.uiSchema}
ref='backupInput'
/>
}
</form>
</Section>
<Section icon='schedule' title='schedule'>
<Scheduler ref='scheduler' onChange={this._updateCronPattern} />
<Scheduler
cronPattern={cronPattern}
timezone={timezone}
onChange={this._updateCronPattern}
/>
</Section>
<Section icon='preview' title='preview' summary>
<div className='card-block'>
<SchedulePreview cron={this.state.cronPattern} />
<SchedulePreview cronPattern={cronPattern} />
{process.env.XOA_PLAN < 4 && backupInfo && process.env.XOA_PLAN < REQUIRED_XOA_PLAN[backupInfo.jobKey]
? <Upgrade place='newBackup' available={REQUIRED_XOA_PLAN[backupInfo.jobKey]} />
: <fieldset className='pull-xs-right p-t-1'>

View File

@@ -4,19 +4,17 @@ import ActionToggle from 'action-toggle'
import filter from 'lodash/filter'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import Link from 'link'
import LogList from '../../logs'
import map from 'lodash/map'
import orderBy from 'lodash/orderBy'
import React, { Component } from 'react'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { Link } from 'react-router'
import {
Card,
CardHeader,
CardBlock
} from 'card'
import {
deleteBackupSchedule,
disableSchedule,
@@ -148,6 +146,7 @@ export default class Overview extends Component {
<th>{_('job')}</th>
<th>{_('jobTag')}</th>
<th className='hidden-xs-down'>{_('jobScheduling')}</th>
<th className='hidden-xs-down'>{_('jobTimezone')}</th>
<th>{_('jobState')}</th>
</tr>
</thead>
@@ -160,6 +159,7 @@ export default class Overview extends Component {
<td>{this._getJobLabel(job)}</td>
<td>{this._getScheduleTag(schedule, job)}</td>
<td className='hidden-xs-down'>{schedule.cron}</td>
<td className='hidden-xs-down'>{schedule.timezone || _('jobServerTimezone')}</td>
<td>
{this._getScheduleToggle(schedule)}
<fieldset className='pull-xs-right'>

View File

@@ -3,7 +3,7 @@ import ActionButton from 'action-button'
import ActionRowButton from 'action-row-button'
import find from 'lodash/find'
import forEach from 'lodash/forEach'
import Link from 'react-router/lib/Link'
import Link from 'link'
import map from 'lodash/map'
import moment from 'moment'
import orderBy from 'lodash/orderBy'
@@ -25,6 +25,7 @@ import { SelectSr } from 'select-objects'
import {
importBackup,
importDeltaBackup,
isSrWritable,
listRemote,
startVm,
subscribeRemotes
@@ -43,7 +44,7 @@ const backupOptionRenderer = backup => <span>
@connectStore(() => ({
writableSrs: createGetObjectsOfType('SR').filter(
[ sr => sr.content_type !== 'iso' ]
[ isSrWritable ]
).sort()
}))
export default class Restore extends Component {
@@ -102,8 +103,10 @@ export default class Restore extends Component {
}
}
}
backupInfoByVm[backup.name] || (backupInfoByVm[backup.name] = [])
backupInfoByVm[backup.name].push(backup)
if (backup) {
backupInfoByVm[backup.name] || (backupInfoByVm[backup.name] = [])
backupInfoByVm[backup.name].push(backup)
}
})
for (let vm in backupInfoByVm) {
const bks = backupInfoByVm[vm]
@@ -192,7 +195,8 @@ const BK_COLUMNS = [
{
name: _('lastBackupColumn'),
itemRenderer: info => <span><FormattedDate value={info.last.date} month='long' day='numeric' year='numeric' hour='2-digit' minute='2-digit' second='2-digit' /> ({info.last.type})</span>,
sortCriteria: info => info.last.date
sortCriteria: info => info.last.date,
sortOrder: 'desc'
},
{
name: _('availableBackupsColumn'),
@@ -208,12 +212,11 @@ const BK_COLUMNS = [
}
]
const srWritablePredicate = sr => sr.content_type !== 'iso'
const notifyImportStart = () => info(_('importBackupTitle'), _('importBackupMessage'))
@connectStore(() => ({
writableSrs: createGetObjectsOfType('SR').filter(
[ sr => sr.content_type !== 'iso' ]
[ isSrWritable ]
).sort()
}), { withRef: true })
class _ModalBody extends Component {
@@ -274,7 +277,7 @@ class _ModalBody extends Component {
render () {
return <div>
<SelectSr ref='sr' predicate={srWritablePredicate} />
<SelectSr ref='sr' predicate={isSrWritable} />
<br />
<SelectPlainObject ref='backup' options={this.state.options} optionKey='path' optionRenderer={backupOptionRenderer} placeholder={this.props.intl.formatMessage(messages.importBackupModalSelectBackup)} />
<br />

View File

@@ -7,8 +7,9 @@ import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import Upgrade from 'xoa-upgrade'
import React, { Component } from 'react'
import { Card, CardHeader, CardBlock } from 'card'
import { confirm } from 'modal'
import { deleteMessage, deleteVdi, deleteVm } from 'xo'
import { deleteMessage, deleteVdi, deleteVm, isSrWritable } from 'xo'
import { FormattedRelative, FormattedTime } from 'react-intl'
import { Container, Row, Col } from 'grid'
import {
@@ -65,7 +66,8 @@ const SR_COLUMNS = [
{
name: _('srUsage'),
itemRenderer: sr => sr.size > 1 && <meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90'></meter>,
sortCriteria: sr => sr.physical_usage / sr.size
sortCriteria: sr => sr.physical_usage / sr.size,
sortOrder: 'desc'
}
]
@@ -73,7 +75,8 @@ const VDI_COLUMNS = [
{
name: _('snapshotDate'),
itemRenderer: vdi => <span><FormattedTime value={vdi.snapshot_time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={vdi.snapshot_time * 1000} />)</span>,
sortCriteria: vdi => vdi.snapshot_time
sortCriteria: vdi => vdi.snapshot_time,
sortOrder: 'desc'
},
{
name: _('vdiNameLabel'),
@@ -111,7 +114,8 @@ const VM_COLUMNS = [
{
name: _('snapshotDate'),
itemRenderer: vm => <span><FormattedTime value={vm.snapshot_time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={vm.snapshot_time * 1000} />)</span>,
sortCriteria: vm => vm.snapshot_time
sortCriteria: vm => vm.snapshot_time,
sortOrder: 'desc'
},
{
name: _('vmNameLabel'),
@@ -146,7 +150,8 @@ const ALARM_COLUMNS = [
itemRenderer: message => (
<span><FormattedTime value={message.time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={message.time * 1000} />)</span>
),
sortCriteria: message => message.time
sortCriteria: message => message.time,
sortOrder: 'desc'
},
{
name: _('alarmContent'),
@@ -182,7 +187,7 @@ const ALARM_COLUMNS = [
.filter([ snapshot => !snapshot.$snapshot_of ])
.sort()
const getUserSrs = createGetObjectsOfType('SR')
.filter([ sr => sr.content_type === 'user' ])
.filter([ isSrWritable ])
const getVdiSrs = createGetObjectsOfType('SR')
.pick(createSelector(
getOrphanVdiSnapshots,
@@ -230,11 +235,11 @@ export default class Health extends Component {
? <Container>
<Row>
<Col>
<div className='card-dashboard'>
<div className='card-header-dashboard'>
<Card>
<CardHeader>
<Icon icon='disk' /> {_('srStatePanel')}
</div>
<div className='card-block'>
</CardHeader>
<CardBlock>
{isEmpty(this.props.userSrs)
? <p className='text-xs-center'>{_('noSrs')}</p>
: <Row>
@@ -243,17 +248,17 @@ export default class Health extends Component {
</Col>
</Row>
}
</div>
</div>
</CardBlock>
</Card>
</Col>
</Row>
<Row>
<Col>
<div className='card-dashboard'>
<div className='card-header-dashboard'>
<Card>
<CardHeader>
<Icon icon='disk' /> {_('orphanedVdis')}
</div>
<div className='card-block'>
</CardHeader>
<CardBlock>
{isEmpty(this.props.vdiOrphaned)
? <p className='text-xs-center'>{_('noOrphanedObject')}</p>
: <div>
@@ -274,32 +279,32 @@ export default class Health extends Component {
</Row>
</div>
}
</div>
</div>
</CardBlock>
</Card>
</Col>
</Row>
<Row>
<Col>
<div className='card-dashboard'>
<div className='card-header-dashboard'>
<Card>
<CardHeader>
<Icon icon='vm' /> {_('orphanedVms')}
</div>
<div className='card-block'>
</CardHeader>
<CardBlock>
{isEmpty(this.props.vmOrphaned)
? <p className='text-xs-center'>{_('noOrphanedObject')}</p>
: <SortedTable collection={this.props.vmOrphaned} columns={VM_COLUMNS} />
}
</div>
</div>
</CardBlock>
</Card>
</Col>
</Row>
<Row>
<Col>
<div className='card-dashboard'>
<div className='card-header-dashboard'>
<Card>
<CardHeader>
<Icon icon='alarm' /> {_('alarmMessage')}
</div>
<div className='card-block'>
</CardHeader>
<CardBlock>
{isEmpty(this.props.alertMessages)
? <p className='text-xs-center'>{_('noAlarms')}</p>
: <div>
@@ -320,8 +325,8 @@ export default class Health extends Component {
</Row>
</div>
}
</div>
</div>
</CardBlock>
</Card>
</Col>
</Row>
</Container>

View File

@@ -33,7 +33,7 @@ const Dashboard = routes('overview', {
stats: Stats,
visualizations: Visualizations
})(
({ children }) => <Page header={HEADER}>{children}</Page>
({ children }) => <Page header={HEADER} title='dashboardPage' formatTitle>{children}</Page>
)
export default Dashboard

View File

@@ -0,0 +1,4 @@
.bigCardContent {
font-size: 4em;
text-align: center;
}

View File

@@ -1,15 +1,23 @@
import _ from 'intl'
import ChartistGraph from 'react-chartist'
import Component from 'base-component'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import propTypes from 'prop-types'
import Link, { BlockLink } from 'link'
import map from 'lodash/map'
import HostsPatchesTable from 'hosts-patches-table'
import React from 'react'
import size from 'lodash/size'
import Upgrade from 'xoa-upgrade'
import React, { Component } from 'react'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { Card, CardBlock, CardHeader } from 'card'
import { Container, Row, Col } from 'grid'
import {
createCollectionWrapper,
createCounter,
createGetObjectsOfType,
createGetHostMetrics,
createSelector,
createTop
} from 'selectors'
@@ -18,35 +26,51 @@ import {
formatSize
} from 'utils'
import {
isSrWritable,
subscribeUsers
} from 'xo'
import styles from './index.css'
// ===================================================================
@propTypes({
hosts: propTypes.object.isRequired
})
class PatchesCard extends Component {
_getContainer = () => this.refs.container
render () {
return (
<Card>
<CardHeader>
<Icon icon='host-patch-update' /> {_('update')}
<div ref='container' className='pull-right' />
</CardHeader>
<CardBlock>
<HostsPatchesTable
buttonsGroupContainer={this._getContainer}
container={ButtonGroup}
displayPools
hosts={this.props.hosts}
/>
</CardBlock>
</Card>
)
}
}
// ===================================================================
@connectStore(() => {
const getHosts = createGetObjectsOfType('host')
const getVms = createGetObjectsOfType('VM')
const getHostMetrics = createCollectionWrapper(
createSelector(
getHosts,
hosts => {
const metrics = {
cpus: 0,
memoryTotal: 0,
memoryUsage: 0
}
forEach(hosts, host => {
metrics.cpus += host.cpus.cores
metrics.memoryTotal += host.memory.size
metrics.memoryUsage += host.memory.usage
})
return metrics
}
)
)
const getHostMetrics = createGetHostMetrics(getHosts)
const userSrs = createTop(
createGetObjectsOfType('SR').filter(
[ sr => sr.content_type === 'user' ]
[ isSrWritable ]
),
[ sr => sr.physical_usage / sr.size ],
5
@@ -111,6 +135,7 @@ import {
return {
hostMetrics: getHostMetrics,
hosts: getHosts,
nAlarmMessages: getNumberOfAlarmMessages,
nHosts: getNumberOfHosts,
nPools: getNumberOfPools,
@@ -128,167 +153,212 @@ export default class Overview extends Component {
})
}
render () {
const { state } = this
const { props, state } = this
const users = state && state.users
const nUsers = users && Object.keys(users).length
const nUsers = size(users)
return process.env.XOA_PLAN > 2
? <Container>
<Row>
<Col mediumSize={4}>
<div className='card-dashboard'>
<div className='card-header-dashboard'>
<Icon icon='pool' /> {_('poolPanel', { pools: this.props.nPools })}
</div>
<div className='card-block-dashboard'>
<p>{this.props.nPools}</p>
</div>
</div>
<Card>
<CardHeader>
<Icon icon='pool' /> {_('poolPanel', { pools: props.nPools })}
</CardHeader>
<CardBlock>
<p className={styles.bigCardContent}>
<Link to='/home?t=pool'>{props.nPools}</Link>
</p>
</CardBlock>
</Card>
</Col>
<Col mediumSize={4}>
<div className='card-dashboard'>
<div className='card-header-dashboard'>
<Icon icon='host' /> {_('hostPanel', { hosts: this.props.nHosts })}
</div>
<div className='card-block-dashboard'>
<p>{this.props.nHosts}</p>
</div>
</div>
<Card>
<CardHeader>
<Icon icon='host' /> {_('hostPanel', { hosts: props.nHosts })}
</CardHeader>
<CardBlock>
<p className={styles.bigCardContent}>
<Link to='/home?t=host'>{props.nHosts}</Link>
</p>
</CardBlock>
</Card>
</Col>
<Col mediumSize={4}>
<div className='card-dashboard'>
<div className='card-header-dashboard'>
<Icon icon='vm' /> {_('vmPanel', { vms: this.props.nVms })}
</div>
<div className='card-block-dashboard'>
<p>{this.props.nVms}</p>
</div>
</div>
<Card>
<CardHeader>
<Icon icon='vm' /> {_('vmPanel', { vms: props.nVms })}
</CardHeader>
<CardBlock>
<p className={styles.bigCardContent}>
<Link to='/home?s=&t=VM'>{props.nVms}</Link>
</p>
</CardBlock>
</Card>
</Col>
</Row>
<Row>
<Col mediumSize={4}>
<div className='card-dashboard'>
<div className='card-header-dashboard'>
<Card>
<CardHeader>
<Icon icon='memory' /> {_('memoryStatePanel')}
</div>
<div className='card-block'>
</CardHeader>
<CardBlock>
<ChartistGraph
data={{
labels: ['Used Memory', 'Total Memory'],
series: [this.props.hostMetrics.memoryUsage, this.props.hostMetrics.memoryTotal - this.props.hostMetrics.memoryUsage]
series: [props.hostMetrics.memoryUsage, props.hostMetrics.memoryTotal - props.hostMetrics.memoryUsage]
}}
options={{ donut: true, donutWidth: 40, showLabel: false }}
type='Pie' />
<p className='text-xs-center'>{formatSize(this.props.hostMetrics.memoryUsage)} ({_('ofUsage')} {formatSize(this.props.hostMetrics.memoryTotal)})</p>
</div>
</div>
type='Pie'
/>
<p className='text-xs-center'>
{_('ofUsage', {
total: formatSize(props.hostMetrics.memoryTotal),
usage: formatSize(props.hostMetrics.memoryUsage)
})}
</p>
</CardBlock>
</Card>
</Col>
<Col mediumSize={4}>
<div className='card-dashboard'>
<div className='card-header-dashboard'>
<Card>
<CardHeader>
<Icon icon='cpu' /> {_('cpuStatePanel')}
</div>
<div className='card-block'>
</CardHeader>
<CardBlock>
<div className='ct-chart'>
<ChartistGraph
data={{
labels: ['vCPUs', 'CPUs'],
series: [this.props.vmMetrics.vcpus, this.props.hostMetrics.cpus]
series: [props.vmMetrics.vcpus, props.hostMetrics.cpus]
}}
options={{ showLabel: false, showGrid: false, distributeSeries: true }}
type='Bar' />
<p className='text-xs-center'>{this.props.vmMetrics.vcpus} vCPUS ({_('ofUsage')} {this.props.hostMetrics.cpus} CPUs)</p>
type='Bar'
/>
<p className='text-xs-center'>
{_('ofUsage', {
total: `${props.vmMetrics.vcpus} vCPUS`,
usage: `${props.hostMetrics.cpus} CPUs`
})}
</p>
</div>
</div>
</div>
</CardBlock>
</Card>
</Col>
<Col mediumSize={4}>
<div className='card-dashboard'>
<div className='card-header-dashboard'>
<Card>
<CardHeader>
<Icon icon='disk' /> {_('srUsageStatePanel')}
</div>
<div className='card-block'>
</CardHeader>
<CardBlock>
<div className='ct-chart'>
<BlockLink to='/dashboard/health'>
<ChartistGraph
data={{
labels: ['Used Space', 'Total Space'],
series: [props.srMetrics.srUsage, props.srMetrics.srTotal - props.srMetrics.srUsage]
}}
options={{ donut: true, donutWidth: 40, showLabel: false }}
type='Pie'
/>
<p className='text-xs-center'>
{_('ofUsage', {
total: formatSize(props.srMetrics.srUsage),
usage: formatSize(props.srMetrics.srTotal)
})}
</p>
</BlockLink>
</div>
</CardBlock>
</Card>
</Col>
</Row>
<Row>
<Col mediumSize={4}>
<Card>
<CardHeader>
<Icon icon='alarm' /> {_('alarmMessage')}
</CardHeader>
<CardBlock>
<p className={styles.bigCardContent}>
<Link to='/dashboard/health' className={props.nAlarmMessages > 0 ? 'text-warning' : ''}>{props.nAlarmMessages}</Link>
</p>
</CardBlock>
</Card>
</Col>
<Col mediumSize={4}>
<Card>
<CardHeader>
<Icon icon='task' /> {_('taskStatePanel')}
</CardHeader>
<CardBlock>
<p className={styles.bigCardContent}>
<Link to='/tasks'>{props.nTasks}</Link>
</p>
</CardBlock>
</Card>
</Col>
<Col mediumSize={4}>
<Card>
<CardHeader>
<Icon icon='user' /> {_('usersStatePanel')}
</CardHeader>
<CardBlock>
<p className={styles.bigCardContent}>
<Link to='/settings/users'>{nUsers}</Link>
</p>
</CardBlock>
</Card>
</Col>
</Row>
<Row>
<Col mediumSize={4}>
<Card>
<CardHeader>
<Icon icon='vm-force-shutdown' /> {_('vmStatePanel')}
</CardHeader>
<CardBlock>
<BlockLink to='/home?t=VM'>
<ChartistGraph
data={{
labels: ['Used Space', 'Total Space'],
series: [this.props.srMetrics.srUsage, this.props.srMetrics.srTotal - this.props.srMetrics.srUsage]
labels: ['Running', 'Halted', 'Other'],
series: [props.vmMetrics.running, props.vmMetrics.halted, props.vmMetrics.other]
}}
options={{ donut: true, donutWidth: 40, showLabel: false }}
type='Pie' />
<p className='text-xs-center'>{formatSize(this.props.srMetrics.srUsage)} ({_('ofUsage')} {formatSize(this.props.srMetrics.srTotal)})</p>
</div>
</div>
</div>
</Col>
</Row>
<Row>
<Col mediumSize={4}>
<div className='card-dashboard'>
<div className='card-header-dashboard'>
<Icon icon='alarm' /> {_('alarmMessage')}
</div>
<div className='card-block-dashboard'>
<p className={this.props.nAlarmMessages > 0 ? 'text-warning' : ''}>{this.props.nAlarmMessages}</p>
</div>
</div>
</Col>
<Col mediumSize={4}>
<div className='card-dashboard'>
<div className='card-header-dashboard'>
<Icon icon='task' /> {_('taskStatePanel')}
</div>
<div className='card-block-dashboard'>
<p>{this.props.nTasks}</p>
</div>
</div>
</Col>
<Col mediumSize={4}>
<div className='card-dashboard'>
<div className='card-header-dashboard'>
<Icon icon='user' /> {_('usersStatePanel')}
</div>
<div className='card-block-dashboard'>
<p>{nUsers}</p>
</div>
</div>
</Col>
</Row>
<Row>
<Col mediumSize={4}>
<div className='card-dashboard'>
<div className='card-header-dashboard'>
<Icon icon='vm-force-shutdown' /> {_('vmStatePanel')}
</div>
<div className='card-block'>
<ChartistGraph
data={{
labels: ['Running', 'Halted', 'Other'],
series: [this.props.vmMetrics.running, this.props.vmMetrics.halted, this.props.vmMetrics.other]
}}
options={{ showLabel: false }}
type='Pie' />
<p className='text-xs-center'>{this.props.vmMetrics.running} running ({this.props.vmMetrics.halted} halted)</p>
</div>
</div>
options={{ showLabel: false }}
type='Pie'
/>
<p className='text-xs-center'>
{_('vmsStates', { running: props.vmMetrics.running, halted: props.vmMetrics.halted })}
</p>
</BlockLink>
</CardBlock>
</Card>
</Col>
<Col mediumSize={8}>
<div className='card-dashboard'>
<div className='card-header-dashboard'>
<Card>
<CardHeader>
<Icon icon='disk' /> {_('srTopUsageStatePanel')}
</div>
<div className='card-block'>
<ChartistGraph
style={{strokeWidth: '30px'}}
data={{
labels: map(this.props.userSrs, 'name_label'),
series: map(this.props.userSrs, sr => (sr.physical_usage / sr.size) * 100)
}}
options={{ showLabel: false, showGrid: false, distributeSeries: true, high: 100 }}
type='Bar' />
</div>
</div>
</CardHeader>
<CardBlock>
<BlockLink to='/dashboard/health'>
<ChartistGraph
style={{strokeWidth: '30px'}}
data={{
labels: map(props.userSrs, 'name_label'),
series: map(props.userSrs, sr => (sr.physical_usage / sr.size) * 100)
}}
options={{ showLabel: false, showGrid: false, distributeSeries: true, high: 100 }}
type='Bar'
/>
</BlockLink>
</CardBlock>
</Card>
</Col>
</Row>
<Row>
<Col>
<PatchesCard hosts={props.hosts} />
</Col>
</Row>
</Container>

View File

@@ -1,15 +1,16 @@
import ActionButton from 'action-button'
import Component from 'base-component'
import Icon from 'icon'
import React from 'react'
import XoWeekCharts from 'xo-week-charts'
import XoWeekHeatmap from 'xo-week-heatmap'
import _ from 'intl'
import ActionButton from 'action-button'
import cloneDeep from 'lodash/cloneDeep'
import Component from 'base-component'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import map from 'lodash/map'
import propTypes from 'prop-types'
import React from 'react'
import renderXoItem from 'render-xo-item'
import sortBy from 'lodash/sortBy'
import XoWeekCharts from 'xo-week-charts'
import XoWeekHeatmap from 'xo-week-heatmap'
import { Container, Row, Col } from 'grid'
import { error } from 'notification'
import { SelectHostVm } from 'select-objects'
@@ -17,8 +18,7 @@ import { createGetObjectsOfType } from 'selectors'
import {
connectStore,
formatSize,
mapPlus,
propTypes
mapPlus
} from 'utils'
import {
fetchHostStats,

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