Compare commits

...

273 Commits

Author SHA1 Message Date
Julien Fontanet
563b4cb1ec 5.2.5 2016-10-07 15:45:28 +02:00
Olivier Lambert
45bad231cf feat(changelog): add 5.2.4 and 5.2.5 release 2016-10-07 15:44:47 +02:00
Pierre Donias
d76bd2484b fix(console): disable shortcuts when console is focused (#1637)
Fixes #1614
2016-10-07 15:26:20 +02:00
Pierre Donias
445b60bb63 fix(vm/console): initial scale value should be 1 (#1639) 2016-10-07 14:06:16 +02:00
Julien Fontanet
3214e0e41e fix: style & minor issues 2016-10-06 18:28:23 +02:00
Julien Fontanet
c61230e145 fix(intl/locales/fr): remove incorrect entry 2016-10-06 16:13:54 +02:00
fufroma
fac6a29226 feat(intl): new translatable messages (#1627) 2016-10-06 16:05:47 +02:00
Olivier Lambert
7a8f414748 feat(home/host): sparklines in expanded zone (#1619)
Fixes #1634
2016-10-06 15:14:35 +02:00
Julien Fontanet
9f450d282e chore(package): use index-modules 2016-10-06 14:41:46 +02:00
Pierre Donias
31787067e3 feat(new-vm): set dynamic and static memory bounds (#1618)
Fixes #1603
2016-10-05 17:27:59 +02:00
fufroma
1a769b23e2 feat(i18n): update French translation (#1600) 2016-10-05 10:38:12 +02:00
Olivier Lambert
ae002abafc feat(home/pool): bar for pool RAM usage (#1626)
Fixes #1625
2016-10-05 10:26:47 +02:00
Julien Fontanet
31a25d9c16 5.2.4 2016-10-04 15:35:11 +02:00
Julien Fontanet
356295c361 fix(package): add missing make-error 2016-10-04 15:33:24 +02:00
Julien Fontanet
d10681b6d1 fix(package): add missing even-to-promise 2016-10-04 15:33:24 +02:00
Julien Fontanet
0602410aa8 fix(package): update xo-acl-resolver to 0.2.2
Fixes vatesfr/xo-web#1621
2016-10-04 15:33:23 +02:00
Olivier Lambert
1112768adc feat(home/host): add memory bar (#1617)
Fixes #1616
2016-10-03 18:06:36 +02:00
Julien Fontanet
86b599df89 5.2.3 2016-10-03 09:39:59 +02:00
Olivier Lambert
88f7661172 feat(changelog): add info for 5.2.3 release 2016-10-03 09:21:45 +02:00
Julien Fontanet
29c96c0119 chore(gitignore): pnpm compat 2016-10-03 09:15:18 +02:00
Julien Fontanet
d8c6e54c68 fix(user): add VM template label 2016-10-03 09:13:42 +02:00
Julien Fontanet
df053eb016 fix(user): do not crash on missing type label 2016-10-03 09:13:26 +02:00
Julien Fontanet
d1715f7711 chore(intl): homeTypeTemplate → homeTypeVmTemplate 2016-10-03 09:09:40 +02:00
Julien Fontanet
240282c72d chore(intl): remove unused message 2016-10-03 09:09:03 +02:00
Olivier Lambert
9e8dd6ea21 fix(README): broken link to doc 2016-10-02 23:23:19 +02:00
Julien Fontanet
32806a20c9 fix(sr): goes to homepage if object disappear
Fixes #1611
2016-10-02 23:02:14 +02:00
Pierre Donias
34dcfbbf49 fix(home/item): prevent item from being displayed on 2 rows (#1608)
Fixes #1580
2016-09-30 13:33:05 +02:00
Pierre Donias
91fec43866 feat(pool/network): create a bonded network (#1605)
See #876
2016-09-30 11:23:44 +02:00
Greenkeeper
aa2d196a79 chore(package): update vinyl to version 2.0.0 (#1607)
https://greenkeeper.io/
2016-09-29 23:06:53 +02:00
Pierre Donias
180ca458ad feat(vm/network): allow VIF edition (#1596)
See #1446
2016-09-28 14:29:15 +02:00
Greenkeeper
aa881c60e7 chore(package): update babel-eslint to version 7.0.0 (#1597)
https://greenkeeper.io/
2016-09-27 23:39:03 +02:00
Greenkeeper
5b6966042d standard@8.2.0 breaks build 🚨 (#1595)
https://greenkeeper.io/
2016-09-27 01:11:46 +02:00
Julien Fontanet
dc859da0cd chore(build): remove embedded dev server 2016-09-26 16:45:02 +02:00
Pierre Donias
151eb6cbd6 feat(updates,users,servers): disable credentials autocomplete (#1592)
Fixes #1304
2016-09-26 16:00:04 +02:00
Olivier Lambert
16db591bbf feat(vm): add red icon if VM doesn't have tools. Fixes #1575 2016-09-26 12:24:58 +02:00
Pierre Donias
05a55e5eb2 fix(xoa-upgrade): more suitable message for non-admin users (#1591)
Fixes #1564
2016-09-26 10:31:23 +02:00
Pierre Donias
dcd84b2b8f feat(shortcuts): help modal and new home shortcuts (#1588)
Fixes #1578
2016-09-23 18:43:10 +02:00
Greenkeeper
4a89119f0a Update react-virtualized to version 8.0.8 🚀 (#1587)
https://greenkeeper.io/
2016-09-23 17:11:55 +02:00
Julien Fontanet
bc1c30a7bf chore(package): add __self prop, better React warnings 2016-09-23 16:02:19 +02:00
Julien Fontanet
33cffbf28b fix(Copiable): do not pass tagName prop to wrapper 2016-09-23 15:50:40 +02:00
Julien Fontanet
a18b68116c chore(package): improve React stack traces in dev build 2016-09-23 15:45:03 +02:00
Pierre Donias
d5acf15bca feat(vm/network): indicate when an IP is already used (#1584)
Fixes #1566
2016-09-23 12:13:48 +02:00
Pierre Donias
84f970af68 fix(shortcuts): prevent Shortcuts from stopping events propagation (#1583)
`Editable`s were broken (could not use *enter* to save).
2016-09-23 11:35:56 +02:00
Julien Fontanet
969f636bb7 fix(host/storage): name sorting 2016-09-23 10:54:36 +02:00
Pierre Donias
6939aee20a feat(home): keyboard shortcuts (#1400)
Fixes #1279
2016-09-22 19:01:05 +02:00
Pierre Donias
ab2a02a555 fix(vm/tab-network): lock icon conditions (#1576)
Fixes #1573
2016-09-22 16:52:37 +02:00
Pierre Donias
70038e0764 fix(new-vm): lodash/sum instead of lodash/sumBy (#1577) 2016-09-22 16:19:34 +02:00
Olivier Lambert
e730ef5e11 feat(host/sr): Sr link and better storage tab. (#1572)
Fixes #1567
2016-09-22 16:04:45 +02:00
Olivier Lambert
835ad5aaf1 feat(vm/host/pools/sr): add tooltips. Fixes #1568 2016-09-22 11:02:05 +02:00
Pierre Donias
ac645c8617 fix(home): types dropdown button title (#1570) 2016-09-22 10:47:48 +02:00
Julien Fontanet
b801fdbab2 5.2.2 2016-09-21 18:01:03 +02:00
Pierre Donias
bf495953e2 feat(new-vm): show resource set limits (#1563)
Fixes #1541
2016-09-21 11:58:07 -04:00
Pierre Donias
45b165deec fix(home): VM bulk restart (#1562)
Fixes #1561
2016-09-21 14:48:16 +02:00
Olivier Lambert
09169578e8 feat(changelog): add issue #1562 2016-09-21 14:47:52 +02:00
Olivier Lambert
43b2366927 feat(changelog): update changelog for 5.2.2 2016-09-21 12:22:37 +02:00
Julien Fontanet
f015a69eec feat(host/patches): display if needs to be restarted (#1559)
Fixes #1352
2016-09-21 10:10:44 +02:00
Olivier Lambert
99568508dd fix(charts): change color order to avoid confusions. Fixes #1265 2016-09-20 18:41:44 +02:00
Olivier Lambert
e8515344dd feat(home): display a message if a filter is empty. (#1560)
feat(home): display a message if a filter is empty. Fixes #1517
2016-09-20 12:34:07 -04:00
Julien Fontanet
edc873a570 fix(pool/storage): fix sort by usage 2016-09-20 15:53:45 +02:00
Julien Fontanet
1a03e96ab2 fix(pool/storage): fix passing pool to SortedTable 2016-09-20 15:53:25 +02:00
Olivier Lambert
89e0bb4f0a feat(home/templates): template management (#1533)
Fixes #1091
2016-09-20 13:46:58 +02:00
Olivier Lambert
7d0fd60908 feat(pool/storage): display default SR and add button to set it (#1557)
Fixes #1554
2016-09-20 13:19:47 +02:00
Pierre Donias
6b20523df4 fix(vm/tab-network): check for undefined network (#1556)
Fixes #1518
2016-09-20 11:52:17 +02:00
Julien Fontanet
e9a612647e fix(home): do not overwrite current filter (#1555)
Fixes #1513
2016-09-20 05:38:33 -04:00
Julien Fontanet
28404ef149 feat(SortedTable): default column can be set by a simple prop 2016-09-20 09:43:57 +02:00
Olivier Lambert
a5f8230def feat(self): hide some buttons and tabs for self service VMs (#1550) 2016-09-19 15:17:45 +02:00
Pierre Donias
39171de5de fix(job/new): forbid "_" character in job names (#1548)
Fixes #1414
2016-09-19 11:58:46 +02:00
Pierre Donias
5aa5a0acbc fix(home): set items per page in createPager (#1549) 2016-09-19 11:39:56 +02:00
Olivier Lambert
a4518e630a fix(log): sort logs by end date 2016-09-19 11:04:15 +02:00
Julien Fontanet
94975f5ea6 feat(settings/logs): group action buttons
Fixes #1547
2016-09-19 10:59:19 +02:00
Julien Fontanet
7e98838d96 feat(ActionButton): can have a tooltip 2016-09-19 10:59:19 +02:00
Pierre Donias
e8c9c196ff feat(vm/snapshot): "Snapshot before" checkbox in revert modal (#1543)
Fixes #1445
2016-09-17 00:11:51 +02:00
Julien Fontanet
db314a238f fix(xo/subcriptions): mark as non-running on error 2016-09-16 17:13:56 +02:00
Julien Fontanet
2c85a6d4ab fix(self/admin): do not fail on empty limits
Fixes #1537
2016-09-16 17:13:56 +02:00
Pierre Donias
b683e14e80 feat(self): merge dashboard and administration (#1542)
Fixes #1429
2016-09-16 16:47:20 +02:00
Olivier Lambert
ba45095fa8 fix(editable): limit input size for long text to 50ex. Fixes #1528 2016-09-15 16:45:19 +02:00
Olivier Lambert
b8e5ffa9f7 feat(backup): improved view. Fixes #1534 2016-09-15 16:26:42 +02:00
Pierre Donias
b4bff9e032 feat(vm/disks): "Long click to migrate" tooltip on SR (#1529)
* feat(vm/disks): "Long click to migrate" tooltip on SR

Fixes #1512

* feat(vm/disks): migrate VDI button, force cursor on editable

* fix bulk VDI migration

* Return Promise.all

* Ternary operator
2016-09-15 16:08:50 +02:00
Olivier Lambert
0c461bc4e2 feat(backuplogs): shorten the start/end date format. Fixes #1535 2016-09-15 15:55:01 +02:00
Olivier Lambert
a33b2a5294 fix(home/menu): adapt message depending of user perm (#1532)
Fixes #1519
2016-09-15 13:56:33 +02:00
Olivier Lambert
298e1c4471 fix(host console): removing useless ISO selector for host. Fixes #1527 2016-09-15 13:07:33 +02:00
Julien Fontanet
1c70cdc10b fix(render-xo-item): remove unused import 2016-09-15 13:07:04 +02:00
Julien Fontanet
160e4bb530 fix(SelectVmTemplates): use correct placeholder
Fixes #1530
2016-09-15 12:00:32 +02:00
fufroma
e69ba8dd96 Fix typo on Changelog (#1526) 2016-09-15 04:44:42 -04:00
Julien Fontanet
e55f4c3eb2 feat(settings/logs): show complete error object 2016-09-14 17:42:32 +02:00
Pierre Donias
1a3272b980 feat(new-vm): auto poweron setting (#1514)
Fixes #1444
2016-09-14 15:25:54 +02:00
Julien Fontanet
7bed5e025a fix(home): fix empty default filter (#1504)
Fixes #1354
2016-09-14 14:38:24 +02:00
Pierre Donias
29d22c0598 feat(backup/new): confirm modal when local remote is selected (#1507)
Fixes #1441
2016-09-14 13:20:51 +02:00
Greenkeeper
a38c7c34ac chore(package): update react-key-handler to version 0.3.0 (#1505)
https://greenkeeper.io/
2016-09-14 01:42:16 +02:00
Julien Fontanet
8d690ce4ff 5.2.1 2016-09-13 16:54:42 +02:00
Olivier Lambert
2569568a03 feat(changelog): patch version 5.2.1 changelog 2016-09-13 16:52:47 +02:00
Olivier Lambert
2c6ff6b5b8 feat(stopHost modal): explain consequences if halting a pool master. Fixes #1458 2016-09-13 15:28:30 +02:00
Pierre Donias
1257f01027 feat(new-vm): create tags during VM creation (#1501)
See #1431
2016-09-13 14:17:04 +02:00
Pierre Donias
fad6830863 fix(settings/remotes): SMB domain is required (#1502)
Fixes #1499
2016-09-13 11:28:07 +02:00
Olivier Lambert
66262bb20b fix(vm console): re-fix issue #1485 2016-09-13 11:01:18 +02:00
Julien Fontanet
4abb0754c7 fix(remotes): edition of URL parts (#1500)
Fixes #1498
2016-09-13 10:20:48 +02:00
Pierre Donias
78c53bf3ad fix(xo): better message for snapshot deletion (#1497)
Fixes #1433
2016-09-13 09:42:18 +02:00
Pierre Donias
810d666d84 feat(home): tooltips on bulk action buttons (#1496)
Fixes #1456
2016-09-12 17:04:02 +02:00
Pierre Donias
67699f0bb6 feat(pool/network): SortedTable and collapsable PIFs table (#1494)
Fixes #1461
2016-09-12 16:44:43 +02:00
Pierre Donias
46274948c0 fix(settings/logs): properly handle unknown user (#1495) 2016-09-12 16:42:52 +02:00
Olivier Lambert
28e3a842ef feat(vm): new containers tab (#1492)
Fixes #1442
2016-09-12 13:50:14 +02:00
Pierre Donias
6d90f1d45d fix(user): prevent SSH key overflow (#1491)
Fixes #1475
2016-09-12 12:57:11 +02:00
Pierre Donias
09642c347d feat(new-vm): Advanced section (#1486)
Fixes #1444
2016-09-12 12:27:11 +02:00
Pierre Donias
2d0e06f785 fix(backup/new): better month and week days layout (#1490)
Fixes #1488
2016-09-12 11:58:24 +02:00
Pierre Donias
a5bc8497cf fix(vm/tab-network): set default network and MTU for VIF creation (#1487)
Fixes #1472
2016-09-12 11:26:32 +02:00
Julien Fontanet
4bcb65c518 5.2.0 2016-09-09 16:31:24 +02:00
Olivier Lambert
25361fa7eb feat(changelog): add IP management feature in changelog 2016-09-09 16:30:13 +02:00
Pierre Donias
889a265000 feat(settings/ips): new page to manage IP pools (#1418)
Fixes #1350, fixes #988 and fixes #240.
2016-09-09 16:19:26 +02:00
Olivier Lambert
3122f6dcd5 feat(changelog): update changelog for 5.2.0 2016-09-09 14:21:54 +02:00
Olivier Lambert
16aa2e8085 bug(vmConsole): re-display header if VM goes from running to halted state. Fixes #1485 2016-09-08 16:29:56 +02:00
Julien Fontanet
074d51a670 fix(xo/deleteVms): delete disks as well
Fixes #1484
2016-09-08 15:06:42 +02:00
Greenkeeper
2122a79132 chore(package): update chartist-plugin-legend to version 0.5.0 (#1482)
https://greenkeeper.io/
2016-09-08 11:01:50 +02:00
Ronan Abhamon
26dbc585ba feat(vm-import): supports ova import (#709) (#1361)
Fixes #709
2016-09-08 10:15:44 +02:00
Greenkeeper
4b3cfbd424 chore(package): update modular-css to version 0.27.1 (#1478)
https://greenkeeper.io/
2016-09-08 09:55:38 +02:00
Julien Fontanet
035191a2cc feat(xo/installAllPatchesOnPool): better pool-wide patching (#1476)
Fixes #1392
2016-09-07 17:55:02 +02:00
Olivier Lambert
06a40180a1 fix(vm): do not display VDI or VIF disconnect but when VM is not running. Fixes #1470 and fixes #1468 2016-09-05 16:59:21 +02:00
Julien Fontanet
aaf4c5dff7 chore(Vm): move isRunning into utils/isVmRunning 2016-09-05 16:40:13 +02:00
Olivier Lambert
0c83bc2b0e feat(logview): standardize and improve the settings/log view. Fixes #1467 2016-09-05 16:10:12 +02:00
Julien Fontanet
2d412fd8db fix(scheduling): month and day display
There were issues with some timezones.

Fixes #1404 & fixes #1438.
2016-09-01 11:47:50 -03:00
Julien Fontanet
443e2bec25 chore(NewVm#_getIsoPredicate): memoise selector 2016-09-01 10:57:13 -03:00
Olivier Lambert
d5e1323d82 feat(newVif): select management network by default when adding a vif. Fixes #1425 2016-09-01 15:34:49 +02:00
Julien Fontanet
7f0b77cc89 chore(package): update chartist-plugin-legend to version 0.4.0 (#1450)
https://greenkeeper.io/
2016-08-31 10:32:50 +02:00
greenkeeperio-bot
0169cff66c chore(package): update chartist-plugin-legend to version 0.4.0
https://greenkeeper.io/
2016-08-31 10:17:48 +02:00
Olivier Lambert
0fd1424a41 fix(newVm): check pool object for ISO selector when creating a VM from selfservice. Fixes #1448 2016-08-30 21:26:59 +02:00
Julien Fontanet
6280d56f32 chore(xo): use resolveId() (only) where it makes sense 2016-08-26 16:18:03 -04:00
Julien Fontanet
9f2a77872f fix(xo/deleteUser): dont attempt to display error when cancelled
Fixes vatesfr/xo-web#1439
2016-08-26 14:38:02 -04:00
Pierre Donias
b571c18e9a feat(host): indicate pool master in multiple places (#1423)
Fixes #1407
2016-08-25 12:33:55 -04:00
Greenkeeper
49863d6e4d Update standard to version 8.0.0 🚀 (#1435)
https://greenkeeper.io/
2016-08-24 13:00:00 -04:00
Julien Fontanet
48cc7bb647 5.1.9 2016-08-22 14:02:40 -04:00
Pierre Donias
442d42d8dc fix(settings/logs): show params in a modal (#1424) 2016-08-19 16:09:20 +02:00
Olivier Lambert
9501ebacfc feat(menu): add warning icon when disconnected 2016-08-19 13:52:31 +02:00
Pierre Donias
23f9fa46f8 fix(home/host-item): do not show pool name if not enough permissions (#1421) 2016-08-19 13:40:55 +02:00
Julien Fontanet
1bd0f37fd4 feat(Menu): display when disconnected
Fixes vatesfr/xo-web#1417
2016-08-19 12:27:48 +02:00
Pierre Donias
ed74ded923 feat(settings/logs): display parameters (#1420) 2016-08-19 10:06:45 +02:00
Olivier Lambert
b732410b74 feat(vm and home): add tooltip to OS icon. Fixes #1416 2016-08-18 14:12:46 +02:00
Olivier Lambert
a51f2b7fcf fix(newvm): check if ssh keys object exists 2016-08-18 13:59:08 +02:00
Olivier Lambert
fe12bbb60d fix(sr): container var check if not defined 2016-08-18 13:51:37 +02:00
Olivier Lambert
8882df7939 5.1.8 2016-08-17 11:07:30 +02:00
Olivier Lambert
185a554cd9 fix(newVm): fix wrong ISO SR predicate. Fixes #1415 2016-08-17 11:06:35 +02:00
Olivier Lambert
230e0dc2a5 5.1.7 2016-08-16 15:38:59 +02:00
Pierre Donias
f5b69fdfdc feat(vm/console): hide header and resize console (#1410)
Fix #1268
2016-08-16 14:49:44 +02:00
Greenkeeper
01dc0d8f1e chore(package): update modular-css to version 0.26.0 (#1385)
https://greenkeeper.io/
2016-08-16 12:45:29 +02:00
Greenkeeper
8035886a3c chore(package): update promise-toolbox to version 0.5.0 (#1409)
https://greenkeeper.io/
2016-08-16 12:22:45 +02:00
Olivier Lambert
0ab5f4b13f fix(host): wrong storage link. Fixes #1408 2016-08-16 11:16:56 +02:00
Pierre Donias
a1bc98def8 feat(host): redirect to home when host disappears (#1406) 2016-08-16 09:50:29 +02:00
Olivier Lambert
868cf6140b feat(settings): more tooltips for server connect/disconnect 2016-08-15 18:04:01 +02:00
Olivier Lambert
4b3473f480 feat(logstackmodal): use pre tag for stack trace 2016-08-15 17:52:04 +02:00
Olivier Lambert
7bc782cc62 feat(copiable): add tooltip on copiable component 2016-08-15 17:33:59 +02:00
Olivier Lambert
e625a53e4a fix(vm migration): allow target network without IPs. Fixes #1403 2016-08-15 15:20:59 +02:00
Olivier Lambert
b31185d96d fix(newVm): typo spotted by @Danp2 2016-08-15 14:07:12 +02:00
Olivier Lambert
09d75e972f feat(newVm): add missing tooltips. Fixes #1402 2016-08-15 11:44:36 +02:00
Olivier Lambert
f33568951b 5.1.6 2016-08-12 17:28:49 +02:00
Pierre Donias
8d8c442be5 feat(settings/logs): new view to display API logs (#1401)
Fix #1344
2016-08-12 17:27:50 +02:00
Olivier Lambert
f890b8ea7a feat(modal text): warns users about consequences of host eject 2016-08-11 21:13:32 +02:00
Pierre Donias
1b80b3929c feat(host): detach host from its pool (#1399)
Fixes #1395
2016-08-11 17:49:25 +02:00
Pierre Donias
4f946293f6 feat(pool): add host (#1398)
Fixes #1374
2016-08-11 17:05:41 +02:00
Olivier Lambert
36788cde2b feat(vm disk): add VBD connect for a running VM. Fixes #1397 2016-08-11 16:52:52 +02:00
Pierre Donias
1547c99e5a feat(new-vm): use saved SSH key in cloud config(#1394)
* feat(new-vm): use saved SSH key in cloud config. Fixes #1319
2016-08-11 13:32:54 +02:00
Olivier Lambert
5c9606dad8 feat(pool): improve pool view. Fixes #1393 2016-08-11 10:34:03 +02:00
Olivier Lambert
fdcb1dccf5 feat(pool): start to work on adding an existing host to a pool 2016-08-11 09:47:52 +02:00
Olivier Lambert
12812b8c23 5.1.5 2016-08-10 18:06:19 +02:00
Olivier Lambert
0098497255 fix(select): select color modified due to an update. Fixes #1391 2016-08-10 16:02:48 +02:00
Olivier Lambert
6562d2de7f feat(sr select): display space left on SR. Fixes #1358 2016-08-10 15:58:36 +02:00
Olivier Lambert
1f0e88cdb0 feat(backup): better tooltips. Fixes #1363 2016-08-10 14:17:27 +02:00
Olivier Lambert
197da91ef3 feat(vdi remove): add modal when removing a VDI. Fixes #1388 2016-08-10 13:39:13 +02:00
Olivier Lambert
cbd59789e2 fix(vm disks): _isFreeForWriting missing case. Fixes #1386 2016-08-10 13:13:17 +02:00
Olivier Lambert
190ecf3d74 fix(pool): pool name and description edition. Fixes #1390 2016-08-10 12:42:46 +02:00
Olivier Lambert
15b8f6bca2 feat(meter tooltips): add tooltips for meter object. Fixes #1387 2016-08-10 12:31:28 +02:00
Pierre Donias
5b406d731b fix(vm): select destination SR when at least one VDI is local (#1382)
* Fixes #1357 
* fix(vm): select destination SR when at least one VDI is local
* fix(vm): do not send map when not necessary
2016-08-09 17:03:08 +02:00
Olivier Lambert
4be9e67ac4 fix(metercss): remove useless and conflicting CSS styles 2016-08-09 10:31:03 +02:00
Olivier Lambert
d047421685 feat(updates): enhance update view. Also fixes #1341 2016-08-08 16:46:47 +02:00
Olivier Lambert
f6f415a421 fix(network): name instead of description 2016-08-08 14:55:15 +02:00
Pierre Donias
edfaaebac0 feat(dashboard/health): Storage table: BlockLink (SR) and Link (SR's pool)
Fixes #1381
2016-08-08 14:15:49 +02:00
Olivier Lambert
67df22a1bf feat(vmsnapshot): add snapshot export and copy. Fixes #1353 and #1336 2016-08-08 14:05:27 +02:00
Pierre Donias
7dc59a00f6 feat(pool): action button to create an SR (#1380)
Fixes #1372
2016-08-08 12:45:12 +02:00
Pierre Donias
6214fe4c2e feat(pool): action button to create a VM (#1379)
Fix #1373
2016-08-08 11:35:24 +02:00
Greenkeeper
21610c3e0a chore(package): update ava to version 0.16.0 (#1377)
https://greenkeeper.io/
2016-08-08 09:57:36 +02:00
Olivier Lambert
87550b0189 5.1.4 2016-08-07 19:35:37 +02:00
Ronan Abhamon
b7c42d0a08 fix(scheduling): range not working
Fixes #1376
2016-08-07 19:35:05 +02:00
Olivier Lambert
c15ad299ac fix(sparklines): smaller sparklines and removing useless dots 2016-08-05 14:39:07 +02:00
Olivier Lambert
48c56cd602 5.1.3 2016-08-05 12:42:01 +02:00
Ronan Abhamon
7957f621ef fix(backups-edit): display correctly old backup jobs
Fixes #1366
2016-08-05 12:12:20 +02:00
Olivier Lambert
38ddbfdc9c fix(dashboard): inverted value for SR total/used. Fixes #1370 2016-08-04 14:42:50 +02:00
Olivier Lambert
3d2aae81da 5.1.2 2016-08-03 17:22:49 +02:00
Olivier Lambert
2227b9d061 feat(new vm): hide URL install for HVM templates. Fixes #1362 2016-08-03 16:55:43 +02:00
Olivier Lambert
12aab5fa8c feat(snapshots): add tooltips and confirm modal for snapshot oprations. Fixes #1349 2016-08-03 16:34:31 +02:00
Olivier Lambert
7323e6e117 fix(tooltip): remove tooltip if button changes. Fixes #1360 2016-08-03 15:53:38 +02:00
Greenkeeper
6f36869609 chore(package): update gulp-uglify to version 2.0.0 (#1355)
https://greenkeeper.io/
2016-08-02 14:47:18 +02:00
Ronan Abhamon
4a12419162 feat(backups): supports smart backup (close #837) (#1335)
Fixes #837
2016-07-31 19:03:41 +02:00
ABHAMON Ronan
bf91938aa6 fix(form/Toggle): refresh when set is used (#1347)
Fixes #1339
2016-07-29 11:35:58 +02:00
ABHAMON Ronan
bd70bd2b45 fix(scheduling): fix month selection highlighting (#1345)
Fixes #1338
2016-07-29 10:31:42 +02:00
Greenkeeper
bb26c8e449 chore(package): update modular-css to version 0.25.0 (#1331)
https://greenkeeper.io/
2016-07-28 00:16:06 +02:00
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
150 changed files with 14836 additions and 5287 deletions

8
.gitignore vendored
View File

@@ -1,9 +1,7 @@
/.nyc_output/
/bower_components/
/dist/
/node_modules/
npm-debug.log
npm-debug.log.*
!node_modules/*
node_modules/*/
pnpm-debug.log
pnpm-debug.log.*

View File

@@ -1,5 +1,270 @@
# ChangeLog
## **5.2.5** (2016-10-07)
### Enhancements
- Stats in home/host view when expanded [\#1634](https://github.com/vatesfr/xo-web/issues/1634)
- Bar for used and total RAM on home pool view [\#1625](https://github.com/vatesfr/xo-web/issues/1625)
- Can't translate some text [\#1624](https://github.com/vatesfr/xo-web/issues/1624)
- Dynamic RAM allocation at creation time [\#1603](https://github.com/vatesfr/xo-web/issues/1603)
### Bug fixes
- Do not expose shortcuts while console is focused [\#1614](https://github.com/vatesfr/xo-web/issues/1614)
## **5.2.4** (2016-10-04)
### Enhancements
- Display memory bar in home/host view [\#1616](https://github.com/vatesfr/xo-web/issues/1616)
### Bug fixes
- All users can see VM templates [\#1621](https://github.com/vatesfr/xo-web/issues/1621)
## **5.2.3** (2016-10-03)
### Enhancements
- Improve keyboard navigation [\#1578](https://github.com/vatesfr/xo-web/issues/1578)
- Strongly suggest to install the guest tools [\#1575](https://github.com/vatesfr/xo-web/issues/1575)
- Missing tooltip [\#1568](https://github.com/vatesfr/xo-web/issues/1568)
- Emphasize already used ips in ipPools [\#1566](https://github.com/vatesfr/xo-web/issues/1566)
- Change "missing feature message" for non-admins [\#1564](https://github.com/vatesfr/xo-web/issues/1564)
- Allow VIF edition [\#1446](https://github.com/vatesfr/xo-web/issues/1446)
- Disable browser autocomplete on credentials on the Update page [\#1304](https://github.com/vatesfr/xo-web/issues/1304)
- keyboard shortcuts [\#1279](https://github.com/vatesfr/xo-web/issues/1279)
- Add network bond creation [\#876](https://github.com/vatesfr/xo-web/issues/876)
### Bug fixes
- Profile page is broken [\#1612](https://github.com/vatesfr/xo-web/issues/1612)
- SR delete should redirect to home [\#1611](https://github.com/vatesfr/xo-web/issues/1611)
- Delta VHD backup checksum is invalidated by chaining [\#1606](https://github.com/vatesfr/xo-web/issues/1606)
- VM with long description break on 2 lines [\#1580](https://github.com/vatesfr/xo-web/issues/1580)
- Network status on VM edition [\#1573](https://github.com/vatesfr/xo-web/issues/1573)
- VM template deletion fails [\#1571](https://github.com/vatesfr/xo-web/issues/1571)
- Template edition - "no such object" [\#1569](https://github.com/vatesfr/xo-web/issues/1569)
- missing links / element not displayed as links [\#1567](https://github.com/vatesfr/xo-web/issues/1567)
- Backup restore stalled on some SMB shares [\#1412](https://github.com/vatesfr/xo-web/issues/1412)
- Wrong bond display [\#1156](https://github.com/vatesfr/xo-web/issues/1156)
## **5.2.2** (2016-09-21)
### Enhancements
- `pool.setDefaultSr\(\)` should not require `pool` param [\#1558](https://github.com/vatesfr/xo-web/issues/1558)
- Select default SR [\#1554](https://github.com/vatesfr/xo-web/issues/1554)
- No error message when I exceed my resource set quota [\#1541](https://github.com/vatesfr/xo-web/issues/1541)
- Hide some buttons for self service VMs [\#1539](https://github.com/vatesfr/xo-web/issues/1539)
- Add Job ID to backup schedules [\#1534](https://github.com/vatesfr/xo-web/issues/1534)
- Correct name for VM selector with templates [\#1530](https://github.com/vatesfr/xo-web/issues/1530)
- Help text when no matches for a filter [\#1517](https://github.com/vatesfr/xo-web/issues/1517)
- Icon or tooltip to allow VDI migration in VM disk view [\#1512](https://github.com/vatesfr/xo-web/issues/1512)
- Create a snapshot before restoring one [\#1445](https://github.com/vatesfr/xo-web/issues/1445)
- Auto power on setting at creation time [\#1444](https://github.com/vatesfr/xo-web/issues/1444)
- local remotes should be avoided if possible [\#1441](https://github.com/vatesfr/xo-web/issues/1441)
- Self service edition unclear [\#1429](https://github.com/vatesfr/xo-web/issues/1429)
- Avoid "\_" char in job tag name [\#1414](https://github.com/vatesfr/xo-web/issues/1414)
- Display message if host reboot needed to apply patches [\#1352](https://github.com/vatesfr/xo-web/issues/1352)
- Color code on host PIF stats can be misleading [\#1265](https://github.com/vatesfr/xo-web/issues/1265)
- Sign in page is not rendered correctly [\#1161](https://github.com/vatesfr/xo-web/issues/1161)
- Template management [\#1091](https://github.com/vatesfr/xo-web/issues/1091)
### Bug fixes
- Multiple reboot selection doesn't work [\#1562](https://github.com/vatesfr/xo-web/issues/1562)
- Server logs should be displayed in reverse chonological order [\#1547](https://github.com/vatesfr/xo-web/issues/1547)
- Cannot create resource sets without limits [\#1537](https://github.com/vatesfr/xo-web/issues/1537)
- UI - Weird display when editing long VM desc [\#1528](https://github.com/vatesfr/xo-web/issues/1528)
- Useless iso selector in host console [\#1527](https://github.com/vatesfr/xo-web/issues/1527)
- Pool and Host dummy welcome message [\#1519](https://github.com/vatesfr/xo-web/issues/1519)
- Bug on Network VM tab [\#1518](https://github.com/vatesfr/xo-web/issues/1518)
- Link to home with filter in query does not work [\#1513](https://github.com/vatesfr/xo-web/issues/1513)
- VHD merge fails with "RangeError: index out of range" on SMB remote [\#1511](https://github.com/vatesfr/xo-web/issues/1511)
- DR: previous VDIs are not removed [\#1510](https://github.com/vatesfr/xo-web/issues/1510)
- DR: previous copies not removed when same number as depth [\#1509](https://github.com/vatesfr/xo-web/issues/1509)
- Empty Saved Search doesn't load when set to default filter [\#1354](https://github.com/vatesfr/xo-web/issues/1354)
- Removing a user/group should delete its ACLs [\#899](https://github.com/vatesfr/xo-web/issues/899)
- OVA Import - XO stuck during import [\#1551](https://github.com/vatesfr/xo-web/issues/1551)
## **5.2.1** (2016-09-13)
### Enhancements
- On pool view: collapse network list [\#1461](https://github.com/vatesfr/xo-web/issues/1461)
- Alert when trying to reboot/halt the pool master XS [\#1458](https://github.com/vatesfr/xo-web/issues/1458)
- Adding tooltip on Home page [\#1456](https://github.com/vatesfr/xo-web/issues/1456)
- Docker container management functionality missing from v5 [\#1442](https://github.com/vatesfr/xo-web/issues/1442)
- bad error message - delete snapshot [\#1433](https://github.com/vatesfr/xo-web/issues/1433)
- Create tag during VM creation [\#1431](https://github.com/vatesfr/xo-web/issues/1431)
### Bug fixes
- SMB remote empty domain fails [\#1499](https://github.com/vatesfr/xo-web/issues/1499)
- Can't edit a remote password [\#1498](https://github.com/vatesfr/xo-web/issues/1498)
- Issue in VM create with CoreOS [\#1493](https://github.com/vatesfr/xo-web/issues/1493)
- Overlapping months in backup view [\#1488](https://github.com/vatesfr/xo-web/issues/1488)
- No line break for SSH key in user view [\#1475](https://github.com/vatesfr/xo-web/issues/1475)
- Create VIF UI issues [\#1472](https://github.com/vatesfr/xo-web/issues/1472)
## **5.2.0** (2016-09-09)
### Enhancements
- IP management [\#1350](https://github.com/vatesfr/xo-web/issues/1350), [\#988](https://github.com/vatesfr/xo-web/issues/988), [\#1427](https://github.com/vatesfr/xo-web/issues/1427) and [\#240](https://github.com/vatesfr/xo-web/issues/240)
- Update reverse proxy example [\#1474](https://github.com/vatesfr/xo-web/issues/1474)
- Improve log view [\#1467](https://github.com/vatesfr/xo-web/issues/1467)
- Backup Reports: e-mail subject [\#1463](https://github.com/vatesfr/xo-web/issues/1463)
- Backup Reports: report the error [\#1462](https://github.com/vatesfr/xo-web/issues/1462)
- Vif selector: select management network by default [\#1425](https://github.com/vatesfr/xo-web/issues/1425)
- Display when browser disconnected to server [\#1417](https://github.com/vatesfr/xo-web/issues/1417)
- Tooltip on OS icon in VM view [\#1416](https://github.com/vatesfr/xo-web/issues/1416)
- Display pool master [\#1407](https://github.com/vatesfr/xo-web/issues/1407)
- Missing tooltips in VM creation view [\#1402](https://github.com/vatesfr/xo-web/issues/1402)
- Handle VDB disconnect and connect [\#1397](https://github.com/vatesfr/xo-web/issues/1397)
- Eject host from a pool [\#1395](https://github.com/vatesfr/xo-web/issues/1395)
- Improve pool general view [\#1393](https://github.com/vatesfr/xo-web/issues/1393)
- Improve patching system [\#1392](https://github.com/vatesfr/xo-web/issues/1392)
- Pool name modification [\#1390](https://github.com/vatesfr/xo-web/issues/1390)
- Confirmation dialog before destroying VDIs [\#1388](https://github.com/vatesfr/xo-web/issues/1388)
- Tooltips for meter object [\#1387](https://github.com/vatesfr/xo-web/issues/1387)
- New Host assistant [\#1374](https://github.com/vatesfr/xo-web/issues/1374)
- New VM assistant [\#1373](https://github.com/vatesfr/xo-web/issues/1373)
- New SR assistant [\#1372](https://github.com/vatesfr/xo-web/issues/1372)
- Direct access to VDI listing from dashboard's SR usage breakdown [\#1371](https://github.com/vatesfr/xo-web/issues/1371)
- Can't set a network name at pool level [\#1368](https://github.com/vatesfr/xo-web/issues/1368)
- Change a few mouse over descriptions [\#1363](https://github.com/vatesfr/xo-web/issues/1363)
- Hide network install in VM create if template is HVM [\#1362](https://github.com/vatesfr/xo-web/issues/1362)
- SR space left during VM creation [\#1358](https://github.com/vatesfr/xo-web/issues/1358)
- Add destination SR on migration modal in VM view [\#1357](https://github.com/vatesfr/xo-web/issues/1357)
- Ability to create a new VM from a snapshot [\#1353](https://github.com/vatesfr/xo-web/issues/1353)
- Missing explanation/confirmation on Snapshot Page [\#1349](https://github.com/vatesfr/xo-web/issues/1349)
- Log view: expose API errors in the web UI [\#1344](https://github.com/vatesfr/xo-web/issues/1344)
- Registration on update page [\#1341](https://github.com/vatesfr/xo-web/issues/1341)
- Add export snapshot button [\#1336](https://github.com/vatesfr/xo-web/issues/1336)
- Use saved SSH keys in VM create CloudConfig [\#1319](https://github.com/vatesfr/xo-web/issues/1319)
- Collapse header in console view [\#1268](https://github.com/vatesfr/xo-web/issues/1268)
- Two max concurrent jobs in parallel [\#915](https://github.com/vatesfr/xo-web/issues/915)
- Handle OVA import via the web UI [\#709](https://github.com/vatesfr/xo-web/issues/709)
### Bug fixes
- Bug on VM console when header is hidden [\#1485](https://github.com/vatesfr/xo-web/issues/1485)
- Disks not removed when deleting multiple VMs [\#1484](https://github.com/vatesfr/xo-web/issues/1484)
- Do not display VDI disconnect button when a VM is not running [\#1470](https://github.com/vatesfr/xo-web/issues/1470)
- Do not display VIF disconnect button when a VM is not running [\#1468](https://github.com/vatesfr/xo-web/issues/1468)
- Error on migration if no default SR \(even when not used\) [\#1466](https://github.com/vatesfr/xo-web/issues/1466)
- DR issue while rotating old backup [\#1464](https://github.com/vatesfr/xo-web/issues/1464)
- Giving resource set to end-user ends with error [\#1448](https://github.com/vatesfr/xo-web/issues/1448)
- Error thrown when cancelling out of Delete User confirmation dialog [\#1439](https://github.com/vatesfr/xo-web/issues/1439)
- Wrong month label shown in Backup and Job scheduler [\#1438](https://github.com/vatesfr/xo-web/issues/1438)
- Bug on Self service creation/edition [\#1428](https://github.com/vatesfr/xo-web/issues/1428)
- ISO selection during VM create is not mounted after [\#1415](https://github.com/vatesfr/xo-web/issues/1415)
- Hosts general view: bad link for storage [\#1408](https://github.com/vatesfr/xo-web/issues/1408)
- Backup Schedule - "Month" and "Day of Week" display error [\#1404](https://github.com/vatesfr/xo-web/issues/1404)
- Migrate dialog doesn't present all available VIF's in new UI interface [\#1403](https://github.com/vatesfr/xo-web/issues/1403)
- NFS mount issues [\#1396](https://github.com/vatesfr/xo-web/issues/1396)
- Select component color [\#1391](https://github.com/vatesfr/xo-web/issues/1391)
- SR created with local path shouldn't be shared [\#1389](https://github.com/vatesfr/xo-web/issues/1389)
- Disks (VBD) are attached to VM in RO mode instead of RW even if RO is unchecked [\#1386](https://github.com/vatesfr/xo-web/issues/1386)
- Re-connection issues between server and XS hosts [\#1384](https://github.com/vatesfr/xo-web/issues/1384)
- Meter object style with Chrome 52 [\#1383](https://github.com/vatesfr/xo-web/issues/1383)
- Editing a rolling snapshot job seems to fail [\#1376](https://github.com/vatesfr/xo-web/issues/1376)
- Dashboard SR usage and total inverted [\#1370](https://github.com/vatesfr/xo-web/issues/1370)
- XenServer connection issue with host while using VGPUs [\#1369](https://github.com/vatesfr/xo-web/issues/1369)
- Job created with v4 are not correctly displayed in v5 [\#1366](https://github.com/vatesfr/xo-web/issues/1366)
- CPU accounting in resource set [\#1365](https://github.com/vatesfr/xo-web/issues/1365)
- Tooltip stay displayed when a button change state [\#1360](https://github.com/vatesfr/xo-web/issues/1360)
- Failure on host reboot [\#1351](https://github.com/vatesfr/xo-web/issues/1351)
- Editing Backup Jobs Without Compression, Slider Always Set To On [\#1339](https://github.com/vatesfr/xo-web/issues/1339)
- Month Selection on Backup Screen Wrong [\#1338](https://github.com/vatesfr/xo-web/issues/1338)
- Delta backup fail when removed VDIs [\#1333](https://github.com/vatesfr/xo-web/issues/1333)
## **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

@@ -10,7 +10,7 @@ ___
## Installation
XOA or manual install procedure is [available here](https://github.com/vatesfr/xo/blob/master/doc/installation/README.md)
XOA or manual install procedure is [available here](https://xen-orchestra.com/docs/installation.html)
## Compilation

View File

@@ -11,17 +11,6 @@ var DIST_DIR = __dirname + '/dist' // eslint-disable-line no-path-concat
// http://www.random.org/integers/?num=1&min=1024&max=65535&col=1&base=10&format=plain&rnd=new
var LIVERELOAD_PORT = 26242
// Port to use for the embedded web server.
//
// Set to 0 to choose a random port at each run.
var SERVER_PORT = LIVERELOAD_PORT + 1
// Address the server should bind to.
//
// - `'localhost'` to make it accessible from this host only
// - `null` to make it accessible for the whole network
var SERVER_ADDR = 'localhost'
var PRODUCTION = process.env.NODE_ENV === 'production'
var DEVELOPMENT = !PRODUCTION
@@ -313,28 +302,3 @@ gulp.task('build', gulp.parallel(
gulp.task(function clean (done) {
require('rimraf')(DIST_DIR, done)
})
// -------------------------------------------------------------------
gulp.task(function server (done) {
require('connect')()
.use(require('serve-static')(DIST_DIR))
.listen(SERVER_PORT, SERVER_ADDR, function onListen () {
var address = this.address()
var port = address.port
address = address.address
// Correctly handle IPv6 addresses.
if (address.indexOf(':') !== -1) {
address = '[' + address + ']'
}
/* jshint devel: true*/
console.log('Listening on http://' + address + ':' + port)
})
.on('error', done)
.on('close', function onClose () {
done()
})
})

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-web",
"version": "5.0.5",
"version": "5.2.5",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -33,11 +33,13 @@
"devDependencies": {
"ansi_up": "^1.3.0",
"asap": "^2.0.4",
"ava": "^0.15.0",
"babel-eslint": "^6.0.0",
"ava": "^0.16.0",
"babel-eslint": "^7.0.0",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-react-constant-elements": "^6.5.0",
"babel-plugin-transform-react-inline-elements": "^6.6.5",
"babel-plugin-transform-react-jsx-self": "^6.11.0",
"babel-plugin-transform-react-jsx-source": "^6.9.0",
"babel-plugin-transform-runtime": "^6.6.0",
"babel-preset-es2015": "^6.6.0",
"babel-preset-react": "^6.5.0",
@@ -48,17 +50,18 @@
"bootstrap": "github:twbs/bootstrap#v4-dev",
"browserify": "^13.0.0",
"bundle-collapser": "^1.2.1",
"chartist-plugin-legend": "^0.3.1",
"chartist-plugin-legend": "^0.5.0",
"chartist-plugin-tooltip": "0.0.11",
"classnames": "^2.2.3",
"connect": "^3.4.1",
"cookies-js": "^1.2.2",
"d3": "^4.0.0-alpha.50",
"dependency-check": "^2.5.1",
"event-to-promise": "^0.7.0",
"font-awesome": "^4.5.0",
"font-mfizz": "github:fizzed/font-mfizz",
"get-stream": "^2.3.0",
"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",
@@ -67,19 +70,23 @@
"gulp-plumber": "^1.1.0",
"gulp-refresh": "^1.1.0",
"gulp-sass": "^2.2.0",
"gulp-uglify": "^1.5.3",
"gulp-uglify": "^2.0.0",
"gulp-watch": "^4.3.5",
"human-format": "^0.6.0",
"index-modules": "0.0.0",
"is-ip": "^1.0.0",
"jsonrpc-websocket-client": "0.0.1-5",
"later": "^1.2.0",
"lodash": "^4.6.1",
"loose-envify": "^1.1.0",
"make-error": "^1.2.1",
"marked": "^0.3.5",
"modular-css": "^0.22.1",
"modular-css": "^0.27.1",
"moment": "^2.13.0",
"moment-timezone": "^0.5.4",
"notifyjs": "^2.0.1",
"novnc-node": "^0.5.3",
"promise-toolbox": "^0.4.0",
"promise-toolbox": "^0.5.0",
"random-password": "^0.1.2",
"react": "^15.0.0",
"react-addons-shallow-compare": "^15.1.0",
@@ -89,16 +96,18 @@
"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",
"react-key-handler": "^0.2.0",
"react-key-handler": "^0.3.0",
"react-notify": "^2.0.1",
"react-redux": "^4.4.0",
"react-router": "^3.0.0-alpha.1",
"react-select": "^1.0.0-beta13",
"react-shortcuts": "^1.0.7",
"react-sparklines": "^1.5.0",
"react-virtualized": "^7.4.0",
"react-virtualized": "^8.0.8",
"readable-stream": "^2.0.6",
"redux": "^3.3.1",
"redux-devtools": "^3.1.1",
@@ -106,20 +115,21 @@
"redux-devtools-log-monitor": "^1.0.5",
"redux-thunk": "^2.0.1",
"reselect": "^2.2.1",
"serve-static": "^1.10.2",
"standard": "^7.0.0",
"standard": "^8.2.0",
"superagent": "^2.0.0",
"vinyl": "^1.1.1",
"tar-stream": "^1.5.2",
"vinyl": "^2.0.0",
"watchify": "^3.7.0",
"xo-acl-resolver": "^0.2.1",
"xo-lib": "^0.8.0-1",
"xml2js": "^0.4.17",
"xo-acl-resolver": "^0.2.2",
"xo-lib": "^0.8.0",
"xo-remote-parser": "^0.3"
},
"scripts": {
"benchmarks": "./tools/run-benchmarks.js 'src/**/*.bench.js'",
"build": "npm run build-indexes && NODE_ENV=production gulp build",
"build-indexes": "./tools/generate-index src/common/intl/locales",
"dev": "npm run build-indexes && gulp build server",
"build-indexes": "index-modules --auto src",
"dev": "npm run build-indexes && NODE_ENV=development gulp build",
"dev-test": "ava --watch",
"lint": "standard",
"posttest": "npm run lint",
@@ -143,6 +153,12 @@
},
"babel": {
"env": {
"development": {
"plugins": [
"transform-react-jsx-self",
"transform-react-jsx-source"
]
},
"production": {
"plugins": [
"transform-react-constant-elements",

View File

@@ -4,16 +4,16 @@
$ct-series-colors: (
$brand-success,
$brand-primary,
#60bd68,
#f17cb0,
#b2912f,
#b276b2,
#decf3f,
#f15854,
#4d4d4d,
#dda458,
#eacf7d,
#86797d,
#b276b2,
#f15854,
#b2912f,
#decf3f,
#dda458,
#60bd68,
#4d4d4d,
#eacf7d,
#b2c326,
#6188e2,
#a748ca
@@ -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,26 +1,35 @@
// 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'
import {
noop
} from 'utils'
const ActionBar = ({ actions, param }) => (
<ButtonGroup>
{map(actions, ({ handler, handlerParam = param, label, icon }, index) => (
/* <Tooltip key={index} content={_(label)}> TODO: fix tooltip */
{map(actions, (button, index) => {
if (!button) {
return
}
const { handler, handlerParam = param, label, icon, redirectOnSuccess } = button
return <Tooltip key={index} content={_(label)}>
<ActionButton
key={index}
btnStyle='secondary'
handler={handler}
handler={handler || noop}
handlerParam={handlerParam}
icon={icon}
redirectOnSuccess={redirectOnSuccess}
size='large'
/>
/* </Tooltip> */
))}
</Tooltip>
})}
</ButtonGroup>
)
ActionBar.propTypes = {
@@ -28,7 +37,8 @@ ActionBar.propTypes = {
React.PropTypes.shape({
label: React.PropTypes.string.isRequired,
icon: React.PropTypes.string.isRequired,
handler: React.PropTypes.func
handler: React.PropTypes.func,
redirectOnSuccess: React.PropTypes.string
})
).isRequired,
display: React.PropTypes.oneOf(['icon', 'text', 'both'])

View File

@@ -6,6 +6,7 @@ import { Button } from 'react-bootstrap-4/lib'
import Component from './base-component'
import logError from './log-error'
import propTypes from './prop-types'
import Tooltip from './tooltip'
@propTypes({
btnStyle: propTypes.string,
@@ -21,7 +22,8 @@ import propTypes from './prop-types'
size: propTypes.oneOf([
'large',
'small'
])
]),
tooltip: propTypes.node
})
export default class ActionButton extends Component {
static contextTypes = {
@@ -62,7 +64,11 @@ export default class ActionButton extends Component {
error,
working: false
})
logError(error)
// ignore when undefined because it usually means that the action has been canceled
if (error !== undefined) {
logError(error)
}
}
}
_execute = ::this._execute
@@ -98,12 +104,13 @@ export default class ActionButton extends Component {
form,
icon,
size: bsSize,
style
style,
tooltip
},
state: { error, working }
} = this
return <Button
const button = <Button
bsStyle={error ? 'warning' : btnStyle}
form={form}
onClick={!form && this._execute}
@@ -115,5 +122,9 @@ export default class ActionButton extends Component {
{children && ' '}
{children}
</Button>
return tooltip
? <Tooltip content={tooltip}>{button}</Tooltip>
: button
}
}

View File

@@ -1,10 +1,36 @@
import clone from 'lodash/clone'
import includes from 'lodash/includes'
import isArray from 'lodash/isArray'
import forEach from 'lodash/forEach'
import map from 'lodash/map'
import { Component } from 'react'
import getEventValue from './get-event-value'
import invoke from './invoke'
import shallowEqual from './shallow-equal'
const cowSet = (object, path, value, depth) => {
if (depth >= path.length) {
return value
}
object = clone(object)
const prop = path[depth]
object[prop] = cowSet(object[prop], path, value, depth + 1)
return object
}
const get = (object, path, depth) => {
if (depth >= path.length) {
return object
}
const prop = path[depth++]
return isArray(object) && prop === '*'
? map(object, value => get(value, path, depth))
: get(object[prop], path, depth)
}
export default class BaseComponent extends Component {
constructor (props, context) {
super(props, context)
@@ -24,7 +50,42 @@ export default class BaseComponent extends Component {
}
// See https://preactjs.com/guide/linked-state
linkState (name) {
linkState (name, targetPath) {
const key = targetPath
? `${name}##${targetPath}`
: name
let linkedState = this._linkedState
let cb
if (!linkedState) {
linkedState = this._linkedState = {}
} else if ((cb = linkedState[key])) {
return cb
}
let getValue
if (targetPath) {
const path = targetPath.split('.')
getValue = event => get(getEventValue(event), path, 0)
} else {
getValue = getEventValue
}
if (includes(name, '.')) {
const path = name.split('.')
return (linkedState[key] = event => {
this.setState(cowSet(this.state, path, getValue(event), 0))
})
}
return (linkedState[key] = event => {
this.setState({
[name]: getValue(event)
})
})
}
toggleState (name) {
let linkedState = this._linkedState
let cb
if (!linkedState) {
@@ -33,9 +94,16 @@ export default class BaseComponent extends Component {
return cb
}
return (linkedState[name] = event => {
if (includes(name, '.')) {
const path = name.split('.')
return (linkedState[path] = event => {
this.setState(cowSet(this.state, path, !get(this.state, path, 0), 0))
})
}
return (linkedState[name] = () => {
this.setState({
[name]: getEventValue(event)
[name]: !this.state[name]
})
})
}

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

@@ -1,5 +1,7 @@
import _ from 'intl'
import CopyToClipboard from 'react-copy-to-clipboard'
import classNames from 'classnames'
import Tooltip from 'tooltip'
import React, { createElement } from 'react'
import Icon from '../icon'
@@ -10,18 +12,20 @@ import styles from './index.css'
const Copiable = propTypes({
data: propTypes.string,
tagName: propTypes.string
})(props => createElement(
props.tagName || 'span',
})(({ className, tagName = 'span', ...props }) => createElement(
tagName,
{
...props,
className: classNames(styles.container, props.className)
className: classNames(styles.container, className)
},
props.children,
' ',
<CopyToClipboard text={props.data || props.children}>
<button className={classNames('btn btn-sm btn-secondary', styles.button)}>
<Icon icon='clipboard' />
</button>
</CopyToClipboard>
<Tooltip content={_('copyToClipboard')}>
<CopyToClipboard text={props.data || props.children}>
<button className={classNames('btn btn-sm btn-secondary', styles.button)}>
<Icon icon='clipboard' />
</button>
</CopyToClipboard>
</Tooltip>
))
export { Copiable as default }

View File

@@ -35,8 +35,8 @@ class DebugAsync extends Component {
return <pre>
{'Promise { '}
{status === 'rejected' && '<rejected> '}
{toString(value)}
{status === 'rejected' && '<rejected> '}
{toString(value)}
{' }'}
</pre>
}

View File

@@ -0,0 +1,13 @@
.clickToEdit * {
cursor: context-menu !important;
}
.shortClick {
border-bottom: 1px dashed #ccc;
}
.select {
padding: 0px;
}
.size {
width: 10rem;
}

View File

@@ -1,3 +1,4 @@
import classNames from 'classnames'
import findKey from 'lodash/findKey'
import isFunction from 'lodash/isFunction'
import isString from 'lodash/isString'
@@ -5,16 +6,17 @@ import map from 'lodash/map'
import pick from 'lodash/pick'
import React from 'react'
import _ from './intl'
import Component from './base-component'
import Icon from './icon'
import logError from './log-error'
import propTypes from './prop-types'
import Tooltip from './tooltip'
import { formatSize } from './utils'
import { SizeInput } from './form'
import _ from '../intl'
import Component from '../base-component'
import Icon from '../icon'
import logError from '../log-error'
import propTypes from '../prop-types'
import Tooltip from '../tooltip'
import { formatSize } from '../utils'
import { SizeInput } from '../form'
import {
SelectHost,
SelectIp,
SelectNetwork,
SelectPool,
SelectRemote,
@@ -23,12 +25,11 @@ import {
SelectTag,
SelectVm,
SelectVmTemplate
} from './select-objects'
} from '../select-objects'
import styles from './index.css'
const LONG_CLICK = 400
const SELECT_STYLE = { padding: '0px' }
const SIZE_STYLE = { width: '10rem' }
const EDITABLE_STYLE = { borderBottom: '1px dashed #ccc' }
@propTypes({
alt: propTypes.node.isRequired
@@ -157,7 +158,7 @@ class Editable extends Component {
const { useLongClick } = props
const success = <Icon icon='success' />
return <span style={useLongClick ? null : EDITABLE_STYLE}>
return <span className={classNames(styles.clickToEdit, !useLongClick && styles.shortClick)}>
<span
onClick={!useLongClick && this._openEdition}
onMouseDown={useLongClick && this.__startTimer}
@@ -257,7 +258,8 @@ export class Text extends Editable {
readOnly={saving}
ref='input'
style={{
width: `${value.length + 1}ex`
width: `${value.length + 1}ex`,
maxWidth: '50ex'
}}
type={this._isPassword ? 'password' : 'text'}
/>
@@ -271,20 +273,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)}
/>
}
}
@@ -337,14 +353,13 @@ export class Select extends Editable {
return <select
autoFocus
className='form-control'
className={classNames('form-control', styles.select)}
defaultValue={this._defaultValue}
onBlur={this._closeEdition}
onChange={this._onChange}
onKeyDown={this._onKeyDown}
readOnly={saving}
ref={this._onEditionMount}
style={SELECT_STYLE}
>
{map(options, this._optionToJsx)}
</select>
@@ -353,6 +368,7 @@ export class Select extends Editable {
const MAP_TYPE_SELECT = {
host: SelectHost,
ip: SelectIp,
network: SelectNetwork,
pool: SelectPool,
remote: SelectRemote,
@@ -441,15 +457,18 @@ export class Size extends Editable {
const { value } = this.props
return <span
// SizeInput uses `input-group` which makes it behave as a block element (display: table).
// `form-inline` to use it as an inline element
className='form-inline'
onBlur={this._closeEditionIfUnfocused}
onFocus={this._focus}
onKeyDown={this._onKeyDown}
>
<SizeInput
autoFocus
className={styles.size}
ref='input'
readOnly={saving}
style={SIZE_STYLE}
defaultValue={value}
/>
</span>

View File

@@ -1,8 +1,10 @@
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
@@ -11,6 +13,7 @@ import {
import Component from '../base-component'
import propTypes from '../prop-types'
import {
firstDefined,
formatSizeRaw,
parseSize
} from '../utils'
@@ -96,12 +99,9 @@ export class Range extends Component {
}
set value (value) {
const { onChange } = this.props
this.state.value = +value
if (onChange) {
onChange(value)
}
this.setState({
value: +value
})
}
_handleChange = event => {
@@ -158,72 +158,156 @@ const DEFAULT_UNIT = 'GiB'
placeholder: propTypes.string,
readOnly: propTypes.bool,
required: propTypes.bool,
style: propTypes.object
style: propTypes.object,
value: propTypes.oneOfType([
propTypes.number,
propTypes.oneOf([ null ])
])
})
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, null))
}
componentWillReceiveProps (newProps) {
this.value = newProps.defaultValue
componentWillReceiveProps (props) {
const { value } = props
if (value !== undefined && value !== this.props.value) {
this.setState(this._createStateFromBytes(value))
}
}
_createStateFromBytes (bytes) {
if (bytes === this._bytes) {
return {
input: this._input,
unit: this._unit
}
}
if (bytes === null) {
return {
input: '',
unit: this.props.defaultUnit || DEFAULT_UNIT
}
}
const { prefix, value } = formatSizeRaw(bytes)
return {
input: String(round(value, 2)),
unit: `${prefix}B`
}
}
get value () {
const value = this.refs.value.value
return value ? parseSize(value + ' ' + this.state.unit) : undefined
const { input, unit } = this.state
if (!input) {
return null
}
return parseSize(`${+input} ${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 })
set value (value) {
if (
process.env.NODE_ENV !== 'production' &&
this.props.value !== undefined
) {
throw new Error('cannot set value of controlled SizeInput')
}
this.setState(this._createStateFromBytes(value))
}
_onChange = () =>
this.props.onChange && this.props.onChange(this.value)
_onChange (input, unit) {
const { onChange } = this.props
// Empty input equals null.
const bytes = input
? parseSize(`${+input} ${unit}`)
: null
const isControlled = this.props.value !== undefined
if (isControlled) {
// Store input and unit for this change to update correctly on new
// props.
this._bytes = bytes
this._input = input
this._unit = unit
} else {
this.setState({ input, unit })
// onChange is optional in uncontrolled mode.
if (!onChange) {
return
}
}
onChange(bytes)
}
_updateNumber = event => {
const input = event.target.value
if (!input) {
return this._onChange(input, this.state.unit)
}
const number = +input
// NaN: do not ack this change.
if (number !== number) { // eslint-disable-line no-self-compare
return
}
// Same numeric value: simply update the input.
const prevInput = this.state.input
if (prevInput && +prevInput === number) {
return this.setState({ input })
}
this._onChange(input, this.state.unit)
}
_updateUnit = unit => {
this.setState({ unit })
this._onChange()
const { input } = this.state
// 0 is always 0, no matter the unit.
if (+input) {
this._onChange(input, unit)
} else {
this.setState({ unit })
}
}
render () {
const {
autoFocus,
className,
placeholder,
readOnly,
placeholder,
required,
style
} = this.props
return <span
className={classNames(className, 'input-group')}
style={style}
>
return <span className={classNames('input-group', className)} style={style}>
<input
autoFocus={autoFocus}
className='form-control'
defaultValue={this._defaultValue}
min={0}
onChange={this._onChange}
disabled={readOnly}
onChange={this._updateNumber}
placeholder={placeholder}
readOnly={readOnly}
required={required}
ref='value'
type='number'
type='text'
value={this.state.input}
/>
<span className='input-group-btn'>
<DropdownButton
bsStyle='secondary'
disabled={readOnly}
id='size'
pullRight
disabled={readOnly}
title={this.state.unit}
>
{map(UNITS, unit =>

View File

@@ -2,7 +2,7 @@ import React, { Component } from 'react'
import ReactSelect from 'react-select'
import {
AutoSizer,
VirtualScroll
List
} from 'react-virtualized'
import propTypes from '../prop-types'
@@ -40,19 +40,21 @@ export default class Select extends Component {
const focusedOptionIndex = options.indexOf(focusedOption)
const height = Math.min(maxHeight, options.length * optionHeight)
const wrappedRowRenderer = ({ index }) =>
const wrappedRowRenderer = ({ index, key, style }) =>
this._optionRenderer({
...otherOptions,
focusedOption,
focusedOptionIndex,
key,
option: options[index],
options
options,
style
})
return (
<AutoSizer disableHeight>
{({ width }) => (
<VirtualScroll
<List
height={height}
rowCount={options.length}
rowHeight={optionHeight}
@@ -68,8 +70,10 @@ export default class Select extends Component {
_optionRenderer = ({
focusedOption,
focusOption,
key,
labelKey,
option,
style,
selectValue
}) => {
let className = 'Select-option'
@@ -91,7 +95,8 @@ export default class Select extends Component {
className={className}
onClick={!disabled && (() => selectValue(option))}
onMouseOver={!disabled && (() => focusOption(option))}
style={{ height: props.optionHeight }}
style={{ ...style, height: props.optionHeight }}
key={key}
>
{props.optionRenderer(option, labelKey)}
</div>
@@ -102,6 +107,7 @@ export default class Select extends Component {
return (
<ReactSelect
{...this.props}
backspaceToRemoveMessage=''
menuRenderer={this._renderMenu}
menuStyle={SELECT_MENU_STYLE}
style={SELECT_STYLE}

View File

@@ -49,6 +49,7 @@ export default class Toggle extends Component {
}
this.refs.input.checked = Boolean(value)
this.forceUpdate()
}
_onChange = event => {

View File

@@ -0,0 +1,20 @@
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:'
}
export const vmTemplate = {
homeFilterTags: 'tags:'
}

View File

@@ -0,0 +1,226 @@
import isEmpty from 'lodash/isEmpty'
import keys from 'lodash/keys'
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,
installAllPatchesOnPool
} 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 = () => {
const pools = {}
forEach(this._getHosts(), host => {
pools[host.$pool] = true
})
return Promise.all(map(
keys(pools),
installAllPatchesOnPool
)).then(this._refreshMissingPatches)
}
_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} />
)

View File

@@ -66,7 +66,7 @@ export class IntlProvider extends Component {
locale={lang}
messages={locales[lang]}
>
{children}
{children}
</IntlProvider_>
}
}

View File

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -218,9 +218,6 @@ export default {
// Original text: "SR"
homeTypeSr: 'SR',
// Original text: "VDI"
homeTypeVdi: 'VDI',
// Original text: "Sort"
homeSort: 'סינון',
@@ -272,11 +269,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: 'עוד',
@@ -939,7 +936,7 @@ export default {
srType: undefined,
// Original text: "Status"
pdbStatus: undefined,
pbdStatus: undefined,
// Original text: "Connected"
pbdStatusConnected: undefined,
@@ -1676,6 +1673,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,

View File

@@ -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',
@@ -215,9 +221,6 @@ export default {
// Original text: "SR"
homeTypeSr: 'SR',
// Original text: "VDI"
homeTypeVdi: 'VDI',
// Original text: "Sort"
homeSort: 'Classificar',
@@ -240,7 +243,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 +273,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 +408,7 @@ export default {
unknownSchedule: 'Desconhecido',
// Original text: "Job"
job: 'tarefa',
job: 'Tarefa',
// Original text: "Job ID"
jobId: 'ID tarefa',
@@ -414,10 +417,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 +510,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)',
@@ -936,7 +939,7 @@ export default {
srType: 'Tipo',
// Original text: "Status"
pdbStatus: 'Status',
pbdStatus: 'Status',
// Original text: "Connected"
pbdStatusConnected: 'Conectado',
@@ -1308,7 +1311,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 +1395,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 +1539,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 +1572,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 +1599,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 +1608,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 +1623,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 +1676,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}}',
@@ -1712,6 +1739,9 @@ export default {
// Original text: "Migrate VM"
migrateVmModalTitle: 'Migrar VM',
// Original text: "Are you sure you want to migrate this VM to {hostName}?"
migrateVmModalBody: 'Você tem certeza que deseja migrar esta VM para {hostName}?',
// Original text: "Select a destination host:"
migrateVmAdvancedModalSelectHost: 'Selecionar um host de destino:',
@@ -1746,7 +1776,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

@@ -5,6 +5,11 @@ var forEach = require('lodash/forEach')
var isString = require('lodash/isString')
var messages = {
statusConnecting: 'Connecting',
statusDisconnected: 'Disconnected',
statusLoading: 'Loading…',
errorPageNotFound: 'Page not found',
editableLongClickPlaceholder: 'Long click to edit',
editableClickPlaceholder: 'Click to edit',
@@ -13,16 +18,28 @@ var messages = {
confirmOk: 'OK',
confirmCancel: 'Cancel',
// ----- Filters -----
onError: 'On error',
successful: 'Successful',
// ----- Copiable component -----
copyToClipboard: 'Copy to clipboard',
// ----- Pills -----
pillMaster: 'Master',
// ----- Titles -----
homePage: 'Home',
homeVmPage: 'VMs',
homeHostPage: 'Hosts',
homePoolPage: 'Pools',
homeTemplatePage: 'Templates',
dashboardPage: 'Dashboard',
overviewDashboardPage: 'Overview',
overviewVisualizationDashboardPage: 'Visualizations',
overviewStatsDashboardPage: 'Statistics',
overviewHealthDashboardPage: 'Health',
selfServicePage: 'Self service',
selfServiceDashboardPage: 'Dashboard',
selfServiceAdminPage: 'Administration',
backupPage: 'Backup',
jobsPage: 'Jobs',
updatePage: 'Updates',
@@ -32,9 +49,12 @@ var messages = {
settingsGroupsPage: 'Groups',
settingsAclsPage: 'ACLs',
settingsPluginsPage: 'Plugins',
settingsLogsPage: 'Logs',
settingsIpsPage: 'IPs',
aboutPage: 'About',
newMenu: 'New',
taskMenu: 'Tasks',
taskPage: 'Tasks',
newVmPage: 'VM',
newSrPage: 'Storage',
newServerPage: 'Server',
@@ -57,9 +77,16 @@ var messages = {
customJob: 'Custom Job',
userPage: 'User',
// ----- Support -----
noSupport: 'No support',
freeUpgrade: 'Free upgrade!',
// ----- Sign out -----
signOut: 'Sign out',
// ----- User Profile -----
editUserProfile: 'Edit my settings {username}',
// ----- Home view ------
homeFetchingData: 'Fetching data…',
homeWelcome: 'Welcome on Xen Orchestra!',
@@ -76,11 +103,12 @@ var messages = {
homeRestoreBackupMessage: 'Restore a backup from a remote store',
homeNewVmMessage: 'This will create a new VM',
homeFilters: 'Filters',
homeNoMatches: 'No results! Click here to reset your filters',
homeTypePool: 'Pool',
homeTypeHost: 'Host',
homeTypeVm: 'VM',
homeTypeSr: 'SR',
homeTypeVdi: 'VDI',
homeTypeVmTemplate: 'Template',
homeSort: 'Sort',
homeAllPools: 'Pools',
homeAllHosts: 'Hosts',
@@ -98,11 +126,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',
homePoolMaster: 'Master:',
highAvailability: 'High Availability',
// ----- Forms -----
@@ -120,11 +150,19 @@ 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)…',
selectSshKey: 'Select SSH key(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…',
selectIp: 'Select IP(s)...',
fillRequiredInformations: 'Fill required informations.',
fillOptionalInformations: 'Fill informations (optional)',
selectTableReset: 'Reset',
@@ -132,29 +170,30 @@ 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',
jobId: 'Job ID',
jobName: 'Name',
jobNamePlaceholder: 'Name of your job (forbidden: "_")',
jobStart: 'Start',
jobEnd: 'End',
jobDuration: 'Duration',
@@ -163,6 +202,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',
@@ -177,9 +218,17 @@ var messages = {
noJobs: 'No jobs found.',
noSchedules: 'No schedules found',
jobActionPlaceHolder: 'Select a xo-server API command',
jobSchedules: 'Schedules',
jobScheduleNamePlaceHolder: 'Name of your schedule',
jobScheduleJobPlaceHolder: 'Select a Job',
// ------ New backup -----
newBackupSelection: 'Select your backup type:',
smartBackupModeSelection: 'Select backup mode:',
normalBackup: 'Normal backup',
smartBackup: 'Smart backup',
localRemoteWarningTitle: 'Local remote selected',
localRemoteWarningMessage: 'Warning: local remotes will use limited XOA disk space. Only for advanced users.',
// ------ New Remote -----
remoteList: 'Remote stores for backup',
@@ -197,9 +246,34 @@ var messages = {
remoteTestFile: 'Test file',
remoteTestSuccessMessage: 'The remote appears to work correctly',
// ------ Remote -----
remoteName: 'Name',
remotePath: 'Path',
remoteState: 'State',
remoteDevice: 'Device',
remoteShare: 'Share',
remoteAuth: 'Auth',
remoteMounted: 'Mounted',
remoteUnmounted: 'Unmounted',
remoteConnectTip: 'Connect',
remoteDisconnectTip: 'Disconnect',
remoteDeleteTip: 'Delete',
remoteNamePlaceHolder: 'remote name *',
remoteMyNamePlaceHolder: 'Name *',
remoteLocalPlaceHolderPath: '/path/to/backup',
remoteNfsPlaceHolderHost: 'host *',
remoteNfsPlaceHolderPath: '/path/to/backup',
remoteSmbPlaceHolderRemotePath: 'subfolder [path\\to\\backup]',
remoteSmbPlaceHolderUsername: 'Username',
remoteSmbPlaceHolderPassword: 'Password',
remoteSmbPlaceHolderDomain: 'Domain',
remoteSmbPlaceHolderAddressShare: '<address>\\<share> *',
remotePlaceHolderPassword: 'password(fill to edit)',
// ------ New Storage -----
newSrTitle: 'Create a new SR',
newSrGeneral: 'General',
newSrTypeSelection: 'Select Strorage Type:',
newSrTypeSelection: 'Select Storage Type:',
newSrSettings: 'Settings',
newSrUsage: 'Storage Usage',
newSrSummary: 'Summary',
@@ -222,7 +296,9 @@ var messages = {
// ----- Acls, Users, Groups ------
subjectName: 'Users/Groups',
objectName: 'Object',
aclNoneFound: 'No acls found',
roleName: 'Role',
aclCreate: 'Create',
newGroupName: 'New Group Name',
createGroup: 'Create Group',
createGroupButton: 'Create',
@@ -231,6 +307,7 @@ var messages = {
removeUserFromGroup: 'Remove user from Group',
deleteUserConfirm: 'Are you sure you want to delete this user?',
deleteUser: 'Delete User',
noUser: 'no user',
unknownUser: 'unknown user',
noGroupFound: 'No group found',
groupNameColumn: 'Name',
@@ -261,6 +338,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',
@@ -289,6 +385,10 @@ var messages = {
srForget: 'Forget this SR',
srRemoveButton: 'Remove this SR',
srNoVdis: 'No VDIs in this storage',
// ----- Pool general -----
poolTitleRamUsage: 'Pool RAM usage:',
poolRamUsage: '{used} used on {total}',
poolMaster: 'Master:',
// ----- Pool tabs -----
hostsTabName: 'Hosts',
// ----- Pool advanced tab -----
@@ -300,6 +400,7 @@ var messages = {
hostDescription: 'Description',
hostMemory: 'Memory',
noHost: 'No hosts',
memoryLeftTooltip: '{used}% used ({free} free)',
// ----- Pool network tab -----
poolNetworkNameLabel: 'Name',
poolNetworkDescription: 'Description',
@@ -308,6 +409,8 @@ var messages = {
poolNetworkMTU: 'MTU',
poolNetworkPifAttached: 'Connected',
poolNetworkPifDetached: 'Disconnected',
showPifs: 'Show PIFs',
hidePifs: 'Hide PIFs',
// ----- Pool actions ------
addSrLabel: 'Add SR',
addVmLabel: 'Add VM',
@@ -322,6 +425,7 @@ var messages = {
restartHostAgent: 'Restart toolstack',
forceRebootHostLabel: 'Force reboot',
rebootHostLabel: 'Reboot',
rebootUpdateHostLabel: 'Reboot for applying updates',
emergencyModeLabel: 'Emergency mode',
// ----- Host tabs -----
storageTabName: 'Storage',
@@ -350,6 +454,7 @@ var messages = {
hostLicenseExpiry: 'Expiry',
// ----- Host net tabs -----
networkCreateButton: 'Add a network',
networkCreateBondedButton: 'Add a bonded network',
pifDeviceLabel: 'Device',
pifNetworkLabel: 'Network',
pifVlanLabel: 'VLAN',
@@ -360,13 +465,18 @@ var messages = {
pifStatusConnected: 'Connected',
pifStatusDisconnected: 'Disconnected',
pifNoInterface: 'No physical interface detected',
pifInUse: 'This interface is currently in use',
defaultLockingMode: 'Default locking mode',
// ----- Host storage tabs -----
addSrDeviceButton: 'Add a storage',
srNameLabel: 'Name',
srType: 'Type',
pdbStatus: 'Status',
pbdStatus: 'Status',
pbdStatusConnected: 'Connected',
pbdStatusDisconnected: 'Disconnected',
pbdConnect: 'Connect',
pbdDisconnect: 'Disconnect',
pbdForget: 'Forget',
srShared: 'Shared',
srNotShared: 'Not shared',
pbdNoSr: 'No storage detected',
@@ -374,7 +484,7 @@ var messages = {
patchNameLabel: 'Name',
patchUpdateButton: 'Install all patches',
patchDescription: 'Description',
patchApplied: 'Release date',
patchApplied: 'Applied date',
patchSize: 'Size',
patchStatus: 'Status',
patchStatusApplied: 'Applied',
@@ -383,17 +493,21 @@ 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',
// ----- Pool storage tabs -----
defaultSr: 'Default SR',
setAsDefaultSr: 'Set as default SR',
// ----- VM tabs -----
generalTabName: 'General',
statsTabName: 'Stats',
consoleTabName: 'Console',
containersTabName: 'Container',
snapshotsTabName: 'Snapshots',
logsTabName: 'Logs',
advancedTabName: 'Advanced',
@@ -424,7 +538,7 @@ var messages = {
statsCpu: 'CPU usage',
statsMemory: 'Memory usage',
statsNetwork: 'Network throughput',
useCombinedValuesOnStats: 'Use combined values:',
useStackedValuesOnStats: 'Stacked values',
statDisk: 'Disk throughput',
statLastTenMinutes: 'Last 10 minutes',
statLastTwoHours: 'Last 2 hours',
@@ -436,6 +550,21 @@ var messages = {
ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
tipLabel: 'Tip:',
tipConsoleLabel: 'non-US keyboard could have issues with console: switch your own layout to US.',
hideHeaderTooltip: 'Hide infos',
showHeaderTooltip: 'Show infos',
// ----- VM container tab -----
containerName: 'Name',
containerCommand: 'Command',
containerCreated: 'Creation date',
containerStatus: 'Status',
containerAction: 'Action',
noContainers: 'No existing containers',
containerStop: 'Stop this container',
containerStart: 'Start this container',
containerPause: 'Pause this container',
containerResume: 'Resume this container',
containerRestart: 'Restart this container',
// ----- VM disk tab -----
vdiAction: 'Action',
@@ -447,11 +576,21 @@ var messages = {
vdiTags: 'Tags',
vdiSize: 'Size',
vdiSr: 'SR',
vdiVm: 'VM',
vdiMigrate: 'Migrate VDI',
vdiMigrateSelectSr: 'Destination SR:',
vdiMigrateAll: 'Migrate all VDIs',
vdiMigrateNoSr: 'No SR',
vdiMigrateNoSrMessage: 'A target SR is required to migrate a VDI',
vdiForget: 'Forget',
vdiRemove: 'Remove VDI',
vdbBootableStatus: 'Boot flag',
vdbStatus: 'Status',
vbdStatusConnected: 'Connected',
vbdStatusDisconnected: 'Disconnected',
vbdNoVbd: 'No disks',
vbdConnect: 'Connect VBD',
vbdDisconnect: 'Disconnect VBD',
// ----- VM network tab -----
vifCreateDeviceButton: 'New device',
@@ -463,13 +602,26 @@ var messages = {
vifStatusLabel: 'Status',
vifStatusConnected: 'Connected',
vifStatusDisconnected: 'Disconnected',
vifConnect: 'Connect',
vifDisconnect: 'Disconnect',
vifRemove: 'Remove',
vifIpAddresses: 'IP addresses',
vifMacAutoGenerate: 'Auto-generated if empty',
vifAllowedIps: 'Allowed IPs',
vifNoIps: 'No IPs',
vifLockedNetwork: 'Network locked',
vifLockedNetworkNoIps: 'Network locked and no IPs are allowed for this interface',
vifUnLockedNetwork: 'Network not locked',
vifUnknownNetwork: 'Unknown network',
// ----- VM snapshot tab -----
noSnapshots: 'No snapshots',
snapshotCreateButton: 'New snapshot',
tipCreateSnapshotLabel: 'Just click on the snapshot button to create one!',
revertSnapshot: 'Revert VM to this snapshot',
deleteSnapshot: 'Remove this snapshot',
copySnapshot: 'Create a VM from this snapshot',
exportSnapshot: 'Export this snapshot',
snapshotDate: 'Creation date',
snapshotName: 'Name',
snapshotAction: 'Action',
@@ -491,7 +643,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: {
@@ -519,6 +673,14 @@ var messages = {
vmViewNamePlaceholder: 'Click to add a name',
vmViewDescriptionPlaceholder: 'Click to add a description',
// ----- Templates -----
templateHomeNamePlaceholder: 'Click to add a name',
templateHomeDescriptionPlaceholder: 'Click to add a description',
templateDelete: 'Delete template',
templateDeleteModalTitle: 'Delete VM template{templates, plural, one {} other {s}}',
templateDeleteModalBody: 'Are you sure you want to delete {templates, plural, one {this} other {these}} template{templates, plural, one {} other {s}}?',
// ----- Dashboard -----
poolPanel: 'Pool{pools, plural, one {} other {s}}',
hostPanel: 'Host{hosts, plural, one {} other {s}}',
@@ -529,7 +691,7 @@ var messages = {
taskStatePanel: 'Pending tasks',
usersStatePanel: 'Users',
srStatePanel: 'Storage state',
ofUsage: 'of',
ofUsage: '{usage} (of {total})',
noSrs: 'No storage',
srName: 'Name',
srPool: 'Pool',
@@ -541,6 +703,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}',
@@ -572,9 +735,12 @@ var messages = {
alarmObject: 'Issue on',
alarmPool: 'Pool',
alarmRemoveAll: 'Remove all alarms',
spaceLeftTooltip: '{used}% used ({free} left)',
// ----- 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',
@@ -582,6 +748,9 @@ var messages = {
newVmPerfPanel: 'Performances',
newVmVcpusLabel: 'vCPUs',
newVmRamLabel: 'RAM',
newVmStaticMaxLabel: 'Static memory max',
newVmDynamicMinLabel: 'Dynamic memory min',
newVmDynamicMaxLabel: 'Dynamic memory max',
newVmInstallSettingsPanel: 'Install settings',
newVmIsoDvdLabel: 'ISO/DVD',
newVmNetworkLabel: 'Network',
@@ -605,20 +774,29 @@ 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:',
newVmNumberRecalculate: 'Recalculate VMs number',
newVmNameRefresh: 'Refresh VMs name',
newVmAdvancedPanel: 'Advanced',
newVmShowAdvanced: 'Show advanced settings',
newVmHideAdvanced: 'Hide advanced settings',
// ----- Self -----
resourceSets: 'Resource sets',
noResourceSets: 'No resource sets.',
loadingResourceSets: 'Loading resource sets...',
resourceSetName: 'Resource set name',
resourceSetCreation: 'Creation and edition',
recomputeResourceSets: 'Recompute all limits',
saveResourceSet: 'Save',
resetResourceSet: 'Reset',
editResourceSet: 'Edit',
@@ -641,9 +819,10 @@ var messages = {
totalResource: 'Total:',
remainingResource: 'Remaining:',
usedResource: 'Used:',
resourceSetNew: 'New',
// ---- VM import ---
importVmsList: 'Try dropping some backups here, or click to select backups to upload. Accept only .xva files.',
importVmsList: 'Try dropping some VMs files here, or click to select VMs to upload. Accept only .xva/.ova files.',
noSelectedVms: 'No selected VMs.',
vmImportToPool: 'To Pool:',
vmImportToSr: 'To SR:',
@@ -653,48 +832,82 @@ var messages = {
vmImportFailed: 'VM import failed',
startVmImport: 'Import starting…',
startVmExport: 'Export starting…',
nCpus: 'N CPUs',
vmMemory: 'Memory',
diskInfo: 'Disk {position} ({capacity})',
diskDescription: 'Disk description',
noDisks: 'No disks.',
noNetworks: 'No networks.',
networkInfo: 'Network {name}',
noVmImportErrorDescription: 'No description available',
vmImportError: 'Error:',
vmImportFileType: '{type} file:',
vmImportConfigAlert: 'Please to check and/or modify the VM configuration.',
// ---- Tasks ---
noTasks: 'No pending tasks',
xsTasks: 'Currently, there are not any pending XenServer tasks',
// ---- Backup views ---
backupSchedules: 'Schedules',
getRemote: 'Get remote',
listRemote: 'List Remote',
simpleBackup: 'simple',
delta: 'delta',
restoreBackups: 'Restore Backups',
noRemotes: 'No remotes',
remoteEnabled: 'enabled',
remoteError: 'error',
remoteEnabled: 'Enabled',
remoteError: 'Error',
noBackup: 'No backup available',
backupVmNameColumn: 'VM Name',
backupTagColumn: 'Backup Tag',
lastBackupColumn: 'Last Backup',
availableBackupsColumn: 'Available Backups',
restoreColumn: 'Restore',
restoreTip: 'Restore VM',
restoreTip: 'View restore options',
displayBackup: 'Display backups',
importBackupTitle: 'Import VM',
importBackupMessage: 'Starting your backup import',
vmsToBackup: 'VMs to backup',
// ----- 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? If it\'s the pool master, your connection to the pool will be lost',
addHostModalTitle: 'Add host',
addHostModalMessage: 'Are you sure you want to add {host} to {pool}?',
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}}',
snapshotVmsModalMessage: 'Are you sure you want to snapshot {vms} VM{vms, plural, one {} other {s}}?',
deleteVmModalTitle: 'Delete VM',
deleteVmsModalTitle: 'Delete VM{vms, plural, one {} other {s}}',
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',
deleteVmModalTitle: 'Delete VM',
deleteVmModalMessage: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
migrateVmModalTitle: 'Migrate VM',
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',
@@ -703,8 +916,13 @@ var messages = {
migrateVmNetwork: 'Network',
migrateVmNoTargetHost: 'No target host',
migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',
migrateVmBadPowerState: 'Bad power state: only running VMs can be migrated.',
migrateVmSomeBadPowerState: 'Some of the selected VMs are not running (e.g.: {vm}) and therefore will not be migrated.',
deleteVdiModalTitle: 'Delete VDI',
deleteVdiModalMessage: 'Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST',
revertVmModalTitle: 'Revert your VM',
deleteSnapshotModalTitle: 'Delete snapshot',
deleteSnapshotModalMessage: 'Are you sure you want to delete this snapshot?',
revertVmModalMessage: 'Are you sure you want to revert this VM to the snapshot state? This operation is irreversible.',
revertVmModalSnapshotBefore: 'Snapshot before',
importBackupModalTitle: 'Import a {name} Backup',
importBackupModalStart: 'Start VM after restore',
importBackupModalSelectBackup: 'Select your backup…',
@@ -727,17 +945,32 @@ var messages = {
serverPassword: 'Password',
serverAction: 'Action',
serverReadOnly: 'Read Only',
serverDisconnect: 'Disconnect server',
serverPlaceHolderUser: 'username',
serverPlaceHolderPassword: 'password',
serverPlaceHolderAddress: 'address[:port]',
serverConnect: 'Connect',
// ----- Copy VM -----
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',
// ----- Detach host -----
detachHostModalTitle: 'Detach host',
detachHostModalMessage: 'Are you sure you want to detach {host} from its pool? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND REBOOT THE HOST.',
detachHost: 'Detach',
// ----- Network -----
newNetworkCreate: 'Create network',
newBondedNetworkCreate: 'Create bonded network',
newNetworkInterface: 'Interface',
newNetworkName: 'Name',
newNetworkDescription: 'Description',
@@ -745,8 +978,18 @@ var messages = {
newNetworkDefaultVlan: 'No VLAN if empty',
newNetworkMtu: 'MTU',
newNetworkDefaultMtu: 'Default: 1500',
newNetworkNoNameErrorTitle: 'Name required',
newNetworkNoNameErrorMessage: 'A name is required to create a network',
newNetworkBondMode: 'Bond mode',
deleteNetwork: 'Delete network',
deleteNetworkConfirm: 'Are you sure you want to delete this network?',
networkInUse: 'This network is currently in use',
pillBonded: 'Bonded',
// ----- Add host -----
addHostSelectHost: 'Host',
addHostNoHost: 'No host',
addHostNoHostMessage: 'No host selected to be added',
// ----- About View -----
xenOrchestra: 'Xen Orchestra',
@@ -776,18 +1019,23 @@ var messages = {
or: 'Or',
tryIt: 'Try it for free!',
availableIn: 'This feature is available starting from {plan} Edition',
notAvailable: 'This feature is not available in your version, contact your administrator to know more.',
// ----- Updates View -----
updateTitle: 'Updates',
registration: 'Registration',
trial: 'Trial',
settings: 'Settings',
proxySettings: 'Proxy settings',
update: 'Update',
refresh: 'Refresh',
upgrade: 'Upgrade',
noUpdaterCommunity: 'No updater available for Community Edition',
noUpdaterSubscribe: 'Please consider subscribe and try it with all features for free during 15 days on',
noUpdaterWarning: 'Manual update could break your current installation due to dependencies issues, do it with caution',
currentVersion: 'Current version:',
register: 'Register',
editRegistration: 'Edit registration',
trialRegistration: 'Please, take time to register in order to enjoy your trial.',
trialStartButton: 'Start trial',
trialAvailableUntil: 'You can use a trial version until {date, date, medium}. Upgrade your appliance to get it.',
@@ -829,7 +1077,66 @@ var messages = {
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'
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',
// ----- Logs -----
loadingLogs: 'Loading logs...',
logUser: 'User',
logMethod: 'Method',
logParams: 'Params',
logMessage: 'Message',
logError: 'Error',
logDisplayDetails: 'Display details',
logTime: 'Date',
logNoStackTrace: 'No stack trace',
logNoParams: 'No params',
logDelete: 'Delete log',
logDeleteAll: 'Delete all logs',
logDeleteAllTitle: 'Delete all logs',
logDeleteAllMessage: 'Are you sure you want to delete all the logs?',
// ----- IPs ------
ipPoolName: 'Name',
ipPoolIps: 'IPs',
ipPoolIpsPlaceholder: 'IPs (e.g.: 1.0.0.12-1.0.0.17;1.0.0.23)',
ipPoolNetworks: 'Networks',
ipsNoIpPool: 'No IP pools',
ipsCreate: 'Create',
ipsDeleteAllTitle: 'Delete all IP pools',
ipsDeleteAllMessage: 'Are you sure you want to delete all the IP pools?',
ipsVifs: 'VIFs',
ipsNotUsed: 'Not used',
// ----- Shortcuts -----
shortcutModalTitle: 'Keyboard shortcuts',
shortcut_XoApp: 'Global',
shortcut_GO_TO_HOSTS: 'Go to hosts list',
shortcut_GO_TO_POOLS: 'Go to pools list',
shortcut_GO_TO_VMS: 'Go to VMs list',
shortcut_CREATE_VM: 'Create a new VM',
shortcut_UNFOCUS: 'Unfocus field',
shortcut_HELP: 'Show shortcuts key bindings',
shortcut_Home: 'Home',
shortcut_SEARCH: 'Focus search bar',
shortcut_NAV_DOWN: 'Next item',
shortcut_NAV_UP: 'Previous item',
shortcut_SELECT: 'Select item',
shortcut_JUMP_INTO: 'Open'
}
forEach(messages, function (message, id) {
if (isString(message)) {

126
src/common/ip.js Normal file
View File

@@ -0,0 +1,126 @@
import forEachRight from 'lodash/forEachRight'
import forEach from 'lodash/forEach'
import isArray from 'lodash/isArray'
import isIp from 'is-ip'
import some from 'lodash/some'
export { isIp }
export const isIpV4 = isIp.v4
export const isIpV6 = isIp.v6
// Source: https://github.com/ezpaarse-project/ip-range-generator/blob/master/index.js
const ipv4 = /^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(?:\.(?!$)|$)){4}$/
function ip2hex (ip) {
let parts = ip.split('.').map(str => parseInt(str, 10))
let n = 0
n += parts[3]
n += parts[2] * 256 // 2^8
n += parts[1] * 65536 // 2^16
n += parts[0] * 16777216 // 2^24
return n
}
function assertIpv4 (str, msg) {
if (!ipv4.test(str)) { throw new Error(msg) }
}
function *range (ip1, ip2) {
assertIpv4(ip1, 'argument "ip1" must be a valid IPv4 address')
assertIpv4(ip2, 'argument "ip2" must be a valid IPv4 address')
let hex = ip2hex(ip1)
let hex2 = ip2hex(ip2)
if (hex > hex2) {
let tmp = hex
hex = hex2
hex2 = tmp
}
for (let i = hex; i <= hex2; i++) {
yield `${(i >> 24) & 0xff}.${(i >> 16) & 0xff}.${(i >> 8) & 0xff}.${i & 0xff}`
}
}
// -----------------------------------------------------------------------------
export const getNextIpV4 = ip => {
const splitIp = ip.split('.')
if (splitIp.length !== 4 || some(splitIp, value => value < 0 || value > 255)) {
return
}
let index
forEachRight(splitIp, (value, i) => {
if (value < 255) {
index = i
return false
}
splitIp[i] = 1
})
if (index === 0 && +splitIp[0] === 255) {
return 0
}
splitIp[index]++
return splitIp.join('.')
}
export const formatIps = ips => {
if (!isArray(ips)) {
throw new Error('ips must be an array')
}
if (ips.length === 0) {
return []
}
const sortedIps = ips.sort((ip1, ip2) => {
const splitIp1 = ip1.split('.')
const splitIp2 = ip2.split('.')
if (splitIp1.length !== 4) {
return 1
}
if (splitIp2.length !== 4) {
return -1
}
return splitIp1[3] - splitIp2[3] +
(splitIp1[2] - splitIp2[2]) * 256 +
(splitIp1[1] - splitIp2[1]) * 256 * 256 +
(splitIp1[0] - splitIp2[0]) * 256 * 256 * 256
})
const range = { first: '', last: '' }
const formattedIps = []
let index = 0
forEach(sortedIps, ip => {
if (ip !== getNextIpV4(range.last)) {
if (range.first) {
formattedIps[index] = range.first === range.last ? range.first : { ...range }
index++
}
range.first = range.last = ip
} else {
range.last = ip
}
})
formattedIps[index] = range.first === range.last ? range.first : range
return formattedIps
}
export const parseIpPattern = pattern => {
const ips = []
forEach(pattern.split(';'), rawIpRange => {
const ipRange = rawIpRange.split('-')
if (ipRange.length < 2) {
ips.push(ipRange[0])
} else if (!isIpV4(ipRange[0]) || !isIpV4(ipRange[1])) {
ips.push(rawIpRange)
} else {
ips.push(...range(ipRange[0], ipRange[1]))
}
})
return ips
}

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

@@ -48,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'

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

@@ -1,6 +1,7 @@
import React from 'react'
import AbstractInput from './abstract-input'
import Combobox from '../combobox'
import propTypes from '../prop-types'
import { PrimitiveInputWrapper } from './helpers'
@@ -12,19 +13,19 @@ import { PrimitiveInputWrapper } from './helpers'
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.password ? 'password' : 'text'}
type={props.password && 'password'}
/>
</PrimitiveInputWrapper>
)

View File

@@ -60,7 +60,10 @@ const _addRef = (component, ref) => {
class Confirm extends Component {
_resolve = () => {
const { body } = this.refs
this.props.resolve(body && body.value || body.getWrappedInstance && body.getWrappedInstance().value)
this.props.resolve(body && (body.getWrappedInstance
? body.getWrappedInstance().value
: body.value
))
instance.close()
}
_reject = () => {
@@ -79,9 +82,10 @@ class Confirm extends Component {
return <div>
<Header closeButton>
<Title>
{icon
? <span><Icon icon={icon} /> {title}</span>
: title}
{icon
? <span><Icon icon={icon} /> {title}</span>
: title
}
</Title>
</Header>
<Body>

View File

@@ -6,6 +6,7 @@ import {
parse as parseUrl,
resolve as resolveUrl
} from 'url'
import { enable as enableShortcuts, disable as disableShortcuts } from 'shortcuts'
import propTypes from './prop-types'
@@ -70,6 +71,7 @@ export default class NoVnc extends Component {
this._rfb = null
rfb.disconnect()
}
enableShortcuts()
}
_connect = () => {
@@ -92,6 +94,7 @@ export default class NoVnc extends Component {
})
rfb.connect(formatUrl(url))
disableShortcuts()
}
componentDidMount () {
@@ -102,6 +105,14 @@ export default class NoVnc extends Component {
this._clean()
}
componentWillReceiveProps (props) {
const rfb = this._rfb
if (rfb && this.props.scale !== props.scale) {
rfb.get_display().set_scale(props.scale || 1)
rfb.get_mouse().set_scale(props.scale || 1)
}
}
_focus = () => {
const rfb = this._rfb
if (rfb) {
@@ -112,6 +123,8 @@ export default class NoVnc extends Component {
rfb.get_keyboard().grab()
rfb.get_mouse().grab()
disableShortcuts()
}
}
@@ -120,6 +133,8 @@ export default class NoVnc extends Component {
if (rfb) {
rfb.get_keyboard().ungrab()
rfb.get_mouse().ungrab()
enableShortcuts()
}
}

View File

@@ -1,4 +1,4 @@
import React, { Component } from 'react'
import React from 'react'
import Icon from './icon'
import propTypes from './prop-types'
@@ -55,13 +55,12 @@ export const SrItem = propTypes({
let label = `${sr.name_label || sr.id}`
if (isSrWritable(sr)) {
label += ` (${formatSize(sr.size)})`
label += ` (${formatSize(sr.size - sr.physical_usage)} free)`
}
return (
<span>
<Icon icon='sr' /> {label}
{container && ` (${container.name_label || container.id})`}
</span>
)
}))
@@ -108,6 +107,27 @@ const xoItemToRender = {
<Icon icon='user' /> {user.email}
</span>
),
resourceSet: resourceSet => (
<span>
<Icon icon='resource-set' /> {resourceSet.name}
</span>
),
sshKey: key => (
<span>
<Icon icon='ssh-key' /> {key.label}
</span>
),
ipPool: ipPool => (
<span>
<Icon icon='ip' /> {ipPool.name}
</span>
),
ipAddress: ({label, used}) => {
if (used) {
return <strong className='text-warning'>{label}</strong>
}
return <span>{label}</span>
},
// XO objects.
pool: pool => (
@@ -133,6 +153,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 => (
@@ -154,7 +181,10 @@ const renderXoItem = (item, {
} = {}) => {
const { id, type, label } = item
if (!type && label) {
if (!type) {
if (process.env.NODE_ENV !== 'production' && !label) {
throw new Error(`an item must have at least either a type or a label`)
}
return (
<span key={id} className={className}>
{label}

View File

@@ -1,13 +1,11 @@
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 { FormattedTime } from 'react-intl'
import { FormattedDate, FormattedTime } from 'react-intl'
import {
Panel,
Tab,
Tabs
} from 'react-bootstrap-4/lib'
@@ -15,21 +13,29 @@ import {
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
const MONTHS = [
[ 0, 1, 2, 3, 4, 5 ],
[ 6, 7, 8, 9, 10, 11 ]
[ 0, 1, 2 ],
[ 3, 4, 5 ],
[ 6, 7, 8 ],
[ 9, 10, 11 ]
]
const DAYS = (() => {
@@ -48,7 +54,11 @@ const DAYS = (() => {
return days
})()
const WEEK_DAYS = [[ 0, 1, 2, 3, 4, 5, 6 ]]
const WEEK_DAYS = [
[ 0, 1, 2 ],
[ 3, 4, 5 ],
[ 6 ]
]
const HOURS = (() => {
const hours = []
@@ -78,30 +88,46 @@ 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'
}
// ===================================================================
// monthNum: [ 0 : 11 ]
const getMonthName = (monthNum) =>
<FormattedTime value={new Date(1970, monthNum)} month='long' />
<FormattedDate value={Date.UTC(1970, monthNum)} month='long' timeZone='UTC' />
// dayNum: [ 0 : 6 ]
const getDayName = (dayNum) =>
// January, 1970, 5th => Monday
<FormattedTime value={new Date(1970, 0, 4 + dayNum)} weekday='long' />
<FormattedDate value={Date.UTC(1970, 0, 4 + dayNum)} weekday='long' timeZone='UTC' />
// ===================================================================
@propTypes({
cron: propTypes.string.isRequired
cronPattern: propTypes.string.isRequired
})
export class SchedulePreview extends Component {
_handleChange = value => {
@@ -111,12 +137,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>
@@ -137,34 +166,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>
)
}
@@ -173,79 +189,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)}
/>
)
})}
@@ -264,177 +265,153 @@ 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,
valueRenderer: propTypes.func
})
class TimePicker extends Component {
static defaultProps = {
valueRenderer: e => +e
}
constructor () {
super()
this.state = {
activeKey: NAV_EVERY
activeKey: NAV_EACH_SELECTED,
tableValue: []
}
}
get value () {
const { activeKey } = this.state
const { refs } = this
_update (props) {
const { value, valueRenderer } = 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
}, () => { this.refs.range.value = value.split('/')[1] })
} else {
this.setState({
activeKey: NAV_EACH_SELECTED,
tableValue: value === '*'
? []
: map(value.split(','), valueRenderer)
})
refs.range.value = value
}
}
_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]
const decrement = e => e - 1
@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)
@@ -443,49 +420,71 @@ 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']]}
valueRenderer={decrement}
/>
<TimePicker
ref='monthDay'
type='MonthDay'
data={DAYS}
labelId='MonthDay'
options={DAYS}
onChange={this._onMonthDayChange}
value={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
valueRenderer={decrement}
/>
<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

@@ -2,12 +2,17 @@ import React from 'react'
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 isEmpty from 'lodash/isEmpty'
import keyBy from 'lodash/keyBy'
import keys from 'lodash/keys'
import map from 'lodash/map'
import mapValues from 'lodash/mapValues'
import pick from 'lodash/pick'
import sortBy from 'lodash/sortBy'
import store from 'store'
import { parse as parseRemote } from 'xo-remote-parser'
import _ from './intl'
@@ -19,16 +24,21 @@ import {
createFilter,
createGetObjectsOfType,
createGetTags,
createSelector
createSelector,
getObject
} from './selectors'
import {
connectStore,
mapPlus
mapPlus,
resolveResourceSets
} from './utils'
import {
isSrWritable,
subscribeCurrentUser,
subscribeGroups,
subscribeIpPools,
subscribeRemotes,
subscribeResourceSets,
subscribeRoles,
subscribeUsers
} from './xo'
@@ -45,6 +55,32 @@ const getLabel = object =>
// ===================================================================
/*
* WITHOUT xoContainers :
*
* xoObjects: [
* { type: 'myType', id: 'abc', label: 'First object' },
* { type: 'myType', id: 'def', label: 'Second object' }
* ]
*
*
* WITH xoContainers :
*
* xoContainers: [
* { type: 'containerType', id: 'ghi', label: 'First container' },
* { type: 'containerType', id: 'jkl', label: 'Second container' }
* ]
*
* xoObjects: {
* ghi: [
* { type: 'objectType', id: 'mno', label: 'First object' }
* { type: 'objectType', id: 'pqr', label: 'Second object' }
* ],
* jkl: [
* { type: 'objectType', id: 'stu', label: 'Third object' }
* ]
* }
*/
@propTypes({
autoFocus: propTypes.bool,
clearable: propTypes.bool,
@@ -77,7 +113,7 @@ export class GenericSelect extends Component {
// Returns the values of the selected objects
// if they are contained in xoObjectsById.
return mapPlus(value, (value, push) => {
const o = xoObjectsById[value.value || value]
const o = xoObjectsById[value.value !== undefined ? value.value : value]
if (o) {
push(o)
@@ -91,11 +127,11 @@ export class GenericSelect extends Component {
// Supports id strings and objects.
_setValue (value, props = this.props) {
if (props.multi) {
return map(value, object => object.id || object)
return map(value, object => object.id !== undefined ? object.id : object)
}
return (value != null)
? value.id || value
? value.id !== undefined ? value.id : value
: ''
}
@@ -197,7 +233,7 @@ export class GenericSelect extends Component {
this.setState({
value: this._setValue(value)
}, onChange && (() => { onChange(this.value) }))
}, onChange && (() => onChange(this.value)))
}
// GroupBy: Display option with margin if not disabled and containers exists.
@@ -262,13 +298,28 @@ const makeSubscriptionSelect = (subscribe, props) => (
class extends Component {
constructor (props) {
super(props)
this.state = {
xoObjects: []
}
this._getFilteredXoObjects = createFilter(
this._getFilteredXoContainers = createFilter(
() => this.state.xoContainers,
() => this.props.containerPredicate
)
this._getFilteredXoObjects = createSelector(
() => this.state.xoObjects,
() => this.props.predicate
() => this.state.xoContainers && this._getFilteredXoContainers(),
() => this.props.predicate,
(xoObjects, xoContainers, predicate) => {
if (xoContainers == null) {
return filter(xoObjects, predicate)
} else {
// Filter xoObjects with `predicate`...
const filteredObjects = mapValues(xoObjects, xoObjectsGroup =>
filter(xoObjectsGroup, predicate)
)
// ...and keep only those whose xoContainer hasn't been filtered out
return pick(filteredObjects, map(xoContainers, container => container.id))
}
}
)
}
@@ -291,7 +342,7 @@ const makeSubscriptionSelect = (subscribe, props) => (
{...props}
{...this.props}
xoObjects={this._getFilteredXoObjects()}
xoContainers={this.state.xoContainers}
xoContainers={this.state.xoContainers && this._getFilteredXoContainers()}
/>
)
}
@@ -414,7 +465,7 @@ export const SelectVmTemplate = makeStoreSelect(() => {
xoObjects: getVmTemplatesByPool,
xoContainers: getPools
}
}, { placeholder: _('selectVms') })
}, { placeholder: _('selectVmTemplates') })
// ===================================================================
@@ -580,3 +631,244 @@ 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()}
/>
)
}
}
// ===================================================================
export class SelectSshKey extends Component {
get value () {
return this.refs.select.value
}
set value (value) {
this.refs.select.value = value
}
componentWillMount () {
this.componentWillUnmount = subscribeCurrentUser(user => {
this.setState({
sshKeys: user && user.preferences && map(user.preferences.sshKeys, (key, id) => ({
id,
label: key.title,
type: 'sshKey'
}))
})
})
}
render () {
return (
<GenericSelect
ref='select'
placeholder={_('selectSshKey')}
{...this.props}
xoObjects={this.state.sshKeys || []}
/>
)
}
}
// ===================================================================
export const SelectIp = makeSubscriptionSelect(subscriber => {
const unsubscribeIpPools = subscribeIpPools(ipPools => {
const sortedIpPools = sortBy(ipPools, 'name')
const xoObjects = mapValues(
groupBy(sortedIpPools, 'id'),
ipPools => map(ipPools[0].addresses, (address, ip) => ({
...address,
id: ip,
label: ip,
type: 'ipAddress',
used: !isEmpty(address.vifs)
}))
)
const xoContainers = map(sortedIpPools, ipPool => ({
...ipPool,
type: 'ipPool'
}))
subscriber({ xoObjects, xoContainers })
})
return unsubscribeIpPools
}, { placeholder: _('selectIp') })

View File

@@ -7,6 +7,7 @@ import isArray from 'lodash/isArray'
import isArrayLike from 'lodash/isArrayLike'
import isFunction from 'lodash/isFunction'
import keys from 'lodash/keys'
import map from 'lodash/map'
import orderBy from 'lodash/orderBy'
import pickBy from 'lodash/pickBy'
import size from 'lodash/size'
@@ -212,6 +213,8 @@ const _getId = (state, { routeParams, id }) => routeParams
export const getLang = state => state.lang
export const getStatus = state => state.status
export const getUser = state => state.user
const _getPermissionsPredicate = invoke(() => {
@@ -240,18 +243,29 @@ const _getPermissionsPredicate = invoke(() => {
}
})
export const isAdmin = (...args) => {
const user = getUser(...args)
return user && user.permission === 'admin'
}
// ===================================================================
// Common selector creators.
// 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
@@ -315,7 +329,11 @@ const _extendCollectionSelector = (selector, objectsType) => {
return selector
}
_addGroupBy(selector)
selector.find = predicate => createFinder(selector, predicate)
const _addFind = selector => {
selector.find = predicate => createFinder(selector, predicate)
return selector
}
_addFind(selector)
// groupBy can be chained.
const _addSort = selector => {
@@ -335,9 +353,9 @@ const _extendCollectionSelector = (selector, objectsType) => {
_addFilter(selector)
// filter, groupBy and sort can be chained.
selector.pick = idsSelector => _addFilter(_addGroupBy(_addSort(
selector.pick = idsSelector => _addFind(_addFilter(_addGroupBy(_addSort(
createPicker(selector, idsSelector)
)))
))))
return selector
}
@@ -355,7 +373,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 (filter, groupBy and sort can be chained)
// ids (filter, find, groupBy and sort can be chained)
// - sort: returns a selector which returns the objects appropriately
// sorted (groupBy can be chained)
export const createGetObjectsOfType = type => {
@@ -409,6 +427,32 @@ export const createGetObjectMessages = objectSelector =>
// ...
export const getObject = createGetObject((_, id) => id)
export const createDoesHostNeedRestart = hostSelector => {
// Returns the first patch of the host which requires it to be
// restarted.
const restartPoolPatch = createGetObjectsOfType('pool_patch').pick(
create(
createGetObjectsOfType('host_patch').pick(
(state, props) => {
const host = hostSelector(state, props)
return host && host.patches
}
).filter(create(
(state, props) => {
const host = hostSelector(state, props)
return host && host.startTime
},
startTime => patch => patch.time > startTime
)),
hostPatches => map(hostPatches, hostPatch => hostPatch.pool_patch)
)
).find([ ({ guidance }) => find(guidance, action =>
action === 'restartHost' || action === 'restartXapi'
) ])
return (state, props) => restartPoolPatch(state, props) !== undefined
}
export const createGetHostMetrics = hostSelector => _createCollectionWrapper(
create(
hostSelector,

35
src/common/shortcuts.js Normal file
View File

@@ -0,0 +1,35 @@
import Component from 'base-component'
import forEach from 'lodash/forEach'
import React from 'react'
import remove from 'lodash/remove'
import { Shortcuts as ReactShortcuts } from 'react-shortcuts'
let enabled = true
const instances = []
const updateInstances = () => {
forEach(instances, instance => instance.forceUpdate())
}
export const enable = () => {
enabled = true
updateInstances()
}
export const disable = () => {
enabled = false
updateInstances()
}
export default class Shortcuts extends Component {
componentDidMount () {
instances.push(this)
}
componentWillUnmount () {
remove(instances, this)
}
render () {
return enabled ? <ReactShortcuts {...this.props} /> : null
}
}

View File

@@ -1,14 +1,21 @@
import React from 'react'
import _ from 'intl'
import ceil from 'lodash/ceil'
import debounce from 'lodash/debounce'
import findIndex from 'lodash/findIndex'
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 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 {
@@ -24,16 +31,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 => {
@@ -46,7 +56,22 @@ class TableFilter extends Component {
return (
<div className='input-group'>
<span className='input-group-addon'>{props.nFilteredItems} / {props.nItems}</span>
<span className='input-group-addon'><Icon icon='search' /></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'
@@ -115,6 +140,7 @@ const DEFAULT_ITEMS_PER_PAGE = 10
propTypes.object
]).isRequired,
columns: propTypes.arrayOf(propTypes.shape({
default: propTypes.bool,
name: propTypes.node.isRequired,
itemRenderer: propTypes.func.isRequired,
sortCriteria: propTypes.oneOfType([
@@ -124,16 +150,30 @@ 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 {
constructor (props) {
super(props)
let selectedColumn = props.defaultColumn
if (selectedColumn == null) {
selectedColumn = findIndex(props.columns, 'default')
if (selectedColumn === -1) {
selectedColumn = 0
}
}
this.state = {
selectedColumn: props.defaultColumn || 0,
selectedColumn,
itemsPerPage: props.itemsPerPage || DEFAULT_ITEMS_PER_PAGE
}
@@ -173,7 +213,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 () {
@@ -220,6 +262,8 @@ export default class SortedTable extends Component {
const {
paginationContainer,
filterContainer,
filters,
rowLink,
userData
} = props
@@ -242,6 +286,7 @@ export default class SortedTable extends Component {
const filterInstance = (
<TableFilter
filters={filters}
nFilteredItems={nFilteredItems}
nItems={this._getTotalNumberOfItems()}
onChange={this._onFilterChange}
@@ -265,15 +310,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 columns = 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}
>{columns}</BlockLink>
: <tr key={id}>{columns}</tr>
})}
</tbody>
</table>
{(!paginationContainer || !filterContainer) && (

View File

@@ -1,12 +1,38 @@
import React from 'react'
import filter from 'lodash/filter'
import includes from 'lodash/includes'
import map from 'lodash/map'
import React from 'react'
import Component from './base-component'
import Icon from './icon'
import propTypes from './prop-types'
const INPUT_STYLE = {
margin: '2px',
maxWidth: '4em'
}
const TAG_STYLE = {
backgroundColor: '#2598d9',
borderRadius: '0.5em',
color: 'white',
fontSize: '0.6em',
margin: '0.2em',
marginTop: '-0.1em',
padding: '0.3em',
verticalAlign: 'middle'
}
const ADD_TAG_STYLE = {
cursor: 'pointer',
fontSize: '0.8em',
marginLeft: '0.2em'
}
const REMOVE_TAG_STYLE = {
cursor: 'pointer'
}
@propTypes({
labels: propTypes.arrayOf(React.PropTypes.string).isRequired,
onChange: propTypes.func,
onDelete: propTypes.func,
onAdd: propTypes.func
})
@@ -22,45 +48,70 @@ export default class Tags extends Component {
this.setState({ editing: false })
}
_addTag = newTag => {
const { labels, onAdd, onChange } = this.props
if (!includes(labels, newTag)) {
onAdd && onAdd(newTag)
onChange && onChange([ ...labels, newTag ])
}
}
_deleteTag = tag => {
const { onChange, onDelete } = this.props
onDelete && onDelete(tag)
onChange && onChange(filter(this.props.labels, t => t !== tag))
}
_onKeyDown = event => {
const { keyCode, target } = event
if (keyCode === 13) {
if (target.value) {
this._addTag(target.value)
target.value = ''
}
} else if (keyCode === 27) {
this._stopEdit()
} else {
return
}
event.preventDefault()
}
render () {
const {
labels,
onDelete,
onAdd
onAdd,
onChange,
onDelete
} = this.props
const deleteTag = (onDelete || onChange) && this._deleteTag
return (
<span className='form-group' style={{ color: '#999' }}>
<Icon icon='tags' />
{' '}
<span>
{map(labels.sort(), (label, index) =>
<Tag label={label} onDelete={onDelete} key={index} />
<Tag label={label} onDelete={deleteTag} key={index} />
)}
</span>
{onAdd
? !this.state.editing
? <span className='add-tag-action' onClick={this._startEdit} style={{cursor: 'pointer'}}>
<Icon icon='add-tag' />
</span>
: <span>
<input
type='text'
autoFocus
style={{maxWidth: '4em', margin: '2px'}}
onKeyDown={event => {
const { target } = event
if (event.keyCode === 13 && target.value) {
onAdd(target.value)
target.value = ''
} else if (event.keyCode === 27) {
this._stopEdit()
}
}}
onBlur={this._stopEdit}
></input>
</span>
: []
{(onAdd || onChange) && !this.state.editing
? <span onClick={this._startEdit} style={ADD_TAG_STYLE}>
<Icon icon='add-tag' />
</span>
: <span>
<input
type='text'
autoFocus
style={INPUT_STYLE}
onKeyDown={this._onKeyDown}
onBlur={this._stopEdit}
/>
</span>
}
</span>
)
@@ -68,10 +119,10 @@ export default class Tags extends Component {
}
export const Tag = ({ label, onDelete }) => (
<span className='xo-tag'>
<span style={TAG_STYLE}>
{label}{' '}
{onDelete
? <span onClick={onDelete && (() => onDelete(label))} style={{cursor: 'pointer'}}>
? <span onClick={onDelete && (() => onDelete(label))} style={REMOVE_TAG_STYLE}>
<Icon icon='remove-tag' />
</span>
: []

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,148 @@
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
this._hideTooltip()
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
}
}

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

@@ -0,0 +1,72 @@
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 }
export const Limits = ({ used, toBeUsed, limit }) => {
const available = limit - used
return <span className='limits'>
<span
className='limits-used'
style={{ width: ((used || 0) / limit) * 100 + '%' }}
/>
<span
className={toBeUsed > available ? 'limits-over-used' : 'limits-to-be-used'}
style={{ width: (Math.min((toBeUsed || 0), available) / limit) * 100 + '%' }}
/>
</span>
}
Limits.propTypes = {
used: PropTypes.number,
toBeUsed: PropTypes.number,
limit: PropTypes.number.isRequired
}

View File

@@ -1,17 +1,26 @@
import * as actions from 'store/actions'
import escapeRegExp from 'lodash/escapeRegExp'
import every from 'lodash/every'
import forEach from 'lodash/forEach'
import getStream from 'get-stream'
import humanFormat from 'human-format'
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 React from 'react'
import ReadableStream from 'readable-stream'
import replace from 'lodash/replace'
import store from 'store'
import { connect } from 'react-redux'
import { getObject } from 'selectors'
import _ from './intl'
import BaseComponent from './base-component'
import invoke from './invoke'
@@ -174,11 +183,12 @@ export const firstDefined = function () {
const n = arguments.length
for (let i = 0; i < n; ++i) {
const arg = arguments[i]
if (arg != null) {
if (arg !== undefined) {
return arg
}
}
}
// -------------------------------------------------------------------
// Returns the current XOA Plan or the Plan name if number given
@@ -203,7 +213,7 @@ export const getXoaPlan = plan => {
export const mapPlus = (collection, cb) => {
const result = []
const push = ::result.push
forEach(collection, value => cb(value, push))
forEach(collection, (value, index) => cb(value, push, index))
return result
}
@@ -216,10 +226,10 @@ export const noop = () => {}
export const osFamily = invoke({
centos: [ 'centos' ],
debian: [ 'debian' ],
docker: [ 'coreos' ],
fedora: [ 'fedora' ],
freebsd: [ 'freebsd' ],
gentoo: [ 'gentoo' ],
linux: [ 'coreos' ],
'linux-mint': [ 'linux-mint' ],
netbsd: [ 'netbsd' ],
oracle: [ 'oracle' ],
@@ -275,7 +285,7 @@ export const normalizeXenToolsStatus = status => {
// -------------------------------------------------------------------
const _NotFound = () => <h1>Page not found</h1>
const _NotFound = () => <h1>{_('errorPageNotFound')}</h1>
// Decorator to declare routes on a component.
//
@@ -350,3 +360,102 @@ export function rethrow (cb) {
Promise.resolve(cb(error)).then(() => { throw error })
)
}
// ===================================================================
export const resolveResourceSet = resourceSet => {
if (!resourceSet) {
return
}
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
}
export const resolveResourceSets = resourceSets =>
map(resourceSets, resolveResourceSet)
// -------------------------------------------------------------------
// 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
})
}
// ===================================================================
export const streamToString = getStream
// ===================================================================
/* global FileReader */
// Creates a readable stream from a HTML file.
export const htmlFileToStream = file => {
const reader = new FileReader()
const stream = new ReadableStream()
let offset = 0
reader.onloadend = evt => {
stream.push(evt.target.result)
}
reader.onerror = error => {
stream.emit('error', error)
}
stream._read = function (size) {
if (offset >= file.size) {
stream.push(null)
} else {
reader.readAsBinaryString(file.slice(offset, offset + size))
offset += size
}
}
return stream
}

View File

@@ -7,6 +7,7 @@ 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 XoTagInput from './xo-tag-input'
import XoVmInput from './xo-vm-input'
import { getType, getXoType } from '../json-schema-input/helpers'
@@ -14,13 +15,14 @@ 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
tag: XoTagInput,
vm: XoVmInput,
xoobject: XoHighLevelObjectInput
}
// ===================================================================

View File

@@ -0,0 +1,26 @@
import React from 'react'
import { SelectTag } from 'select-objects'
import XoAbstractInput from './xo-abstract-input'
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
// ===================================================================
export default class TagInput extends XoAbstractInput {
render () {
const { props } = this
return (
<PrimitiveInputWrapper {...props}>
<SelectTag
disabled={props.disabled}
multi={props.multi}
onChange={props.onChange}
ref='input'
required={props.required}
defaultValue={props.defaultValue}
/>
</PrimitiveInputWrapper>
)
}
}

View File

@@ -3,8 +3,10 @@ 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 find from 'lodash/find'
import propTypes from '../prop-types'
import { computeArraysSum } from '../xo-stats'
@@ -16,7 +18,15 @@ import styles from './index.css'
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,
@@ -73,10 +83,15 @@ const buildSeries = ({ stats, label, addSumSeries }) => {
for (const io in stats) {
const ioData = stats[io]
for (const letter in ioData) {
series.push({
name: `${label}${letter} (${io})`,
data: ioData[letter]
})
const data = ioData[letter]
// See xo-stats.js, data can be null.
if (data) {
series.push({
name: `${label}${letter} (${io})`,
data
})
}
}
if (addSumSeries) {
@@ -104,7 +119,7 @@ export const CpuLineChart = injectIntl(propTypes({
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const stats = data.stats.cpus
const { length } = (stats && stats[0]) || {}
const length = getStatsLength(stats)
if (!length) {
return templateError
@@ -187,7 +202,7 @@ export const XvdLineChart = injectIntl(propTypes({
options: propTypes.object
})(({ 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
@@ -219,7 +234,7 @@ export const VifLineChart = injectIntl(propTypes({
options: propTypes.object
})(({ 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
@@ -251,7 +266,7 @@ export const PifLineChart = injectIntl(propTypes({
options: propTypes.object
})(({ 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

View File

@@ -1,8 +1,7 @@
import React from 'react'
import {
Sparklines,
SparklinesLine,
SparklinesSpots
SparklinesLine
} from 'react-sparklines'
import propTypes from './prop-types'
@@ -14,7 +13,8 @@ import {
const STYLE = {}
const WIDTH = 120
const HEIGHT = 40
const HEIGHT = 20
const STROKE_WIDTH = 0.5
// ===================================================================
@@ -27,7 +27,7 @@ const templateError =
export const CpuSparkLines = propTypes({
data: propTypes.object.isRequired
})(({ data }) => {
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
const { cpus } = data.stats
if (!cpus) {
@@ -35,16 +35,15 @@ export const CpuSparkLines = propTypes({
}
return (
<Sparklines style={STYLE} data={computeArraysAvg(cpus)} max={100} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 1, stroke: '#366e98', fill: '#366e98', fillOpacity: 0.5 }} color='#2598d9' />
<SparklinesSpots />
<Sparklines style={STYLE} data={computeArraysAvg(cpus)} max={100} min={0} width={width} height={height}>
<SparklinesLine style={{ strokeWidth, stroke: '#366e98', fill: '#366e98', fillOpacity: 0.5 }} color='#2598d9' />
</Sparklines>
)
})
export const MemorySparkLines = propTypes({
data: propTypes.object.isRequired
})(({ data }) => {
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
const { memory, memoryUsed } = data.stats
if (!memory || !memoryUsed) {
@@ -52,16 +51,15 @@ export const MemorySparkLines = propTypes({
}
return (
<Sparklines style={STYLE} data={memoryUsed} max={memory[memory.length - 1]} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 1, stroke: '#990822', fill: '#990822', fillOpacity: 0.5 }} color='#cc0066' />
<SparklinesSpots />
<Sparklines style={STYLE} data={memoryUsed} max={memory[memory.length - 1]} min={0} width={width} height={height}>
<SparklinesLine style={{ strokeWidth, stroke: '#990822', fill: '#990822', fillOpacity: 0.5 }} color='#cc0066' />
</Sparklines>
)
})
export const XvdSparkLines = propTypes({
data: propTypes.object.isRequired
})(({ data }) => {
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
const { xvds } = data.stats
if (!xvds) {
@@ -69,16 +67,15 @@ export const XvdSparkLines = propTypes({
}
return (
<Sparklines style={STYLE} data={computeObjectsAvg(xvds)} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 1, stroke: '#089944', fill: '#089944', fillOpacity: 0.5 }} color='#33cc33' />
<SparklinesSpots />
<Sparklines style={STYLE} data={computeObjectsAvg(xvds)} min={0} width={width} height={height}>
<SparklinesLine style={{ strokeWidth, stroke: '#089944', fill: '#089944', fillOpacity: 0.5 }} color='#33cc33' />
</Sparklines>
)
})
export const VifSparkLines = propTypes({
data: propTypes.object.isRequired
})(({ data }) => {
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
const { vifs } = data.stats
if (!vifs) {
@@ -86,16 +83,15 @@ export const VifSparkLines = propTypes({
}
return (
<Sparklines style={STYLE} data={computeObjectsAvg(vifs)} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 1, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
<SparklinesSpots />
<Sparklines style={STYLE} data={computeObjectsAvg(vifs)} min={0} width={width} height={height}>
<SparklinesLine style={{ strokeWidth, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
</Sparklines>
)
})
export const PifSparkLines = propTypes({
data: propTypes.object.isRequired
})(({ data }) => {
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
const { pifs } = data.stats
if (!pifs) {
@@ -103,16 +99,15 @@ export const PifSparkLines = propTypes({
}
return (
<Sparklines style={STYLE} data={computeObjectsAvg(pifs)} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 1, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
<SparklinesSpots />
<Sparklines style={STYLE} data={computeObjectsAvg(pifs)} min={0} width={width} height={height}>
<SparklinesLine style={{ strokeWidth, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
</Sparklines>
)
})
export const LoadSparkLines = propTypes({
data: propTypes.object.isRequired
})(({ data }) => {
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
const { load } = data.stats
if (!load) {
@@ -120,9 +115,8 @@ export const LoadSparkLines = propTypes({
}
return (
<Sparklines style={STYLE} data={load} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 1, stroke: '#33cc33', fill: '#33cc33', fillOpacity: 0.5 }} color='#33cc33' />
<SparklinesSpots />
<Sparklines style={STYLE} data={load} min={0} width={width} height={height}>
<SparklinesLine style={{ strokeWidth, stroke: '#33cc33', fill: '#33cc33', fillOpacity: 0.5 }} color='#33cc33' />
</Sparklines>
)
})

View File

@@ -4,10 +4,11 @@
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]
export const computeArraysSum = arrays => {
const _computeArraysSum = arrays => {
if (!arrays || !arrays.length || !arrays[0].length) {
return []
}
@@ -30,9 +31,12 @@ export const computeArraysSum = arrays => {
// Returns a new array with arrays avgs.
// Example: computeArraysAvg([[1, 2], [3, 4], [5, 0]) = [4.5, 2]
export const computeArraysAvg = arrays => {
const sums = computeArraysSum(arrays)
const _computeArraysAvg = arrays => {
const sums = _computeArraysSum(arrays)
if (!arrays[0]) {
return []
}
const n = arrays && arrays[0].length
const m = arrays.length
@@ -43,6 +47,20 @@ export const computeArraysAvg = arrays => {
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:
@@ -53,7 +71,7 @@ export const computeArraysAvg = arrays => {
//
// Note: The parameter can be also an 3D array.
export const computeObjectsAvg = objects => {
return computeArraysAvg(
return _computeArraysAvg(
map(objects, object =>
computeArraysAvg(values(object))
)

View File

@@ -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,37 @@
import _ from 'intl'
import BaseComponent from 'base-component'
import every from 'lodash/every'
import React from 'react'
import SingleLineRow from 'single-line-row'
import { SelectHost } from 'select-objects'
import { Col } from 'grid'
import { connectStore } from 'utils'
import { createGetObjectsOfType } from 'selectors'
@connectStore(() => ({
hosts: createGetObjectsOfType('host')
}), { withRef: true })
export default class AddHostModal extends BaseComponent {
get value () {
return this.state
}
_hostPredicate = host =>
host.$pool !== this.props.pool.id &&
every(this.props.hosts, h => h.$pool !== host.$pool || h.id === host.id)
render () {
return <div>
<SingleLineRow>
<Col size={6}>{_('addHostSelectHost')}</Col>
<Col size={6}>
<SelectHost
onChange={this.linkState('host')}
predicate={this._hostPredicate}
value={this.state.host}
/>
</Col>
</SingleLineRow>
</div>
}
}

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 })

View File

@@ -0,0 +1,109 @@
import Component from 'base-component'
import map from 'lodash/map'
import React from 'react'
import { createGetObject, createSelector } from 'selectors'
import { getBondModes } from 'xo'
import { injectIntl } from 'react-intl'
import _, { messages } from '../../intl'
import { Col } from '../../grid'
import { connectStore } from '../../utils'
import { SelectPif } from '../../select-objects'
import SingleLineRow from '../../single-line-row'
@connectStore(() => ({
poolMaster: createSelector(
createGetObject(
(_, props) => props.pool
),
pool => pool.master
)
}), { withRef: true })
class CreateBondedNetworkModalBody extends Component {
componentWillMount () {
getBondModes().then(
bondModes => this.setState({ bondModes, bondMode: bondModes[0] })
)
}
_getPifPredicate = createSelector(
() => this.props.poolMaster,
hostId => pif =>
pif.$host === hostId && pif.vlan === -1
)
get value () {
const { name, description, pifs, mtu, bondMode } = this.state
return {
pool: this.props.pool,
name,
description,
pifs: map(pifs, pif => pif.id),
mtu,
bondMode
}
}
render () {
const { formatMessage } = this.props.intl
return <div>
<SingleLineRow>
<Col size={6}>{_('newNetworkInterface')}</Col>
<Col size={6}>
<SelectPif
multi
onChange={this.linkState('pifs')}
predicate={this._getPifPredicate()}
/>
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('newNetworkName')}</Col>
<Col size={6}>
<input
className='form-control'
onChange={this.linkState('name')}
type='text'
/>
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('newNetworkDescription')}</Col>
<Col size={6}>
<input
className='form-control'
onChange={this.linkState('description')}
type='text'
/>
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('newNetworkMtu')}</Col>
<Col size={6}>
<input
className='form-control'
onChange={this.linkState('mtu')}
placeholder={formatMessage(messages.newNetworkDefaultMtu)}
type='text'
/>
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('newNetworkBondMode')}</Col>
<Col size={6}>
<select
className='form-control'
onChange={this.linkState('bondMode')}
>
{map(this.state.bondModes, mode => <option value={mode}>{mode}</option>)}
</select>
</Col>
</SingleLineRow>
</div>
}
}
export default injectIntl(CreateBondedNetworkModalBody, { withRef: true })

View File

@@ -20,7 +20,7 @@ class CreateNetworkModalBody extends Component {
const { refs } = this
const { container } = this.props
return {
pool: container === 'pool' ? container.id : container.$pool,
pool: container.$pool,
name: refs.name.value,
description: refs.description.value,
pif: refs.pif.value.id,

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,11 @@
import BaseComponent from 'base-component'
import every from 'lodash/every'
import forEach from 'lodash/forEach'
import find from 'lodash/find'
import map from 'lodash/map'
import mapValues from 'lodash/mapValues'
import React from 'react'
import store from 'store'
import _ from '../../intl'
import invoke from '../../invoke'
@@ -22,8 +24,12 @@ import {
import {
createGetObjectsOfType,
createPicker,
createSelector
createSelector,
getObject
} from '../../selectors'
import {
isSrShared
} from 'xo'
import { isSrWritable } from '../'
@@ -59,6 +65,7 @@ import styles from './index.css'
networks: getNetworks,
pifs: getPifs,
pools: getPools,
vbds: getVbds,
vdis: getVdis,
vifs: getVifs
}
@@ -85,7 +92,26 @@ export default class MigrateVmModalBody extends BaseComponent {
)
)
this._getNetworkPredicate = createSelector(
this._getTargetNetworkPredicate = createSelector(
createPicker(
() => this.props.pifs,
() => this.state.host.$PIFs
),
pifs => {
if (!pifs) {
return false
}
const networks = {}
forEach(pifs, pif => {
networks[pif.$network] = true
})
return network => networks[network.id]
}
)
this._getMigrationNetworkPredicate = createSelector(
createPicker(
() => this.props.pifs,
() => this.state.host.$PIFs
@@ -118,14 +144,53 @@ export default class MigrateVmModalBody extends BaseComponent {
}
}
_getObject (id) {
return getObject(store.getState(), id)
}
_selectHost = host => {
// No host selected
if (!host) {
this.setState({ intraPool: undefined, host: undefined })
this.setState({
host: undefined,
intraPool: undefined
})
return
}
const { networks, pools, pifs, vdis, vifs } = this.props
const defaultMigrationNetworkId = find(pifs, pif => pif.$host === host.id && pif.management).$network
const { pools, vbds, vdis, vm } = this.props
const intraPool = vm.$pool === host.$pool
// Intra-pool
const defaultSr = pools[host.$pool].default_SR
if (intraPool) {
let doNotMigrateVdis
if (vm.$container === host.id) {
doNotMigrateVdis = true
} else {
const _doNotMigrateVdi = {}
forEach(vbds, vbd => {
if (vbd.VDI != null) {
_doNotMigrateVdi[vbd.VDI] = isSrShared(this._getObject(this._getObject(vbd.VDI).$SR))
}
})
doNotMigrateVdis = every(_doNotMigrateVdi)
}
this.setState({
doNotMigrateVdis,
host,
intraPool,
mapVdisSrs: doNotMigrateVdis ? undefined : mapValues(vdis, vdi => defaultSr),
mapVifsNetworks: undefined,
migrationNetwork: undefined
})
return
}
// Inter-pool
const { networks, pifs, vifs } = this.props
const defaultMigrationNetworkId = find(pifs, pif => pif.$host === host.id && pif.management).$network
const defaultNetwork = invoke(() => {
// First PIF with an IP.
@@ -144,8 +209,9 @@ export default class MigrateVmModalBody extends BaseComponent {
})
this.setState({
doNotMigrateVdis: false,
host,
intraPool: this.props.vm.$pool === host.$pool,
intraPool,
mapVdisSrs: mapValues(vdis, vdi => defaultSr),
mapVifsNetworks: defaultNetworksForVif,
migrationNetworkId: defaultMigrationNetworkId
@@ -157,6 +223,7 @@ export default class MigrateVmModalBody extends BaseComponent {
render () {
const { vdis, vifs, networks } = this.props
const {
doNotMigrateVdis,
host,
intraPool,
mapVdisSrs,
@@ -176,6 +243,28 @@ export default class MigrateVmModalBody extends BaseComponent {
</Col>
</SingleLineRow>
</div>
{host && !doNotMigrateVdis && <div className={styles.groupBlock}>
<SingleLineRow>
<Col>{_('migrateVmSelectSrs')}</Col>
</SingleLineRow>
<br />
<SingleLineRow>
<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
onChange={sr => this.setState({ mapVdisSrs: { ...mapVdisSrs, [vdi.id]: sr.id } })}
predicate={this._getSrPredicate()}
value={mapVdisSrs[vdi.id]}
/>
</Col>
</SingleLineRow>
</div>)}
</div>}
{intraPool !== undefined &&
(!intraPool &&
<div>
@@ -185,34 +274,12 @@ export default class MigrateVmModalBody extends BaseComponent {
<Col size={6}>
<SelectNetwork
onChange={this._selectMigrationNetwork}
predicate={this._getNetworkPredicate()}
predicate={this._getMigrationNetworkPredicate()}
value={migrationNetworkId}
/>
</Col>
</SingleLineRow>
</div>
<div className={styles.groupBlock}>
<SingleLineRow>
<Col>{_('migrateVmSelectSrs')}</Col>
</SingleLineRow>
<br />
<SingleLineRow>
<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
onChange={sr => this.setState({ mapVdisSrs: { ...mapVdisSrs, [vdi.id]: sr.id } })}
predicate={this._getSrPredicate()}
value={mapVdisSrs[vdi.id]}
/>
</Col>
</SingleLineRow>
</div>)}
</div>
<div className={styles.groupBlock}>
<SingleLineRow>
<Col>{_('migrateVmSelectNetworks')}</Col>
@@ -228,7 +295,7 @@ export default class MigrateVmModalBody extends BaseComponent {
<Col size={6}>
<SelectNetwork
onChange={network => this.setState({ mapVifsNetworks: { ...mapVifsNetworks, [vif.id]: network.id } })}
predicate={this._getNetworkPredicate()}
predicate={this._getTargetNetworkPredicate()}
value={mapVifsNetworks[vif.id]}
/>
</Col>

View File

@@ -2,14 +2,16 @@ 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 Icon from 'icon'
import invoke from '../../invoke'
import SingleLineRow from '../../single-line-row'
import { Col } from '../../grid'
@@ -23,11 +25,14 @@ import {
connectStore
} from '../../utils'
import {
createFinder,
createGetObjectsOfType,
createPicker,
createSelector
createSelector,
getObject
} from '../../selectors'
import {
isSrShared
} from 'xo'
import { isSrWritable } from '../'
@@ -39,16 +44,7 @@ const LINE_STYLE = { paddingBottom: '1em' }
const getPools = createGetObjectsOfType('pool')
const getVms = createGetObjectsOfType('VM').pick(
(_, props) => props.vms
).filter(
[ vm => vm.power_state === 'Running' ]
)
const getNonRunningVm = createFinder(
createGetObjectsOfType('VM').pick(
(_, props) => props.vms
),
[ vm => vm.power_state !== 'Running' ]
(_, props) => props.vms
)
const getVbdsByVm = createGetObjectsOfType('VBD').pick(
@@ -67,7 +63,6 @@ const LINE_STYLE = { paddingBottom: '1em' }
return {
networks: getNetworks,
nonRunningVm: getNonRunningVm,
pifs: getPifs,
pools: getPools,
vbdsByVm: getVbdsByVm,
@@ -92,7 +87,26 @@ export default class MigrateVmsModalBody extends BaseComponent {
)
)
this._getNetworkPredicate = createSelector(
this._getTargetNetworkPredicate = createSelector(
createPicker(
() => this.props.pifs,
() => this.state.host.$PIFs
),
pifs => {
if (!pifs) {
return false
}
const networks = {}
forEach(pifs, pif => {
networks[pif.$network] = true
})
return network => networks[network.id]
}
)
this._getMigrationNetworkPredicate = createSelector(
createPicker(
() => this.props.pifs,
() => this.state.host.$PIFs
@@ -117,8 +131,8 @@ export default class MigrateVmsModalBody extends BaseComponent {
}
get value () {
const { vms } = this.props
const { host } = this.state
const vms = filter(this.props.vms, vm => vm.$container !== host.id)
if (!host || isEmpty(vms)) {
return { vms }
}
@@ -129,6 +143,9 @@ export default class MigrateVmsModalBody extends BaseComponent {
vifsByVm
} = this.props
const {
intraPool,
doNotMigrateVdi,
doNotMigrateVmVdis,
migrationNetworkId,
networkId,
smartVifMapping,
@@ -138,10 +155,14 @@ export default class MigrateVmsModalBody extends BaseComponent {
// Map VM --> ( Map VDI --> SR )
const mapVmsMapVdisSrs = {}
forEach(vbdsByVm, (vbds, vm) => {
if (doNotMigrateVmVdis[vm]) {
return
}
const mapVdisSrs = {}
forEach(vbds, vbd => {
if (!vbd.is_cd_drive && vbd.VDI) {
mapVdisSrs[vbd.VDI] = srId
const vdi = vbd.VDI
if (!vbd.is_cd_drive && vdi) {
mapVdisSrs[vdi] = intraPool && doNotMigrateVdi[vdi] ? this._getObject(vdi).SR : srId
}
})
mapVmsMapVdisSrs[vm] = mapVdisSrs
@@ -158,6 +179,9 @@ export default class MigrateVmsModalBody extends BaseComponent {
// 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
@@ -166,15 +190,25 @@ export default class MigrateVmsModalBody extends BaseComponent {
})
mapVmsMapVifsNetworks[vm.id] = mapVifsNetworks
})
// Map VM --> migration network
const mapVmsMigrationNetwork = mapValues(doNotMigrateVmVdis, doNotMigrateVdis =>
doNotMigrateVdis ? undefined : migrationNetworkId
)
return {
mapVmsMapVdisSrs,
mapVmsMapVifsNetworks,
migrationNetwork: migrationNetworkId,
mapVmsMigrationNetwork,
targetHost: host.id,
vms
}
}
_getObject (id) {
return getObject(store.getState(), id)
}
_selectHost = host => {
if (!host) {
this.setState({ targetHost: undefined })
@@ -183,11 +217,30 @@ export default class MigrateVmsModalBody extends BaseComponent {
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
})
@@ -198,19 +251,12 @@ export default class MigrateVmsModalBody extends BaseComponent {
_toggleSmartVifMapping = () => this.setState({ smartVifMapping: !this.state.smartVifMapping })
render () {
if (isEmpty(this.props.vms)) {
return <div>
<Icon icon='error' />
{' '}
{_('migrateVmBadPowerState')}
</div>
}
const { nonRunningVm } = this.props
const {
host,
intraPool,
migrationNetworkId,
networkId,
noVdisMigration,
smartVifMapping,
srId
} = this.state
@@ -234,17 +280,17 @@ export default class MigrateVmsModalBody extends BaseComponent {
<Col size={6}>
<SelectNetwork
onChange={this._selectMigrationNetwork}
predicate={this._getNetworkPredicate()}
predicate={this._getMigrationNetworkPredicate()}
value={migrationNetworkId}
/>
</Col>
</SingleLineRow>
</div>
}
{host && [
{host && (!intraPool || !noVdisMigration) &&
<div key='sr' style={LINE_STYLE}>
<SingleLineRow>
<Col size={6}>{_('migrateVmsSelectSr')}</Col>
<Col size={6}>{!intraPool ? _('migrateVmsSelectSr') : _('migrateVmsSelectSrIntraPool')}</Col>
<Col size={6}>
<SelectSr
onChange={this._selectSr}
@@ -253,7 +299,9 @@ export default class MigrateVmsModalBody extends BaseComponent {
/>
</Col>
</SingleLineRow>
</div>,
</div>
}
{host && !intraPool &&
<div key='network' style={LINE_STYLE}>
<SingleLineRow>
<Col size={6}>{_('migrateVmsSelectNetwork')}</Col>
@@ -261,7 +309,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
<SelectNetwork
disabled={smartVifMapping}
onChange={this._selectNetwork}
predicate={this._getNetworkPredicate()}
predicate={this._getTargetNetworkPredicate()}
value={networkId}
/>
</Col>
@@ -274,12 +322,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
</Col>
</SingleLineRow>
</div>
]}
{nonRunningVm && <div>
<Icon icon='error' />
{' '}
{_('migrateVmSomeBadPowerState', { vm: nonRunningVm.name_label })}
</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>
}
}

View File

@@ -0,0 +1,23 @@
import _ from 'intl'
import BaseComponent from 'base-component'
import React from 'react'
export default class RevertSnapshotModalBody extends BaseComponent {
state = { snapshotBefore: true }
get value () {
return this.state.snapshotBefore
}
render () {
return <div>
<div>{_('revertVmModalMessage')}</div>
<br />
<label>
<input type='checkbox' onChange={this.linkState('snapshotBefore')} checked={this.state.snapshotBefore} />
{' '}
{_('revertVmModalSnapshotBefore')}
</label>
</div>
}
}

View File

@@ -5,28 +5,37 @@ import Icon from './icon'
import Link from './link'
import propTypes from './prop-types'
import { Card, CardHeader, CardBlock } from './card'
import { getXoaPlan } from './utils'
import { connectStore, getXoaPlan } from './utils'
import { isAdmin } from 'selectors'
const Upgrade = propTypes({
available: propTypes.number.isRequired,
place: propTypes.string.isRequired
})(({
})(connectStore({
isAdmin
}))(({
available,
isAdmin,
place
}) => (
<Card>
<CardHeader>{_('upgradeNeeded')}</CardHeader>
<CardBlock className='text-xs-center'>
<p>{_('availableIn', {plan: getXoaPlan(available)})}</p>
<p>
<a href={`https://xen-orchestra.com/#!/pricing?pk_campaign=xoa_${getXoaPlan()}_upgrade&pk_kwd=${place}`} className='btn btn-primary btn-lg'>
<Icon icon='plan-upgrade' /> {_('upgradeNow')}
</a> {_('or')}&nbsp;
<Link className='btn btn-success btn-lg' to={'/xoa-update'}>
<Icon icon='plan-trial' /> {_('tryIt')}
</Link>
</p>
</CardBlock>
{isAdmin
? <CardBlock className='text-xs-center'>
<p>{_('availableIn', {plan: getXoaPlan(available)})}</p>
<p>
<a href={`https://xen-orchestra.com/#!/pricing?pk_campaign=xoa_${getXoaPlan()}_upgrade&pk_kwd=${place}`} className='btn btn-primary btn-lg'>
<Icon icon='plan-upgrade' /> {_('upgradeNow')}
</a> {_('or')}&nbsp;
<Link className='btn btn-success btn-lg' to={'/xoa-update'}>
<Icon icon='plan-trial' /> {_('tryIt')}
</Link>
</p>
</CardBlock>
: <CardBlock className='text-xs-center'>
<p>{_('notAvailable')}</p>
</CardBlock>
}
</Card>
))

View File

@@ -19,6 +19,10 @@
@extend .fa;
@extend .fa-tasks;
}
&-template {
@extend .fa;
@extend .fa-thumb-tack;
}
&-message {
@extend .fa;
@extend .fa-envelope-o;
@@ -92,6 +96,10 @@
@extend .fa;
@extend .fa-clipboard;
}
&-shortcuts {
@extend .fa;
@extend .fa-keyboard-o;
}
&-info {
@extend .fa;
@extend .fa-info-circle;
@@ -112,6 +120,14 @@
@extend .fa;
@extend .fa-play;
}
&-ssh-key {
@extend .fa;
@extend .fa-key;
}
&-ip {
@extend .fa;
@extend .fa-map-marker;
}
&-shown {
@extend .fa;
@@ -144,11 +160,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;
@@ -159,12 +175,24 @@
@extend .fa;
@extend .fa-link;
}
&-disconnect {
@extend .fa;
@extend .fa-chain-broken;
}
&-lock {
@extend .fa;
@extend .fa-lock;
}
&-unlock {
@extend .fa;
@extend .fa-unlock;
}
&-unknown-status {
@extend .fa;
@extend .fa-question-circle;
}
&-cpu {
@extend .fa;
@extend .fa-dashboard;
@@ -377,6 +405,10 @@
@extend .fa;
@extend .fa-trash;
}
&-migrate {
@extend .fa;
@extend .fa-share;
}
}
// Host
&-host {
@@ -496,6 +528,10 @@
@extend .fa;
@extend .fa-sort;
}
&-reset {
@extend .fa;
@extend .fa-undo;
}
&-save {
@extend .fa;
@extend .fa-floppy-o;
@@ -537,6 +573,10 @@
@extend .fa;
@extend .fa-clock-o;
}
&-time {
@extend .fa;
@extend .fa-clock-o;
}
&-database {
@extend .fa;
@extend .fa-database;
@@ -596,14 +636,6 @@
&-menu-self-service {
@extend .fa;
@extend .fa-cloud;
&-dashboard {
@extend .fa;
@extend .fa-dashboard;
}
&-admin {
@extend .fa;
@extend .fa-wrench;
}
}
&-menu-backup {
@extend .fa;
@@ -668,6 +700,10 @@
@extend .fa;
@extend .fa-puzzle-piece;
}
&-logs {
@extend .fa;
@extend .fa-list;
}
}
&-menu-about {
@extend .fa;
@@ -741,6 +777,10 @@
@extend .fa;
@extend .icon-debian;
}
&-docker {
@extend .fa;
@extend .icon-docker;
}
&-fedora {
@extend .fa;
@extend .icon-fedora;

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

@@ -30,10 +30,10 @@ $fa-font-path: "./";
// -------------------------------------------------------------------
@import "./chartist";
@import "./meter";
@import "./icons";
@import "./usage";
// ROOT STYLES =================================================================
@@ -72,17 +72,27 @@ $select-input-height: 40px; // Bootstrap input height
width: 100%;
}
.Select-value-label {
color: #373a3c;
}
.Select-control {
border-radius: unset;
}
// Disabled option style.
.Select-menu-outer {
.Select-option.is-disabled {
cursor: default;
font-weight: bold;
color: #777;
}
.Select-menu-outer .Select-option.is-disabled {
cursor: default;
font-weight: bold;
color: #777;
}
.Select-placeholder {
color: #999;
}
.Select--single > .Select-control .Select-value {
color: #333;
}
// COLORS ======================================================================
@@ -128,24 +138,6 @@ $select-input-height: 40px; // Bootstrap input height
text-align: center;
}
// TAG STYLE ===================================================================
.xo-tag {
vertical-align: middle;
background-color: #2598d9;
border-radius: 0.5em;
color: white;
padding: 0.3em;
margin: 0.2em;
margin-top: -0.1em;
font-size: 0.6em;
}
.add-tag-action {
font-size: 0.8em;
margin-left: 0.2em;
}
// GENERAL STYLES ==============================================================
.tag-ip {
@@ -202,35 +194,6 @@ $select-input-height: 40px; // Bootstrap input height
background: $gray-lighter;
}
// MEMORY/DISK BAR STYLE =======================================================
.progress-usage {
@extend .progress;
background-color: #eee;
height: 2em;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset;
margin-top: 1em;
margin-bottom: 2em;
}
.progress-dom0 {
display: inline-block;
background-color: #337ab7;
height: 2em;
}
.progress-object {
background-color: #5cb85c;
height: 2em;
margin-right: 0px;
display: inline-block;
transition: all 0.3s ease 0s;
}
.progress-object:hover {
opacity: 0.5;
}
// NOTIFICATIONS STYLE =========================================================
.notify-container {

29
src/keymap.js Normal file
View File

@@ -0,0 +1,29 @@
import _ from 'intl'
import mapValues from 'lodash/mapValues'
const keymap = {
XoApp: {
GO_TO_HOSTS: 'g h',
GO_TO_POOLS: 'g p',
GO_TO_VMS: 'g v',
CREATE_VM: 'c v',
UNFOCUS: 'esc',
HELP: ['?', 'h']
},
Home: {
SEARCH: '/',
NAV_DOWN: 'j',
NAV_UP: 'k',
SELECT: 'x',
JUMP_INTO: 'enter'
}
}
export { keymap as default }
export const help = mapValues(keymap, (shortcuts, contextLabel) => ({
name: _(`shortcut_${contextLabel}`),
shortcuts: mapValues(shortcuts, (shortcut, label) => ({
keys: shortcuts[label],
message: _(`shortcut_${label}`)
}))
}))

View File

@@ -6,10 +6,6 @@
// error for usage > 90%
meter {
/* Reset the default appearance */
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
/* For Firefox */
background: #EEE;
box-shadow: 0 2px 3px rgba(0,0,0,0.2) inset;

62
src/usage.scss Normal file
View File

@@ -0,0 +1,62 @@
// Usage
.usage {
@extend .progress;
background-color: #eee;
height: 2em;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset;
margin-top: 1em;
margin-bottom: 2em;
}
.usage-element {
background-color: #5cb85c;
box-shadow: -1px 0 0 0 white;
height: 2em;
display: inline-block;
transition: all 0.3s ease 0s;
}
.usage-element-highlight {
background-color: $brand-primary;
}
.usage-element-others {
background-color: $brand-info;
}
.usage-element:hover {
opacity: 0.6;
}
// Limits
.limits {
@extend .progress;
background-color: #eee;
height: 1.1em;
width: 100%;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset;
}
.limits-element {
background-color: #5cb85c;
height: 100%;
display: inline-block;
transition: all 0.3s ease 0s;
}
.limits-used {
@extend .limits-element;
background-color: $brand-primary;
}
.limits-to-be-used {
@extend .limits-element;
background-color: $brand-success;
}
.limits-over-used {
@extend .limits-element;
background-color: $brand-danger;
}

View File

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

@@ -1,3 +1,4 @@
import _ from 'intl'
import Component from 'base-component'
import React from 'react'
import { getJob, getSchedule } from 'xo'
@@ -23,7 +24,7 @@ export default class Edit extends Component {
const { job, schedule } = this.state
if (!job || !schedule) {
return <h1>Loading</h1>
return <h1>{_('statusLoading')}</h1>
}
return <New job={job} schedule={schedule} />

View File

@@ -1 +0,0 @@
export const getJobValues = job => job.values || job.items

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

@@ -1,27 +1,78 @@
import _, { messages } from 'intl'
import _ from 'intl'
import ActionButton from 'action-button'
import Component from 'base-component'
import delay from 'lodash/delay'
import forEach from 'lodash/forEach'
import GenericInput from 'json-schema-input'
import Icon from 'icon'
import map from 'lodash/map'
import React from 'react'
import Scheduler, { SchedulePreview } from 'scheduling'
import startsWith from 'lodash/startsWith'
import Upgrade from 'xoa-upgrade'
import Wizard, { Section } from 'wizard'
import { Container } from 'grid'
import { Container, Row, Col } from 'grid'
import { error } from 'notification'
import { generateUiSchema } from 'xo-json-schema-input'
import { injectIntl } from 'react-intl'
import { confirm } from 'modal'
import {
createJob,
createSchedule,
getRemote,
setJob,
setSchedule
updateSchedule
} from 'xo'
import { getJobValues } from '../helpers'
// ===================================================================
const NO_SMART_SCHEMA = {
type: 'object',
properties: {
vms: {
type: 'array',
items: {
type: 'string',
'xo:type': 'vm'
},
title: 'VMs',
description: 'Choose VMs to backup.'
}
},
required: [ 'vms' ]
}
const NO_SMART_UI_SCHEMA = generateUiSchema(NO_SMART_SCHEMA)
const SMART_SCHEMA = {
type: 'object',
properties: {
status: {
default: 'All',
enum: [ 'All', 'Running', 'Halted' ],
title: 'VMs statuses',
description: 'The statuses of VMs to backup.'
},
pools: {
type: 'array',
items: {
type: 'string',
'xo:type': 'pool'
},
title: 'Resident on'
},
tags: {
type: 'array',
items: {
type: 'string',
'xo:type': 'tag'
},
title: 'VMs Tags',
description: 'VMs which contains at least one of these tags. Not used if empty.'
}
},
required: [ 'status', 'pools' ]
}
const SMART_UI_SCHEMA = generateUiSchema(SMART_SCHEMA)
// ===================================================================
@@ -33,15 +84,6 @@ const COMMON_SCHEMA = {
title: 'Tag',
description: 'Back-up tag.'
},
vms: {
type: 'array',
items: {
type: 'string',
'xo:type': 'vm'
},
title: 'VMs',
description: 'Choose VMs to backup.'
},
_reportWhen: {
enum: [ 'never', 'always', 'failure' ],
title: 'Report',
@@ -189,13 +231,12 @@ const BACKUP_METHOD_TO_INFO = {
// ===================================================================
@injectIntl
const DEFAULT_CRON_PATTERN = '0 0 * * *'
export default class New extends Component {
constructor (props) {
super(props)
const { state } = this
state.cronPattern = '* * * * *'
this.state.cronPattern = DEFAULT_CRON_PATTERN
}
componentWillMount () {
@@ -208,53 +249,114 @@ 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
}
_populateForm = (job) => {
let values = getJobValues(job.paramsVector)
const { backupInput } = this.refs
_populateForm = job => {
let values = job.paramsVector.items
const {
backupInput,
vmsInput
} = this.refs
if (values.length === 1) {
// Older versions of XenOrchestra uses only values[0].
values = getJobValues(values[0])
const array = values[0].values
const config = array[0]
const reportWhen = config._reportWhen
backupInput.value = {
...values[0],
vms: map(values, value => value.id)
...config,
_reportWhen:
// Fix old reportWhen values...
(reportWhen === 'fail' && 'failure') ||
(reportWhen === 'alway' && 'always') ||
reportWhen
}
vmsInput.value = { vms: map(array, ({ id, vm }) => id || vm) }
} else {
backupInput.value = {
...getJobValues(values[1])[0],
vms: getJobValues(values[0])
if (values[1].type === 'map') {
// Smart backup.
const {
$pool: { __or: pools },
tags: { __or: tags } = {},
power_state: status = 'All'
} = values[1].collection.pattern
backupInput.value = values[0].values[0]
this.setState({
smartBackupMode: true
}, () => {
vmsInput.value = {
pools,
status,
tags: map(tags, tag => tag[0])
}
})
} else {
// Normal backup.
backupInput.value = values[1].values[0]
vmsInput.value = { vms: values[0].values }
}
}
}
_handleSubmit = () => {
const backup = this.refs.backupInput.value
_handleSubmit = async () => {
const {
vms,
enabled,
...callArgs
} = backup
} = this.refs.backupInput.value
const vmsInputValue = this.refs.vmsInput.value
const { backupInfo } = this.state
const job = {
type: 'call',
key: backupInfo.jobKey,
method: backupInfo.method,
paramsVector: {
const {
backupInfo,
smartBackupMode,
timezone
} = this.state
const paramsVector = !smartBackupMode
? {
type: 'crossProduct',
items: [{
type: 'set',
values: map(vms, vm => ({ id: vm }))
values: map(vmsInputValue.vms, vm => ({ id: vm }))
}, {
type: 'set',
values: [ callArgs ]
}]
} : {
type: 'crossProduct',
items: [{
type: 'set',
values: [ callArgs ]
}, {
type: 'map',
collection: {
type: 'fetchObjects',
pattern: {
$pool: !vmsInputValue.pools.length ? undefined : { __or: vmsInputValue.pools },
power_state: vmsInputValue.status === 'All' ? undefined : vmsInputValue.status,
tags: !vmsInputValue.tags.length ? undefined : { __or: map(vmsInputValue.tags, tag => [ tag ]) },
type: 'VM'
}
},
iteratee: {
type: 'extractProperties',
mapping: { id: 'id' }
}
}]
}
const job = {
type: 'call',
key: backupInfo.jobKey,
method: backupInfo.method,
paramsVector
}
// Update backup schedule.
@@ -262,33 +364,62 @@ 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
}))
}
let remoteId
if (job.type === 'call') {
const { paramsVector } = job
if (paramsVector.type === 'crossProduct') {
const { items } = paramsVector
forEach(items, item => {
if (item.type === 'set') {
forEach(item.values, value => {
if (value.remoteId) {
remoteId = value.remoteId
return false
}
})
if (remoteId) {
return false
}
}
})
}
}
if (remoteId) {
const remote = await getRemote(remoteId)
if (startsWith(remote.url, 'file:')) {
await confirm({
title: _('localRemoteWarningTitle'),
body: _('localRemoteWarningMessage')
})
}
}
// Create backup schedule.
return createJob(job).then(jobId => {
createSchedule(jobId, this.state.cronPattern, enabled)
})
return createSchedule(await createJob(job), { 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 => {
@@ -297,68 +428,126 @@ export default class New extends Component {
})
}
_handleSmartBackupMode = event => {
this.setState({
smartBackupMode: event.target.value === 'smart'
})
}
render () {
const { backupInfo, defaultValue } = this.state
const { formatMessage } = this.props.intl
const {
backupInfo,
cronPattern,
smartBackupMode,
timezone
} = this.state
return process.env.XOA_PLAN > 1
? (
<Wizard>
<Section icon='backup' title={this.props.job ? 'editVmBackup' : 'newVmBackup'}>
<fieldset className='form-group'>
<label htmlFor='selectBackup'>{_('newBackupSelection')}</label>
<select
className='form-control'
defaultValue={(backupInfo && backupInfo.method) || null}
id='selectBackup'
onChange={this._handleBackupSelection}
required
>
<option value={null}>{formatMessage(messages.noSelectedValue)}</option>
{map(BACKUP_METHOD_TO_INFO, (info, key) =>
<option key={key} value={key}>{formatMessage(messages[info.label])}</option>
)}
</select>
</fieldset>
<form className='card-block' id='form-new-vm-backup'>
{backupInfo &&
<GenericInput
defaultValue={defaultValue}
label={<span><Icon icon={backupInfo.icon} /> {formatMessage(messages[backupInfo.label])}</span>}
required
schema={backupInfo.schema}
uiSchema={backupInfo.uiSchema}
ref='backupInput'
/>
}
</form>
<Container>
<Row>
<Col>
<fieldset className='form-group'>
<label htmlFor='selectBackup'>{_('newBackupSelection')}</label>
<select
className='form-control'
value={(backupInfo && backupInfo.method) || ''}
id='selectBackup'
onChange={this._handleBackupSelection}
required
>
{_('noSelectedValue', message => <option value=''>{message}</option>)}
{map(BACKUP_METHOD_TO_INFO, (info, key) =>
_(info.label, message => <option key={key} value={key}>{message}</option>)
)}
</select>
</fieldset>
<form id='form-new-vm-backup'>
{backupInfo && (
<div>
<GenericInput
label={<span><Icon icon={backupInfo.icon} /> {_(backupInfo.label)}</span>}
ref='backupInput'
required
schema={backupInfo.schema}
uiSchema={backupInfo.uiSchema}
/>
<fieldset className='form-group'>
<label htmlFor='smartMode'>{_('smartBackupModeSelection')}</label>
<select
className='form-control'
id='smartMode'
onChange={this._handleSmartBackupMode}
required
value={smartBackupMode ? 'smart' : 'normal'}
>
{_('normalBackup', message => <option value='normal'>{message}</option>)}
{_('smartBackup', message => <option value='smart'>{message}</option>)}
</select>
</fieldset>
{smartBackupMode
? (process.env.XOA_PLAN > 2
? <GenericInput
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
ref='vmsInput'
required
schema={SMART_SCHEMA}
uiSchema={SMART_UI_SCHEMA}
/>
: <Container><Upgrade place='newBackup' available={3} /></Container>
) : <GenericInput
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
ref='vmsInput'
required
schema={NO_SMART_SCHEMA}
uiSchema={NO_SMART_UI_SCHEMA}
/>
}
</div>
)}
</form>
</Col>
</Row>
</Container>
</Section>
<Section icon='schedule' title='schedule'>
<Scheduler ref='scheduler' onChange={this._updateCronPattern} />
<Scheduler
cronPattern={cronPattern}
onChange={this._updateCronPattern}
timezone={timezone}
/>
</Section>
<Section icon='preview' title='preview' summary>
<div className='card-block'>
<SchedulePreview cron={this.state.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'>
<ActionButton
btnStyle='primary'
className='btn-lg m-r-1'
disabled={!backupInfo}
form='form-new-vm-backup'
handler={this._handleSubmit}
icon='save'
redirectOnSuccess='/backup/overview'
>
{_('saveBackupJob')}
</ActionButton>
<button type='button' className='btn btn-lg btn-secondary' onClick={this._handleReset}>
{_('selectTableReset')}
</button>
</fieldset>
}
</div>
<Container>
<Row>
<Col>
<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]} />
: (smartBackupMode && process.env.XOA_PLAN < 3
? <Upgrade place='newBackup' available={3} />
: <fieldset className='pull-xs-right p-t-1'>
<ActionButton
btnStyle='primary'
className='btn-lg m-r-1'
disabled={!backupInfo}
form='form-new-vm-backup'
handler={this._handleSubmit}
icon='save'
redirectOnSuccess='/backup/overview'
>
{_('saveBackupJob')}
</ActionButton>
<button type='button' className='btn btn-lg btn-secondary' onClick={this._handleReset}>
{_('selectTableReset')}
</button>
</fieldset>)
}
</Col>
</Row>
</Container>
</Section>
</Wizard>
)

View File

@@ -25,8 +25,6 @@ import {
subscribeScheduleTable
} from 'xo'
import { getJobValues } from '../helpers'
// ===================================================================
const jobKeyToLabel = {
@@ -95,12 +93,12 @@ export default class Overview extends Component {
_getScheduleTag (schedule, job = {}) {
try {
const { paramsVector } = job
const values = getJobValues(paramsVector)
const values = paramsVector.items
// Old versions of XenOrchestra uses values[0]
return (
getJobValues(values[0])[0].tag ||
getJobValues(values[1])[0].tag
values[0].values[0].tag ||
values[1].values[0].tag
)
} catch (_) {}
@@ -136,7 +134,7 @@ export default class Overview extends Component {
<div>
<Card>
<CardHeader>
<h5><Icon icon='schedule' /> Schedules</h5>
<h5><Icon icon='schedule' /> {_('backupSchedules')}</h5>
</CardHeader>
<CardBlock>
{schedules.length ? (
@@ -146,6 +144,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>
@@ -155,9 +154,10 @@ export default class Overview extends Component {
return (
<tr key={key}>
<td>{this._getJobLabel(job)}</td>
<td>{job.id} ({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

@@ -36,9 +36,9 @@ const parseDate = date => +moment(date, 'YYYYMMDDTHHmmssZ').format('x')
const isEmptyRemote = remote => !remote.backupInfoByVm || !size(remote.backupInfoByVm)
const backupOptionRenderer = backup => <span>
{backup.type === 'delta' && <span><span className='tag tag-info'>{_('delta')}</span>{' '}</span>}
{backup.tag}
{' '}
{backup.type === 'delta' && <span><span className='tag tag-info'>{_('delta')}</span>{' '}</span>}
{backup.tag}
{' '}
<FormattedDate value={new Date(backup.date)} month='long' day='numeric' year='numeric' hour='2-digit' minute='2-digit' second='2-digit' />
</span>
@@ -137,7 +137,7 @@ export default class Restore extends Component {
{r.enabled && <span className='tag tag-success'>{_('remoteEnabled')}</span>}
{r.error && <span className='tag tag-danger'>{_('remoteError')}</span>}
<span className='pull-right'>
<ActionButton disabled={!r.enabled} icon='refresh' btnStyle='default' handler={this._list} handlerParam={r.id} />
<Tooltip content={_('displayBackup')}><ActionButton disabled={!r.enabled} icon='refresh' btnStyle='default' handler={this._list} handlerParam={r.id} /></Tooltip>
</span>
{r.backupInfoByVm && <div>
<br />
@@ -195,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'),

View File

@@ -2,9 +2,11 @@ import _ from 'intl'
import ActionRowButton from 'action-row-button'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import Link from 'link'
import map from 'lodash/map'
import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import Tooltip from 'tooltip'
import Upgrade from 'xoa-upgrade'
import React, { Component } from 'react'
import { Card, CardHeader, CardBlock } from 'card'
@@ -25,7 +27,7 @@ import {
const SrColContainer = connectStore(() => ({
container: createGetObject()
}))(({ container }) => <span>{container.name_label}</span>)
}))(({ container }) => <Link to={`pools/${container.id}`}>{container.name_label}</Link>)
const VdiColSr = connectStore(() => ({
sr: createGetObject()
@@ -64,9 +66,14 @@ const SR_COLUMNS = [
sortCriteria: sr => sr.size
},
{
default: true,
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
itemRenderer: sr => sr.size > 1 &&
<Tooltip content={_('spaceLeftTooltip', {used: Math.round((sr.physical_usage / sr.size) * 100), free: formatSize(sr.size - sr.physical_usage)})}>
<meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90' />
</Tooltip>,
sortCriteria: sr => sr.physical_usage / sr.size,
sortOrder: 'desc'
}
]
@@ -74,7 +81,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'),
@@ -112,7 +120,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'),
@@ -147,7 +156,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'),
@@ -226,6 +236,8 @@ export default class Health extends Component {
)
)
_getSrUrl = sr => `srs/${sr.id}`
render () {
return process.env.XOA_PLAN > 3
? <Container>
@@ -240,7 +252,11 @@ export default class Health extends Component {
? <p className='text-xs-center'>{_('noSrs')}</p>
: <Row>
<Col>
<SortedTable collection={this.props.userSrs} columns={SR_COLUMNS} defaultColumn={4} />
<SortedTable
collection={this.props.userSrs}
columns={SR_COLUMNS}
rowLink={this._getSrUrl}
/>
</Col>
</Row>
}

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

@@ -1,22 +1,21 @@
import _ from 'intl'
import ActionButton from 'action-button'
import ChartistGraph from 'react-chartist'
import Component from 'base-component'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import Link from 'link'
import propTypes from 'prop-types'
import Link, { BlockLink } from 'link'
import map from 'lodash/map'
import Upgrade from 'xoa-upgrade'
import HostsPatchesTable from 'hosts-patches-table'
import React from 'react'
import SortedTable from 'sorted-table'
import size from 'lodash/size'
import Upgrade from 'xoa-upgrade'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { Card, CardBlock, CardHeader } from 'card'
import { Container, Row, Col } from 'grid'
import {
createCollectionWrapper,
createCounter,
createFilter,
createGetObjectsOfType,
createGetHostMetrics,
createSelector,
@@ -27,8 +26,6 @@ import {
formatSize
} from 'utils'
import {
getHostMissingPatches,
installAllHostPatches,
isSrWritable,
subscribeUsers
} from 'xo'
@@ -37,149 +34,26 @@ import styles from './index.css'
// ===================================================================
const MISSING_PATCHES_COLUMNS = [
{
name: _('srPool'),
itemRenderer: (host, { pools }) => pools[host.$pool].name_label,
sortCriteria: (host, { pools }) => pools[host.$pool].name_label
},
{
name: _('srHost'),
itemRenderer: host => host.name_label,
sortCriteria: host => host.name_label
},
{
name: _('hostDescription'),
itemRenderer: host => host.name_description,
sortCriteria: host => host.name_description
},
{
name: _('hostMissingPatches'),
itemRenderer: (host, { missingPatches }) => missingPatches[host.id],
sortCriteria: (host, { missingPatches }) => missingPatches[host.id]
},
{
name: _('patchUpdateButton'),
itemRenderer: (host, { installAllHostPatches }) => (
<ActionButton
btnStyle='primary'
handler={installAllHostPatches}
handlerParam={host}
icon='host-patch-update'
/>
)
}
]
// ===================================================================
@connectStore(() => {
const getPools = createGetObjectsOfType('pool')
const getHosts = createGetObjectsOfType('host').sort()
return {
pools: getPools,
hosts: getHosts
}
@propTypes({
hosts: propTypes.object.isRequired
})
class MissingPatchesPanel extends Component {
constructor (props) {
super(props)
this.state.missingPatches = {}
}
_getHosts = createFilter(
() => this.props.hosts,
() => this.state.missingPatches,
[ (host, missingPatches) => 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()
}
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)
})
}
class PatchesCard extends Component {
_getContainer = () => this.refs.container
render () {
const hosts = this._getHosts()
const noPatches = isEmpty(hosts)
return (
<Card>
<CardHeader>
<Icon icon='host-patch-update' /> {_('update')}
<ButtonGroup className='pull-right'>
<ActionButton
btnStyle='secondary'
handler={this._refreshMissingPatches}
icon='refresh'
/>
<ActionButton
btnStyle='primary'
disabled={noPatches}
handler={this._installAllMissingPatches}
icon='host-patch-update'
/>
</ButtonGroup>
<div ref='container' className='pull-right' />
</CardHeader>
<CardBlock>
{!noPatches
? (
<SortedTable
collection={hosts}
columns={MISSING_PATCHES_COLUMNS}
userData={{
installAllHostPatches: this._installAllHostPatches,
missingPatches: this.state.missingPatches,
pools: this.props.pools
}}
/>
) : <p>{_('patchNothing')}</p>
}
<HostsPatchesTable
buttonsGroupContainer={this._getContainer}
container={ButtonGroup}
displayPools
hosts={this.props.hosts}
/>
</CardBlock>
</Card>
)
@@ -261,6 +135,7 @@ class MissingPatchesPanel extends Component {
return {
hostMetrics: getHostMetrics,
hosts: getHosts,
nAlarmMessages: getNumberOfAlarmMessages,
nHosts: getNumberOfHosts,
nPools: getNumberOfPools,
@@ -278,9 +153,9 @@ 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>
@@ -288,11 +163,11 @@ export default class Overview extends Component {
<Col mediumSize={4}>
<Card>
<CardHeader>
<Icon icon='pool' /> {_('poolPanel', { pools: this.props.nPools })}
<Icon icon='pool' /> {_('poolPanel', { pools: props.nPools })}
</CardHeader>
<CardBlock>
<p className={styles.bigCardContent}>
<Link to='/home?t=pool'>{this.props.nPools}</Link>
<Link to='/home?t=pool'>{props.nPools}</Link>
</p>
</CardBlock>
</Card>
@@ -300,11 +175,11 @@ export default class Overview extends Component {
<Col mediumSize={4}>
<Card>
<CardHeader>
<Icon icon='host' /> {_('hostPanel', { hosts: this.props.nHosts })}
<Icon icon='host' /> {_('hostPanel', { hosts: props.nHosts })}
</CardHeader>
<CardBlock>
<p className={styles.bigCardContent}>
<Link to='/home?t=host'>{this.props.nHosts}</Link>
<Link to='/home?t=host'>{props.nHosts}</Link>
</p>
</CardBlock>
</Card>
@@ -312,11 +187,11 @@ export default class Overview extends Component {
<Col mediumSize={4}>
<Card>
<CardHeader>
<Icon icon='vm' /> {_('vmPanel', { vms: this.props.nVms })}
<Icon icon='vm' /> {_('vmPanel', { vms: props.nVms })}
</CardHeader>
<CardBlock>
<p className={styles.bigCardContent}>
<Link to='/home?t=VM'>{this.props.nVms}</Link>
<Link to='/home?s=&t=VM'>{props.nVms}</Link>
</p>
</CardBlock>
</Card>
@@ -332,11 +207,17 @@ export default class Overview extends Component {
<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>
type='Pie'
/>
<p className='text-xs-center'>
{_('ofUsage', {
total: formatSize(props.hostMetrics.memoryTotal),
usage: formatSize(props.hostMetrics.memoryUsage)
})}
</p>
</CardBlock>
</Card>
</Col>
@@ -350,11 +231,17 @@ export default class Overview extends Component {
<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>
</CardBlock>
</Card>
@@ -366,14 +253,22 @@ export default class Overview extends Component {
</CardHeader>
<CardBlock>
<div className='ct-chart'>
<ChartistGraph
data={{
labels: ['Used Space', 'Total Space'],
series: [this.props.srMetrics.srUsage, this.props.srMetrics.srTotal - this.props.srMetrics.srUsage]
}}
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>
<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.srTotal),
usage: formatSize(props.srMetrics.srUsage)
})}
</p>
</BlockLink>
</div>
</CardBlock>
</Card>
@@ -386,8 +281,8 @@ export default class Overview extends Component {
<Icon icon='alarm' /> {_('alarmMessage')}
</CardHeader>
<CardBlock>
<p className={`${styles.bigCardContent} ${this.props.nAlarmMessages > 0 ? 'text-warning' : ''}`}>
{this.props.nAlarmMessages}
<p className={styles.bigCardContent}>
<Link to='/dashboard/health' className={props.nAlarmMessages > 0 ? 'text-warning' : ''}>{props.nAlarmMessages}</Link>
</p>
</CardBlock>
</Card>
@@ -398,7 +293,9 @@ export default class Overview extends Component {
<Icon icon='task' /> {_('taskStatePanel')}
</CardHeader>
<CardBlock>
<p className={styles.bigCardContent}>{this.props.nTasks}</p>
<p className={styles.bigCardContent}>
<Link to='/tasks'>{props.nTasks}</Link>
</p>
</CardBlock>
</Card>
</Col>
@@ -408,7 +305,9 @@ export default class Overview extends Component {
<Icon icon='user' /> {_('usersStatePanel')}
</CardHeader>
<CardBlock>
<p className={styles.bigCardContent}>{nUsers}</p>
<p className={styles.bigCardContent}>
<Link to='/settings/users'>{nUsers}</Link>
</p>
</CardBlock>
</Card>
</Col>
@@ -420,14 +319,19 @@ export default class Overview extends Component {
<Icon icon='vm-force-shutdown' /> {_('vmStatePanel')}
</CardHeader>
<CardBlock>
<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>
<BlockLink to='/home?t=VM'>
<ChartistGraph
data={{
labels: ['Running', 'Halted', 'Other'],
series: [props.vmMetrics.running, props.vmMetrics.halted, props.vmMetrics.other]
}}
options={{ showLabel: false }}
type='Pie'
/>
<p className='text-xs-center'>
{_('vmsStates', { running: props.vmMetrics.running, halted: props.vmMetrics.halted })}
</p>
</BlockLink>
</CardBlock>
</Card>
</Col>
@@ -437,21 +341,24 @@ export default class Overview extends Component {
<Icon icon='disk' /> {_('srTopUsageStatePanel')}
</CardHeader>
<CardBlock>
<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' />
<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>
<MissingPatchesPanel />
<PatchesCard hosts={props.hosts} />
</Col>
</Row>
</Container>

View File

@@ -14,6 +14,7 @@ import { Text } from 'editable'
import {
addTag,
editHost,
fetchHostStats,
removeTag,
startHost,
stopHost
@@ -24,14 +25,62 @@ import {
osFamily
} from 'utils'
import {
createDoesHostNeedRestart,
createGetObject
} from 'selectors'
import {
CpuSparkLines,
LoadSparkLines,
PifSparkLines
} from 'xo-sparklines'
import styles from './index.css'
@connectStore({
container: createGetObject((_, props) => props.item.$pool)
})
const MINI_STATS_PROPS = {
height: 10,
strokeWidth: 0.2,
width: 50
}
class MiniStats extends Component {
_fetch = () => {
fetchHostStats(this.props.hostId).then(stats => {
this.setState({ stats })
})
}
componentWillMount () {
this._fetch()
this.subscriptionId = setInterval(this._fetch, 5e3)
}
componentWillUnmount () {
clearInterval(this.subscriptionId)
}
render () {
const { stats } = this.state
if (!stats) {
return <Icon icon='loading' />
}
return <Row>
<Col mediumSize={4} className={styles.itemExpanded}>
<CpuSparkLines data={stats} {...MINI_STATS_PROPS} />
</Col>
<Col mediumSize={4} className={styles.itemExpanded}>
<PifSparkLines data={stats} {...MINI_STATS_PROPS} />
</Col>
<Col mediumSize={4} className={styles.itemExpanded}>
<LoadSparkLines data={stats} {...MINI_STATS_PROPS} />
</Col>
</Row>
}
}
@connectStore(({
container: createGetObject((_, props) => props.item.$pool),
needsRestart: createDoesHostNeedRestart((_, props) => props.item)
}))
export default class HostItem extends Component {
get _isRunning () {
const host = this.props.item
@@ -71,9 +120,13 @@ export default class HostItem extends Component {
<Ellipsis>
<Text value={host.name_label} onChange={this._setNameLabel} useLongClick />
</Ellipsis>
&nbsp;
{container && host.id === container.master && <span className='tag tag-pill tag-info'>{_('pillMaster')}</span>}
&nbsp;
{this.props.needsRestart && <Tooltip content={_('rebootUpdateHostLabel')}><Link to={`/hosts/${host.id}/patches`}><Icon icon='alarm' /></Link></Tooltip>}
</EllipsisContainer>
</Col>
<Col mediumSize={4} className='hidden-md-down'>
<Col mediumSize={3} className='hidden-lg-down'>
<EllipsisContainer>
<span className={styles.itemActionButons}>
{this._isRunning
@@ -100,20 +153,20 @@ export default class HostItem extends Component {
</Ellipsis>
</EllipsisContainer>
</Col>
<Col largeSize={2} className='hidden-lg-down'>
<Col largeSize={2} className='hidden-md-down'>
<span>
{host.cpus.cores}x <Icon icon='cpu' />
{' '}
{formatSize(host.memory.size)}
<Tooltip content={_('memoryLeftTooltip', {used: Math.round((host.memory.usage / host.memory.size) * 100), free: formatSize(host.memory.size - host.memory.usage)})}>
<progress style={{margin: 0}} className='progress' value={host.memory.usage / host.memory.size * 100} max='100' />
</Tooltip>
</span>
</Col>
<Col largeSize={2} className='hidden-lg-down'>
<span className='tag tag-info tag-ip'>{host.address}</span>
</Col>
<Col mediumSize={2} className='hidden-sm-down'>
{container && <Col mediumSize={2} className='hidden-sm-down'>
<Link to={`/${container.type}s/${container.id}`}>{container.name_label}</Link>
</Col>
<Col mediumSize={1} className={styles.itemExpandRow}>
</Col>}
<Col mediumSize={1} offset={container ? undefined : 2} className={styles.itemExpandRow}>
<a className={styles.itemExpandButton}
onClick={this._toggleExpanded}>
<Icon icon='nav' fixedWidth />&nbsp;&nbsp;&nbsp;
@@ -123,16 +176,16 @@ export default class HostItem extends Component {
</BlockLink>
{(this.state.expanded || expandAll) &&
<Row>
<Col mediumSize={4} className={styles.itemExpanded}>
<Col mediumSize={6} className={styles.itemExpanded}>
<MiniStats hostId={this.props.item} />
</Col>
<Col mediumSize={2} className={styles.itemExpanded} style={{ marginTop: '0.3rem' }}>
<span>
{host.cpus.cores}x <Icon icon='cpu' />
{' '}&nbsp;{' '}
{formatSize(host.memory.size)} <Icon icon='memory' />
</span>
</Col>
<Col mediumSize={4} className={styles.itemExpanded}>
<span className='tag tag-info tag-ip'>{host.address}</span>
</Col>
<Col mediumSize={4}>
<span style={{fontSize: '1.4em'}}>
<Tags labels={host.tags} onDelete={this._removeTag} onAdd={this._addTag} />

View File

@@ -17,6 +17,7 @@
.item {
padding: 0.5em;
border-bottom: 1px solid #eee;
white-space: nowrap;
}
.item:hover {
@@ -38,10 +39,8 @@
}
.itemExpanded {
padding-top: 0.4em;
color: #999;
font-size: 1em;
overflow: hidden;
text-overflow: ellipsis;
white-space:nowrap;
}
@@ -58,3 +57,7 @@
.selectObject {
width: 20em;
}
.highlight {
outline: 2px solid #366e98;
}

View File

@@ -1,4 +1,5 @@
import * as ComplexMatcher from 'complex-matcher'
import * as homeFilters from 'home-filters'
import _ from 'intl'
import ActionButton from 'action-button'
import ceil from 'lodash/ceil'
@@ -8,23 +9,34 @@ import debounce from 'lodash/debounce'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import invoke from 'invoke'
import keys from 'lodash/keys'
import includes from 'lodash/includes'
import isEmpty from 'lodash/isEmpty'
import isString from 'lodash/isString'
import keys from 'lodash/keys'
import Link from 'link'
import map from 'lodash/map'
import Page from '../page'
import React from 'react'
import Shortcuts from 'shortcuts'
import SingleLineRow from 'single-line-row'
import size from 'lodash/size'
import Tooltip from 'tooltip'
import { Card, CardHeader, CardBlock } from 'card'
import {
addCustomFilter,
copyVms,
deleteTemplates,
deleteVms,
emergencyShutdownHosts,
migrateVms,
restartHosts,
restartHostsAgents,
restartVms,
snapshotVms,
startVms,
stopVms
stopHosts,
stopVms,
subscribeCurrentUser
} from 'xo'
import { Container, Row, Col } from 'grid'
import {
@@ -33,7 +45,10 @@ import {
SelectTag
} from 'select-objects'
import {
connectStore
addSubscriptions,
connectStore,
firstDefined,
noop
} from 'utils'
import {
areObjectsFetched,
@@ -57,46 +72,105 @@ import styles from './index.css'
import HostItem from './host-item'
import PoolItem from './pool-item'
import VmItem from './vm-item'
import TemplateItem from './template-item'
const ITEMS_PER_PAGE = 20
const OPTIONS = {
host: {
defaultFilter: 'power_state:running ',
filters: {
homeFilterRunningHosts: 'power_state:running ',
homeFilterTags: 'tags:'
},
Item: HostItem
filters: homeFilters.host,
mainActions: [
{ handler: stopHosts, icon: 'host-stop', tooltip: _('stopHostLabel') },
{ handler: restartHostsAgents, icon: 'host-restart-agent', tooltip: _('restartHostAgent') },
{ handler: emergencyShutdownHosts, icon: 'host-emergency-shutdown', tooltip: _('emergencyModeLabel') },
{ handler: restartHosts, icon: 'host-reboot', tooltip: _('rebootHostLabel') }
],
Item: HostItem,
showPoolsSelector: true,
sortOptions: [
{ labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
{ labelId: 'homeSortByPowerstate', sortBy: 'power_state', sortOrder: 'desc' },
{ labelId: 'homeSortByRAM', sortBy: 'memory.size', sortOrder: 'desc' },
{ labelId: 'homeSortByCpus', sortBy: 'CPUs.cpu_count', sortOrder: 'desc' }
]
},
VM: {
defaultFilter: 'power_state:running ',
filters: {
homeFilterPendingVms: 'current_operations:"" ',
homeFilterNonRunningVms: '!power_state:running ',
homeFilterHvmGuests: 'virtualizationMode:hvm ',
homeFilterRunningVms: 'power_state:running ',
homeFilterTags: 'tags:'
},
Item: VmItem
filters: homeFilters.VM,
mainActions: [
{ handler: stopVms, icon: 'vm-stop', tooltip: _('stopVmLabel') },
{ handler: startVms, icon: 'vm-start', tooltip: _('startVmLabel') },
{ handler: restartVms, icon: 'vm-reboot', tooltip: _('rebootVmLabel') },
{ handler: migrateVms, icon: 'vm-migrate', tooltip: _('migrateVmLabel') },
{ handler: copyVms, icon: 'vm-copy', tooltip: _('copyVmLabel') }
],
otherActions: [{
handler: restartVms,
icon: 'vm-force-reboot',
labelId: 'forceRebootVmLabel',
params: true
}, {
handler: stopVms,
icon: 'vm-force-shutdown',
labelId: 'forceShutdownVmLabel',
params: true
}, {
handler: snapshotVms,
icon: 'vm-snapshot',
labelId: 'snapshotVmLabel'
}, {
handler: deleteVms,
icon: 'vm-delete',
labelId: 'vmRemoveButton'
}],
Item: VmItem,
showPoolsSelector: true,
showHostsSelector: true,
sortOptions: [
{ labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
{ labelId: 'homeSortByPowerstate', sortBy: 'power_state', sortOrder: 'desc' },
{ labelId: 'homeSortByRAM', sortBy: 'memory.size', sortOrder: 'desc' },
{ labelId: 'homeSortByCpus', sortBy: 'CPUs.number', sortOrder: 'desc' }
]
},
pool: {
defaultFilter: '',
filters: {
homeFilterTags: 'tags:'
},
Item: PoolItem
filters: homeFilters.pool,
getActions: noop,
Item: PoolItem,
sortOptions: [
{ labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' }
]
},
'VM-template': {
defaultFilter: '',
filters: homeFilters.vmTemplate,
mainActions: [
{ handler: deleteTemplates, icon: 'delete', tooltip: _('templateDelete') }
],
Item: TemplateItem,
showPoolsSelector: true,
sortOptions: [
{ labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
{ labelId: 'homeSortByRAM', sortBy: 'memory.size', sortOrder: 'desc' },
{ labelId: 'homeSortByCpus', sortBy: 'CPUs.number', sortOrder: 'desc' }
]
}
}
const TYPES = {
VM: _('homeTypeVm'),
'VM-template': _('homeTypeVmTemplate'),
host: _('homeTypeHost'),
pool: _('homeTypePool')
}
const DEFAULT_TYPE = 'VM'
@addSubscriptions({
user: subscribeCurrentUser
})
@connectStore(() => {
const noServersConnected = invoke(
createGetObjectsOfType('host'),
@@ -129,6 +203,9 @@ export default class Home extends Component {
componentWillReceiveProps (props) {
this._initFilter(props)
if (props.type !== this.props.type) {
this.setState({ highlighted: undefined })
}
}
_getNumberOfItems = createCounter(() => this.props.items)
@@ -143,6 +220,33 @@ export default class Home extends Component {
pathname,
query: { ...query, t: type, s: undefined }
})
this.setState({ highlighted: undefined })
}
_getDefaultFilter (props = this.props) {
const { type, user } = props
const defaultFilter = OPTIONS[type].defaultFilter
// No user.
if (!user) {
return defaultFilter
}
const { defaultHomeFilters = {}, filters = {} } = user.preferences || {}
const filterName = defaultHomeFilters[type]
// No filter defined in preferences.
if (!filterName) {
return defaultFilter
}
// Filter defined.
return firstDefined(
homeFilters[type][filterName],
filters[type][filterName],
defaultFilter
)
}
_initFilter (props) {
@@ -150,7 +254,8 @@ export default class Home extends Component {
// If filter is null, set a default filter.
if (filter == null) {
const defaultFilter = OPTIONS[props.type].defaultFilter
const defaultFilter = this._getDefaultFilter(props)
if (defaultFilter != null) {
this._setFilter(defaultFilter, props)
}
@@ -174,7 +279,6 @@ export default class Home extends Component {
const { filterInput } = this.refs
if (filterInput && filterInput.value !== filter) {
filterInput.value = filter
filterInput.focus()
}
}
@@ -229,18 +333,14 @@ export default class Home extends Component {
_getVisibleItems = createPager(
this._getFilteredItems,
() => this.state.activePage || 1
() => this.state.activePage || 1,
ITEMS_PER_PAGE
)
_expandAll = () => this.setState({ expandAll: !this.state.expandAll })
_onPageSelection = (_, event) => { this.page = event.eventKey }
_sortByName = () => this.setState({ sortBy: 'name_label', sortOrder: 'asc' })
_sortByPowerState = () => this.setState({ sortBy: 'power_state', sortOrder: 'desc' })
_sortByRam = () => this.setState({ sortBy: 'memory.size', sortOrder: 'desc' })
_sortByVcpus = () => this.setState({ sortBy: 'CPUs.number', sortOrder: 'desc' })
_tick = isCriteria => <Icon icon={isCriteria ? 'success' : undefined} fixedWidth />
_updateSelectedPools = pools => {
@@ -311,37 +411,90 @@ export default class Home extends Component {
this._updateMasterCheckbox()
}
_addCustomFilter = () => {
return addCustomFilter(
this._getType(),
this._getFilter()
)
}
_getCustomFilters () {
const { preferences } = this.props.user || {}
if (!preferences) {
return
}
const customFilters = preferences.filters || {}
return customFilters[this._getType()]
}
_getShortcutsHandler = createSelector(
() => this._getVisibleItems(),
items => (command, event) => {
event.preventDefault()
switch (command) {
case 'SEARCH':
this.refs.filterInput.focus()
break
case 'NAV_DOWN':
this.setState({ highlighted: (this.state.highlighted + items.length + 1) % items.length || 0 })
break
case 'NAV_UP':
this.setState({ highlighted: (this.state.highlighted + items.length - 1) % items.length || 0 })
break
case 'SELECT':
this._selectItem(items[this.state.highlighted].id)
break
case 'JUMP_INTO':
const item = items[this.state.highlighted]
if (includes(['VM', 'host', 'pool'], item.type)) {
this.context.router.push({
pathname: `${item.type.toLowerCase()}s/${item.id}`
})
}
}
}
)
_typesDropdownItems = map(TYPES, (label, type) =>
<MenuItem onClick={() => this._setType(type)}>{label}</MenuItem>
)
_renderHeader () {
const { filters } = OPTIONS[this.props.type]
const { type } = this.props
const { filters } = OPTIONS[type]
const customFilters = this._getCustomFilters()
return <Container>
<Row className={styles.itemRowHeader}>
<Col mediumSize={3}>
<DropdownButton id='typeMenu' bsStyle='info' title={TYPES[this.props.type]}>
<MenuItem onClick={() => this._setType('VM')}>
VM
</MenuItem>
<MenuItem onClick={() => this._setType('host')}>
Host
</MenuItem>
<MenuItem onClick={() => this._setType('pool')}>
Pool
</MenuItem>
<DropdownButton id='typeMenu' bsStyle='info' title={TYPES[this._getType()]}>
{this._typesDropdownItems}
</DropdownButton>
</Col>
<Col mediumSize={6}>
<div className='input-group'>
{!isEmpty(filters) && <div className='input-group-btn'>
<DropdownButton id='filter' bsStyle='info' title={_('homeFilters')}>
{map(filters, (filter, label) =>
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
{_(label)}
</MenuItem>
)}
</DropdownButton>
</div>}
{!isEmpty(filters) && (
<div className='input-group-btn'>
<DropdownButton id='filter' bsStyle='info' title={_('homeFilters')}>
{!isEmpty(customFilters) && [
map(customFilters, (filter, name) =>
<MenuItem key={`custom-${name}`} onClick={() => this._setFilter(filter)}>
{name}
</MenuItem>
),
<MenuItem divider />
]}
{map(filters, (filter, label) =>
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
{_(label)}
</MenuItem>
)}
</DropdownButton>
</div>
)}
<input
autoFocus
className='form-control'
defaultValue={this._getFilter()}
onChange={this._onFilterChange}
@@ -349,11 +502,18 @@ export default class Home extends Component {
type='text'
/>
<div className='input-group-btn'>
<button
<a
className='btn btn-secondary'
onClick={this._clearFilter}>
<Icon icon='clear-search' />
</button>
</a>
</div>
<div className='input-group-btn'>
<ActionButton
btnStyle='primary'
handler={this._addCustomFilter}
icon='save'
/>
</div>
</div>
</Col>
@@ -425,40 +585,41 @@ export default class Home extends Component {
<p className='text-muted'>{_('homeNewVmMessage')}</p>
</Col>
</Row>
<h2>{_('homeNoVmsOr')}</h2>
<Row>
<Col mediumSize={6}>
<Link to='/import'>
<Icon icon='menu-new-import' size={4} />
<h4>{_('homeImportVm')}</h4>
</Link>
<p className='text-muted'>{_('homeImportVmMessage')}</p>
</Col>
<Col mediumSize={6}>
<Link to='/backup/restore'>
<Icon icon='backup' size={4} />
<h4>{_('homeRestoreBackup')}</h4>
</Link>
<p className='text-muted'>{_('homeRestoreBackupMessage')}</p>
</Col>
</Row>
{isAdmin && <div>
<h2>{_('homeNoVmsOr')}</h2>
<Row>
<Col mediumSize={6}>
<Link to='/import'>
<Icon icon='menu-new-import' size={4} />
<h4>{_('homeImportVm')}</h4>
</Link>
<p className='text-muted'>{_('homeImportVmMessage')}</p>
</Col>
<Col mediumSize={6}>
<Link to='/backup/restore'>
<Icon icon='backup' size={4} />
<h4>{_('homeRestoreBackup')}</h4>
</Link>
<p className='text-muted'>{_('homeRestoreBackupMessage')}</p>
</Col>
</Row>
</div>}
</CardBlock>
</Card>
</CenterPanel>
}
const filteredItems = this._getFilteredItems()
const selectedItemsIds = keys(this._selectedItems)
const visibleItems = this._getVisibleItems()
const { activePage, sortBy } = this.state
const items = {
'VM': VmItem,
'host': HostItem,
'pool': PoolItem
}
const Item = items[props.type] || items[DEFAULT_TYPE]
const { activePage, sortBy, highlighted } = this.state
const { type } = props
const options = OPTIONS[type]
const { Item } = options
const { mainActions, otherActions } = options
const selectedItemsIds = keys(this._selectedItems)
return <Page header={this._renderHeader()}>
<Shortcuts name='Home' handler={this._getShortcutsHandler()} targetNodeSelector='body' stopPropagation={false} />
<div>
<div className={styles.itemContainer}>
<SingleLineRow className={styles.itemContainerHeader}>
@@ -467,118 +628,120 @@ export default class Home extends Component {
{' '}
<span className='text-muted'>
{size(this._selectedItems)
? _('homeSelectedVms', { selected: size(this._selectedItems), total: nItems, vmIcon: <Icon icon='vm' /> })
: _('homeDisplayedVms', { displayed: filteredItems.length, total: nItems, vmIcon: <Icon icon='vm' /> })
? _('homeSelectedItems', {
icon: <Icon icon={type.toLowerCase()} />,
selected: size(this._selectedItems),
total: nItems
})
: _('homeDisplayedItems', {
displayed: filteredItems.length,
icon: <Icon icon={type.toLowerCase()} />,
total: nItems
})
}
</span>
</Col>
<Col mediumSize={8} className='text-xs-right hidden-sm-down'>
{this.state.displayActions
? <div className='btn-group'>
<ActionButton btnStyle='secondary' handler={stopVms} handlerParam={selectedItemsIds} icon='vm-stop' />
<ActionButton btnStyle='secondary' handler={startVms} handlerParam={selectedItemsIds} icon='vm-start' />
<ActionButton btnStyle='secondary' handler={restartVms} handlerParam={selectedItemsIds} icon='vm-reboot' />
<ActionButton btnStyle='secondary' handler={migrateVms} handlerParam={selectedItemsIds} icon='vm-migrate' />
<DropdownButton bsStyle='secondary' id='advanced' title={_('homeMore')}>
<MenuItem onClick={() => { restartVms(selectedItemsIds, true) }}>
<Icon icon='vm-force-reboot' fixedWidth /> {_('forceRebootVmLabel')}
</MenuItem>
<MenuItem onClick={() => { stopVms(selectedItemsIds, true) }}>
<Icon icon='vm-force-shutdown' fixedWidth /> {_('forceShutdownVmLabel')}
</MenuItem>
<MenuItem onClick={() => { snapshotVms(selectedItemsIds) }}>
<Icon icon='vm-snapshot' fixedWidth /> {_('snapshotVmLabel')}
</MenuItem>
<MenuItem onClick={() => { deleteVms(selectedItemsIds) }}>
<Icon icon='vm-delete' fixedWidth /> {_('vmRemoveButton')}
</MenuItem>
</DropdownButton>
</div>
: <div>
<OverlayTrigger
trigger='click'
rootClose
placement='bottom'
overlay={
<Popover className={styles.selectObject} id='poolPopover'>
<SelectPool
autoFocus
multi
onChange={this._updateSelectedPools}
value={this.state.selectedPools}
/>
</Popover>
}
>
<Button className='btn-link'><Icon icon='pool' /> {_('homeAllPools')}</Button>
</OverlayTrigger>
{' '}
<OverlayTrigger
trigger='click'
rootClose
placement='bottom'
overlay={
<Popover className={styles.selectObject} id='HostPopover'>
<SelectHost
autoFocus
multi
onChange={this._updateSelectedHosts}
value={this.state.selectedHosts}
/>
</Popover>
}
>
<Button className='btn-link'><Icon icon='host' /> {_('homeAllHosts')}</Button>
</OverlayTrigger>
{' '}
<OverlayTrigger
autoFocus
trigger='click'
rootClose
placement='bottom'
overlay={
<Popover className={styles.selectObject} id='tagPopover'>
<SelectTag
autoFocus
multi
objects={props.items}
onChange={this._updateSelectedTags}
value={this.state.selectedTags}
/>
</Popover>
}
>
<Button className='btn-link'><Icon icon='tags' /> {_('homeAllTags')}</Button>
</OverlayTrigger>
{' '}
<DropdownButton bsStyle='link' id='sort' title={_('homeSortBy')}>
<MenuItem onClick={this._sortByName}>
{this._tick(sortBy === 'name_label')}
{sortBy === 'name_label'
? <strong>{_('homeSortByName')}</strong>
: _('homeSortByName')}
</MenuItem>
<MenuItem onClick={this._sortByPowerState}>
{this._tick(sortBy === 'power_state')}
{sortBy === 'power_state'
? <strong>{_('homeSortByPowerstate')}</strong>
: _('homeSortByPowerstate')}
</MenuItem>
<MenuItem onClick={this._sortByRam}>
{this._tick(sortBy === 'memory.size')}
{sortBy === 'memory.size'
? <strong>{_('homeSortByRAM')}</strong>
: _('homeSortByRAM')}
</MenuItem>
<MenuItem onClick={this._sortByVcpus}>
{this._tick(sortBy === 'CPUs.number')}
{sortBy === 'CPUs.number'
? <strong>{_('homeSortByvCPUs')}</strong>
: _('homeSortByvCPUs')}
</MenuItem>
</DropdownButton>
</div>
}
{this.state.displayActions
? (
<div>
{mainActions && (
<div className='btn-group'>
{map(mainActions, (action, key) => (
<Tooltip content={action.tooltip} key={key}>
<ActionButton
btnStyle='secondary'
{...action}
handlerParam={selectedItemsIds}
/>
</Tooltip>
))}
</div>
)}
{otherActions && (
<DropdownButton bsStyle='secondary' id='advanced' title={_('homeMore')}>
{map(otherActions, (action, key) => (
<MenuItem key={key} onClick={() => { action.handler(selectedItemsIds, action.params) }}>
<Icon icon={action.icon} fixedWidth /> {_(action.labelId)}
</MenuItem>
))}
</DropdownButton>
)}
</div>
) : <div>
{options.showPoolsSelector && (
<OverlayTrigger
trigger='click'
rootClose
placement='bottom'
overlay={
<Popover className={styles.selectObject} id='poolPopover'>
<SelectPool
autoFocus
multi
onChange={this._updateSelectedPools}
value={this.state.selectedPools}
/>
</Popover>
}
>
<Button className='btn-link'><Icon icon='pool' /> {_('homeAllPools')}</Button>
</OverlayTrigger>
)}
{' '}
{options.showHostsSelector && (
<OverlayTrigger
trigger='click'
rootClose
placement='bottom'
overlay={
<Popover className={styles.selectObject} id='HostPopover'>
<SelectHost
autoFocus
multi
onChange={this._updateSelectedHosts}
value={this.state.selectedHosts}
/>
</Popover>
}
>
<Button className='btn-link'><Icon icon='host' /> {_('homeAllHosts')}</Button>
</OverlayTrigger>
)}
{' '}
<OverlayTrigger
autoFocus
trigger='click'
rootClose
placement='bottom'
overlay={
<Popover className={styles.selectObject} id='tagPopover'>
<SelectTag
autoFocus
multi
objects={props.items}
onChange={this._updateSelectedTags}
value={this.state.selectedTags}
/>
</Popover>
}
>
<Button className='btn-link'><Icon icon='tags' /> {_('homeAllTags')}</Button>
</OverlayTrigger>
{' '}
<DropdownButton bsStyle='link' id='sort' title={_('homeSortBy')}>
{map(options.sortOptions, ({ labelId, sortBy: _sortBy, sortOrder }, key) => (
<MenuItem key={key} onClick={() => this.setState({ sortBy: _sortBy, sortOrder })}>
{this._tick(_sortBy === sortBy)}
{_sortBy === sortBy
? <strong>{_(labelId)}</strong>
: _(labelId)
}
</MenuItem>
))}
</DropdownButton>
</div>
}
</Col>
<Col smallsize={1} mediumSize={1} className='text-xs-right'>
<button className='btn btn-secondary'
@@ -587,15 +750,24 @@ export default class Home extends Component {
</button>
</Col>
</SingleLineRow>
{map(visibleItems, item =>
<Item
expandAll={this.state.expandAll}
item={item}
key={item.id}
onSelect={this._selectItem}
selected={this._selectedItems[item.id]}
/>
)}
{isEmpty(filteredItems)
? <p className='text-xs-center m-t-1'>
<a className='btn btn-link' onClick={this._clearFilter}>
<Icon icon='info' /> {_('homeNoMatches')}
</a>
</p>
: map(visibleItems, (item, index) => (
<div className={highlighted === index && styles.highlight}>
<Item
expandAll={this.state.expandAll}
item={item}
key={item.id}
onSelect={this._selectItem}
selected={this._selectedItems[item.id]}
/>
</div>
))
}
</div>
{filteredItems.length > ITEMS_PER_PAGE && <Row>
<div style={{display: 'flex', width: '100%'}}>

View File

@@ -9,8 +9,8 @@ import SingleLineRow from 'single-line-row'
import size from 'lodash/size'
import Tags from 'tags'
import Tooltip from 'tooltip'
import { BlockLink } from 'link'
import { Row, Col } from 'grid'
import Link, { BlockLink } from 'link'
import { Col } from 'grid'
import { Text } from 'editable'
import {
addTag,
@@ -48,7 +48,8 @@ import styles from './index.css'
return {
hostMetrics: getHostMetrics,
missingPaths: getMissingPatches
missingPaths: getMissingPatches,
poolHosts: getPoolHosts
}
})
export default class PoolItem extends Component {
@@ -64,7 +65,7 @@ export default class PoolItem extends Component {
}
render () {
const { item: pool, expandAll, selected, hostMetrics } = this.props
const { item: pool, expandAll, selected, hostMetrics, poolHosts } = this.props
const { missingPatchCount } = this.state
return <div className={styles.item}>
<BlockLink to={`/pools/${pool.id}`}>
@@ -106,11 +107,9 @@ export default class PoolItem extends Component {
</Col>
<Col largeSize={4} className='hidden-lg-down'>
<span>
{hostMetrics.count}x <Icon icon='host' />
{' '}
{hostMetrics.cpus}x <Icon icon='cpu' />
{' '}
{formatSize(hostMetrics.memoryTotal)}
<Tooltip content={_('memoryLeftTooltip', {used: Math.round((hostMetrics.memoryUsage / hostMetrics.memoryTotal) * 100), free: formatSize(hostMetrics.memoryTotal - hostMetrics.memoryUsage)})}>
<progress style={{margin: 0}} className='progress' value={(hostMetrics.memoryUsage / hostMetrics.memoryTotal) * 100} max='100' />
</Tooltip>
</span>
</Col>
<Col mediumSize={1} className={styles.itemExpandRow}>
@@ -122,22 +121,27 @@ export default class PoolItem extends Component {
</SingleLineRow>
</BlockLink>
{(this.state.expanded || expandAll) &&
<Row>
<Col mediumSize={6} className={styles.itemExpanded}>
<SingleLineRow>
<Col mediumSize={3} className={styles.itemExpanded}>
<span>
{hostMetrics.count}x <Icon icon='host' />
{' '}
{hostMetrics.cpus}x <Icon icon='cpu' />
{' '}
{formatSize(hostMetrics.memory)}
{formatSize(hostMetrics.memoryTotal)}
</span>
</Col>
<Col mediumSize={6}>
<Col mediumSize={4} className={styles.itemExpanded}>
<span>
{_('homePoolMaster')} <Link to={`/hosts/${pool.master}`}>{poolHosts && poolHosts[pool.master].name_label}</Link>
</span>
</Col>
<Col mediumSize={5}>
<span style={{fontSize: '1.4em'}}>
<Tags labels={pool.tags} onDelete={this._removeTag} onAdd={this._addTag} />
</span>
</Col>
</Row>
</SingleLineRow>
}
</div>
}

View File

@@ -0,0 +1,92 @@
import _ from 'intl'
import Component from 'base-component'
import Ellipsis, { EllipsisContainer } from 'ellipsis'
import Icon from 'icon'
import Link from 'link'
import map from 'lodash/map'
import React from 'react'
import SingleLineRow from 'single-line-row'
import Tags from 'tags'
import Tooltip from 'tooltip'
import { Row, Col } from 'grid'
import { Number, Size, Text } from 'editable'
import {
addTag,
editVm,
removeTag
} from 'xo'
import {
connectStore,
firstDefined,
osFamily
} from 'utils'
import {
createGetObject
} from 'selectors'
import styles from './index.css'
@connectStore({
container: createGetObject((_, props) => props.item.$container)
})
export default class TemplateItem extends Component {
_addTag = tag => addTag(this.props.item.id, tag)
_onSelect = () => this.props.onSelect(this.props.item.id)
_removeTag = tag => removeTag(this.props.item.id, tag)
_setNameDescription = nameDescription => editVm(this.props.item, { name_description: nameDescription })
_setNameLabel = nameLabel => editVm(this.props.item, { name_label: nameLabel })
_setCpus = nCpus => editVm(this.props.item, { CPUs: nCpus })
_setMemory = memory => editVm(this.props.item, { memory })
render () {
const { item: vm, container, expandAll, selected } = this.props
return <div className={styles.item}>
<SingleLineRow>
<Col smallSize={10} mediumSize={9} largeSize={5}>
<EllipsisContainer>
<input type='checkbox' checked={selected} onChange={this._onSelect} value={vm.id} />
&nbsp;&nbsp;
<Ellipsis>
<Text value={vm.name_label} onChange={this._setNameLabel} placeholder={_('templateHomeNamePlaceholder')} />
</Ellipsis>
</EllipsisContainer>
</Col>
<Col mediumSize={4} className='hidden-md-down'>
<EllipsisContainer>
<Tooltip content={vm.os_version ? vm.os_version.name : _('unknownOsName')}><Icon className='text-info' icon={vm.os_version && osFamily(vm.os_version.distro)} fixedWidth /></Tooltip>
{' '}
<Ellipsis>
<Text value={vm.name_description} onChange={this._setNameDescription} placeholder={_('templateHomeDescriptionPlaceholder')} />
</Ellipsis>
</EllipsisContainer>
</Col>
<Col mediumSize={2} className='hidden-sm-down'>
{container && <Link to={`/${container.type}s/${container.id}`}>{container.name_label}</Link>}
</Col>
<Col mediumSize={1} className={styles.itemExpandRow}>
<a className={styles.itemExpandButton} onClick={this.toggleState('expanded')}>
<Icon icon='nav' fixedWidth />&nbsp;&nbsp;&nbsp;
</a>
</Col>
</SingleLineRow>
{(this.state.expanded || expandAll) &&
<Row>
<Col mediumSize={4} className={styles.itemExpanded}>
<span>
<Number value={vm.CPUs.number} onChange={this._setCpus} />x <Icon icon='cpu' className='m-r-1' />
<Size value={firstDefined(vm.memory.size, null)} onChange={this._setMemory} /> <Icon icon='memory' />
</span>
</Col>
<Col largeSize={4} className={styles.itemExpanded}>
{map(vm.addresses, address => <span key={address} className='tag tag-info tag-ip'>{address}</span>)}
</Col>
<Col mediumSize={4}>
<span style={{fontSize: '1.4em'}}>
<Tags labels={vm.tags} onDelete={this._removeTag} onAdd={this._addTag} />
</span>
</Col>
</Row>
}
</div>
}
}

View File

@@ -86,22 +86,28 @@ export default class VmItem extends Component {
<span className={styles.itemActionButons}>
{this._isRunning
? <span>
<Tooltip content={_('vmConsoleLabel')}>
<Link to={`/vms/${vm.id}/console`}>
<Icon icon='vm-console' size='1' fixedWidth />
</Link>
</Tooltip>
<Tooltip content={_('stopVmLabel')}>
<a onClick={this._stop}>
<Icon icon='vm-stop' size='1' />
<Icon icon='vm-stop' size='1' fixedWidth />
</a>
</Tooltip>
</span>
: <span>
<Icon fixedWidth />
<Tooltip content={_('startVmLabel')}>
<a onClick={this._start}>
<Icon icon='vm-start' size='1' />
<Icon icon='vm-start' size='1' fixedWidth />
</a>
</Tooltip>
</span>
}
</span>
<Icon className='text-info' icon={vm.os_version && osFamily(vm.os_version.distro)} fixedWidth />
<Tooltip content={vm.os_version ? vm.os_version.name : _('unknownOsName')}><Icon className='text-info' icon={vm.os_version && osFamily(vm.os_version.distro)} fixedWidth /></Tooltip>
{' '}
<Ellipsis>
<Text value={vm.name_description} onChange={this._setNameDescription} placeholder={_('vmHomeDescriptionPlaceholder')} useLongClick />

View File

@@ -10,6 +10,7 @@ import Page from '../page'
import pick from 'lodash/pick'
import React, { cloneElement, Component } from 'react'
import sortBy from 'lodash/sortBy'
import Tooltip from 'tooltip'
import { Text } from 'editable'
import { editHost, fetchHostStats, getHostMissingPatches, installAllHostPatches, installHostPatch } from 'xo'
import { Container, Row, Col } from 'grid'
@@ -18,6 +19,7 @@ import {
routes
} from 'utils'
import {
createDoesHostNeedRestart,
createGetObject,
createGetObjectsOfType,
createSelector
@@ -86,26 +88,18 @@ const isRunning = host => host && host.power_state === 'Running'
)
)
const getPoolPatches = createGetObjectsOfType('pool_patch').pick(
createSelector(
createGetObjectsOfType(
'host_patch',
createSelector(getHost, host => host.patches)
),
hostPatches => map(hostPatches, patch => patch.pool_patch)
)
).sort()
const getPbds = createGetObjectsOfType('PBD').pick(
createSelector(getHost, host => host.$PBDs)
const getHostPatches = createSelector(
createGetObjectsOfType('pool_patch'),
createGetObjectsOfType('host_patch').pick(
createSelector(getHost, host => host.patches)
),
(poolsPatches, hostsPatches) => map(hostsPatches, hostPatch => ({
...hostPatch,
poolPatch: poolsPatches[hostPatch.pool_patch]
}))
)
const getSrs = createGetObjectsOfType('SR').pick(
createSelector(
getPbds,
pbds => map(pbds, pbd => pbd.SR)
)
)
const doesNeedRestart = createDoesHostNeedRestart(getHost)
return (state, props) => {
const host = getHost(state, props)
@@ -115,19 +109,22 @@ const isRunning = host => host && host.power_state === 'Running'
return {
host,
hostPatches: getHostPatches(state, props),
logs: getLogs(state, props),
needsRestart: doesNeedRestart(state, props),
networks: getNetworks(state, props),
pbds: getPbds(state, props),
pifs: getPifs(state, props),
pool: getPool(state, props),
poolPatches: getPoolPatches(state, props),
srs: getSrs(state, props),
vmController: getVmController(state, props),
vms: getHostVms(state, props)
}
}
})
export default class Host extends Component {
static contextTypes = {
router: React.PropTypes.object
}
loop (host = this.props.host) {
if (this.cancel) {
this.cancel()
@@ -181,6 +178,10 @@ export default class Host extends Component {
}
const hostCur = this.props.host
if (hostCur && !hostNext) {
this.context.router.push('/')
}
if (!hostCur) {
this._getMissingPatches(hostNext)
}
@@ -233,7 +234,7 @@ export default class Host extends Component {
value={host.name_description}
onChange={this._setNameDescription}
/>
<span className='text-muted'> - <Link to={`/pools/${pool.id}`}>{pool.name_label}</Link></span>
{pool && <span className='text-muted'> - <Link to={`/pools/${pool.id}`}>{pool.name_label}</Link></span>}
</span>
</Col>
<Col mediumSize={6}>
@@ -251,7 +252,10 @@ export default class Host extends Component {
<NavLink to={`/hosts/${host.id}/console`}>{_('consoleTabName')}</NavLink>
<NavLink to={`/hosts/${host.id}/network`}>{_('networkTabName')}</NavLink>
<NavLink to={`/hosts/${host.id}/storage`}>{_('storageTabName')}</NavLink>
<NavLink to={`/hosts/${host.id}/patches`}>{_('patchesTabName')} {isEmpty(missingPatches) ? null : <span className='tag tag-pill tag-danger'>{missingPatches.length}</span>}</NavLink>
<NavLink to={`/hosts/${host.id}/patches`}>
{_('patchesTabName')} {isEmpty(missingPatches) ? null : <span className='tag tag-pill tag-danger'>{missingPatches.length}</span>}
{(this.props.needsRestart && isEmpty(missingPatches)) && <Tooltip content={_('rebootUpdateHostLabel')}><Icon icon='alarm' /></Tooltip>}
</NavLink>
<NavLink to={`/hosts/${host.id}/logs`}>{_('logsTabName')}</NavLink>
<NavLink to={`/hosts/${host.id}/advanced`}>{_('advancedTabName')}</NavLink>
</NavTabs>
@@ -261,17 +265,17 @@ export default class Host extends Component {
}
render () {
const { host } = this.props
const { host, pool } = this.props
if (!host) {
return <h1>Loading</h1>
return <h1>{_('statusLoading')}</h1>
}
const childProps = assign(pick(this.props, [
'host',
'hostPatches',
'logs',
'networks',
'pbds',
'pifs',
'poolPatches',
'srs',
'vmController',
'vms'
@@ -283,7 +287,7 @@ export default class Host extends Component {
installPatch: this._installPatch
}
)
return <Page header={this.header()}>
return <Page header={this.header()} title={`${host.name_label}${pool ? ` (${pool.name_label})` : ''}`}>
{cloneElement(this.props.children, childProps)}
</Page>
}

View File

@@ -3,7 +3,7 @@ import Copiable from 'copiable'
import React from 'react'
import TabButton from 'tab-button'
import { Toggle } from 'form'
import { enableHost, disableHost, restartHost } from 'xo'
import { enableHost, detachHost, disableHost, restartHost } from 'xo'
import { FormattedRelative, FormattedTime } from 'react-intl'
import { Container, Row, Col } from 'grid'
@@ -39,6 +39,13 @@ export default ({
labelId='enableHostLabel'
/>
}
<TabButton
btnStyle='danger'
handler={detachHost}
handlerParam={host}
icon='host-eject'
labelId='detachHost'
/>
</Col>
</Row>
<Row>

View File

@@ -68,22 +68,7 @@ export default class extends Component {
</Row>}
<br />
<Row>
<Col mediumSize={5}>
{/* TODO: insert real ISO selector, CtrlAltSuppr button and Clipboard */}
<div className='input-group'>
<select className='form-control'>
<option>-- CD Drive (empty) --</option>
<option>Debian-8.iso</option>
<option>Windows7.iso</option>
</select>
<span className='input-group-btn'>
<button className='btn btn-secondary'>
<Icon icon='vm-eject' />
</button>
</span>
</div>
</Col>
<Col mediumSize={5}>
<Col mediumSize={10}>
<div className='input-group'>
<input type='text' className='form-control' ref='clipboard' onChange={this._setRemoteClipboard} />
<span className='input-group-btn'>

View File

@@ -3,13 +3,15 @@ import Copiable from 'copiable'
import Icon from 'icon'
import map from 'lodash/map'
import React from 'react'
import store from 'store'
import Tags from 'tags'
import Tooltip from 'tooltip'
import { addTag, removeTag } from 'xo'
import { BlockLink } from 'link'
import { Container, Row, Col } from 'grid'
import { FormattedRelative } from 'react-intl'
import { formatSize } from 'utils'
import Usage, { UsageElement } from 'usage'
import { getObject } from 'selectors'
import {
CpuSparkLines,
MemorySparkLines,
@@ -22,75 +24,78 @@ export default ({
host,
vmController,
vms
}) => <Container>
<br />
<Row className='text-xs-center'>
<Col mediumSize={3}>
<h2>{host.CPUs.cpu_count}x <Icon icon='cpu' size='lg' /></h2>
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <CpuSparkLines data={statsOverview} />}</BlockLink>
</Col>
<Col mediumSize={3}>
<h2>{formatSize(host.memory.size)} <Icon icon='memory' size='lg' /></h2>
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <MemorySparkLines data={statsOverview} />}</BlockLink>
</Col>
<Col mediumSize={3}>
<BlockLink to={`/hosts/${host.id}/network`}><h2>{host.$PIFs.length}x <Icon icon='network' size='lg' /></h2></BlockLink>
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <PifSparkLines data={statsOverview} />}</BlockLink>
</Col>
<Col mediumSize={3}>
<BlockLink to={`/hosts/${host.id}/disks`}><h2>{host.$PBDs.length}x <Icon icon='disk' size='lg' /></h2></BlockLink>
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <LoadSparkLines data={statsOverview} />}</BlockLink>
</Col>
</Row>
<br />
<Row className='text-xs-center'>
<Col mediumSize={3}>
<p className='text-xs-center'>{_('started', { ago: <FormattedRelative value={host.startTime * 1000} /> })}</p>
</Col>
<Col mediumSize={3}>
<p>{host.license_params.sku_marketing_name} {host.license_params.version} ({host.license_params.sku_type})</p>
</Col>
<Col mediumSize={3}>
<Copiable tagName='p'>
{host.address}
</Copiable>
</Col>
<Col mediumSize={3}>
<p>{host.bios_strings['system-manufacturer']} {host.bios_strings['system-product-name']}</p>
</Col>
</Row>
<Row>
<Col className='text-xs-center'>
<h5>RAM usage:</h5>
</Col>
</Row>
<Row>
<Col smallOffset={1} mediumSize={10}>
<span className='progress-usage'>
<Tooltip content='XenServer'>
<span
className='progress-dom0'
style={{ width: (vmController.memory.size / host.memory.size) * 100 + '%' }}>
</span>
</Tooltip>
{map(vms, vm => (
<Tooltip key={vm.id} content={vm.name_label}>
<a
href={`#/vms/${vm.id}`}
className='progress-object'
style={{ width: (vm.memory.size / host.memory.size) * 100 + '%' }}>
</a>
</Tooltip>
)
)}
</span>
</Col>
</Row>
<Row>
<Col>
<h2 className='text-xs-center'>
<Tags labels={host.tags} onDelete={tag => removeTag(host.id, tag)} onAdd={tag => addTag(host.id, tag)} />
</h2>
</Col>
</Row>
</Container>
}) => {
const pool = getObject(store.getState(), host.$pool)
return <Container>
<br />
<Row className='text-xs-center'>
<Col mediumSize={3}>
<h2>{host.CPUs.cpu_count}x <Icon icon='cpu' size='lg' /></h2>
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <CpuSparkLines data={statsOverview} />}</BlockLink>
</Col>
<Col mediumSize={3}>
<h2>{formatSize(host.memory.size)} <Icon icon='memory' size='lg' /></h2>
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <MemorySparkLines data={statsOverview} />}</BlockLink>
</Col>
<Col mediumSize={3}>
<BlockLink to={`/hosts/${host.id}/network`}><h2>{host.$PIFs.length}x <Icon icon='network' size='lg' /></h2></BlockLink>
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <PifSparkLines data={statsOverview} />}</BlockLink>
</Col>
<Col mediumSize={3}>
<BlockLink to={`/hosts/${host.id}/storage`}><h2>{host.$PBDs.length}x <Icon icon='disk' size='lg' /></h2></BlockLink>
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <LoadSparkLines data={statsOverview} />}</BlockLink>
</Col>
</Row>
<br />
<Row className='text-xs-center'>
<Col mediumSize={3}>
<p className='text-xs-center'>{_('started', { ago: <FormattedRelative value={host.startTime * 1000} /> })}</p>
</Col>
<Col mediumSize={3}>
<p>{host.license_params.sku_marketing_name} {host.version} ({host.license_params.sku_type})</p>
</Col>
<Col mediumSize={3}>
<Copiable tagName='p'>
{host.address}
</Copiable>
</Col>
<Col mediumSize={3}>
<p>{host.bios_strings['system-manufacturer']} {host.bios_strings['system-product-name']}</p>
</Col>
</Row>
<Row>
<Col className='text-xs-center'>
<h5>{_('memoryStatePanel')}</h5>
</Col>
</Row>
<Row>
<Col smallOffset={1} mediumSize={10}>
<Usage total={host.memory.size}>
<UsageElement
highlight
tooltip='XenServer'
value={vmController.memory.size}
/>
{map(vms, vm => <UsageElement
tooltip={vm.name_label}
key={vm.id}
value={vm.memory.size}
href={`#/vms/${vm.id}`}
/>)}
</Usage>
</Col>
</Row>
{pool && host.id === pool.master && <Row className='text-xs-center'>
<Col>
<h3><span className='tag tag-pill tag-info'>{_('pillMaster')}</span></h3>
</Col>
</Row>}
<Row>
<Col>
<h2 className='text-xs-center'>
<Tags labels={host.tags} onDelete={tag => removeTag(host.id, tag)} onAdd={tag => addTag(host.id, tag)} />
</h2>
</Col>
</Row>
</Container>
}

View File

@@ -15,7 +15,8 @@ const LOG_COLUMNS = [
{
name: _('logDate'),
itemRenderer: log => <span><FormattedTime value={log.time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={log.time * 1000} />)</span>,
sortCriteria: log => log.time
sortCriteria: log => log.time,
sortOrder: 'desc'
},
{
name: _('logName'),

View File

@@ -3,15 +3,35 @@ import ActionRowButton from 'action-row-button'
import React from 'react'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import some from 'lodash/some'
import TabButton from 'tab-button'
import { connectPif, createNetwork, deletePif, disconnectPif } from 'xo'
import Tooltip from 'tooltip'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { Container, Row, Col } from 'grid'
import { Toggle } from 'form'
import { connectStore } from 'utils'
import { createGetObjectsOfType } from 'selectors'
import {
connectPif,
createNetwork,
deletePif,
disconnectPif,
editNetwork
} from 'xo'
export default ({
const _toggleDefaultLockingMode = (component, tooltip) => tooltip
? <Tooltip content={tooltip}>
{component}
</Tooltip>
: component
export default connectStore(() => ({
vifsByNetwork: createGetObjectsOfType('VIF').groupBy('$network')
}))(({
host,
networks,
pifs
pifs,
vifsByNetwork
}) => <Container>
<Row>
<Col className='text-xs-right'>
@@ -37,13 +57,16 @@ export default ({
<th>{_('pifAddressLabel')}</th>
<th>{_('pifMacLabel')}</th>
<th>{_('pifMtuLabel')}</th>
<th>{_('defaultLockingMode')}</th>
<th>{_('pifStatusLabel')}</th>
<th />
</tr>
</thead>
<tbody>
{map(pifs, pif =>
<tr key={pif.id}>
{map(pifs, pif => {
const pifInUse = some(vifsByNetwork[pif.$network], vif => vif.attached)
return <tr key={pif.id}>
<td>{pif.device}</td>
<td>{networks[pif.$network].name_label}</td>
<td>{pif.vlan === -1
@@ -53,13 +76,23 @@ export default ({
<td>{pif.ip} ({pif.mode})</td>
<td><pre>{pif.mac}</pre></td>
<td>{pif.mtu}</td>
<td className='text-xs-center'>
{_toggleDefaultLockingMode(
<Toggle
disabled={pifInUse}
onChange={() => editNetwork(pif.$network, { defaultIsLocked: !networks[pif.$network].defaultIsLocked })}
value={networks[pif.$network].defaultIsLocked}
/>,
pifInUse && _('pifInUse')
)}
</td>
<td>
{pif.attached
? <span className='tag tag-success'>
{_('pifStatusConnected')}
{_('pifStatusConnected')}
</span>
: <span className='tag tag-default'>
{_('pifStatusDisconnected')}
{_('pifStatusDisconnected')}
</span>
}
</td>
@@ -68,21 +101,23 @@ export default ({
<ActionRowButton
btnStyle='default'
disabled={pif.attached && (pif.management || pif.disallowUnplug)}
icon={pif.attached ? 'disconnect' : 'connect'}
handler={pif.attached ? disconnectPif : connectPif}
handlerParam={pif}
icon={pif.attached ? 'disconnect' : 'connect'}
tooltip={pif.attached ? _('disconnectPif') : _('connectPif')}
/>
<ActionRowButton
btnStyle='default'
disabled={pif.physical || pif.disallowUnplug || pif.management}
icon='delete'
handler={deletePif}
handlerParam={{ pif }}
icon='delete'
tooltip={_('deletePif')}
/>
</ButtonGroup>
</td>
</tr>
)}
})}
</tbody>
</table>
</span>
@@ -90,4 +125,4 @@ export default ({
}
</Col>
</Row>
</Container>
</Container>)

View File

@@ -1,13 +1,15 @@
import _ from 'intl'
import ActionRowButton from 'action-row-button'
import isEmpty from 'lodash/isEmpty'
import React, { Component } from 'react'
import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import Upgrade from 'xoa-upgrade'
import React, { Component } from 'react'
import { connectStore, formatSize } from 'utils'
import { Container, Row, Col } from 'grid'
import { formatSize } from 'utils'
import { createDoesHostNeedRestart } from 'selectors'
import { FormattedRelative, FormattedTime } from 'react-intl'
import { restartHost } from 'xo'
const MISSING_PATCH_COLUMNS = [
{
@@ -17,13 +19,18 @@ const MISSING_PATCH_COLUMNS = [
},
{
name: _('patchDescription'),
itemRenderer: patch => patch.description,
itemRenderer: patch => (
<a href={patch.documentationUrl} target='_blank'>
{patch.description}
</a>
),
sortCriteria: patch => patch.description
},
{
name: _('patchReleaseDate'),
itemRenderer: patch => <span><FormattedTime value={patch.date} day='numeric' month='long' year='numeric' /> (<FormattedRelative value={patch.date} />)</span>,
sortCriteria: patch => patch.date
sortCriteria: patch => patch.date,
sortOrder: 'desc'
},
{
name: _('patchGuidance'),
@@ -46,59 +53,85 @@ const MISSING_PATCH_COLUMNS = [
const INSTALLED_PATCH_COLUMNS = [
{
name: _('patchNameLabel'),
itemRenderer: patch => patch.name,
sortCriteria: patch => patch.name
itemRenderer: patch => patch.poolPatch.name,
sortCriteria: patch => patch.poolPatch.name
},
{
name: _('patchDescription'),
itemRenderer: patch => patch.description,
sortCriteria: patch => patch.description
itemRenderer: patch => patch.poolPatch.description,
sortCriteria: patch => patch.poolPatch.description
},
{
default: true,
name: _('patchApplied'),
itemRenderer: patch => {
const time = patch.time * 1000
return (
<span>
<FormattedTime value={time} day='numeric' month='long' year='numeric' />
{' '}
(<FormattedRelative value={time} />)
</span>
)
},
sortCriteria: patch => patch.time,
sortOrder: 'desc'
},
{
name: _('patchSize'),
itemRenderer: patch => formatSize(patch.size),
sortCriteria: patch => patch.size
itemRenderer: patch => formatSize(patch.poolPatch.size),
sortCriteria: patch => patch.poolPatch.size
}
]
@connectStore(() => ({
needsRestart: createDoesHostNeedRestart((_, props) => props.host)
}))
export default class HostPatches extends Component {
render () {
const { poolPatches, missingPatches, installAllPatches, installPatch } = this.props
return process.env.XOA_PLAN > 2
const { host, hostPatches, missingPatches, installAllPatches, installPatch } = this.props
return process.env.XOA_PLAN > 1
? <Container>
<Row>
<Col>
<Col className='text-xs-right'>
{(this.props.needsRestart && isEmpty(missingPatches)) && <TabButton
btnStyle='warning'
handler={restartHost}
handlerParam={host}
icon='host-reboot'
labelId='rebootUpdateHostLabel'
/>}
{isEmpty(missingPatches)
? <h4>{_('hostUpToDate')}</h4>
: <span>
<Row>
<Col className='text-xs-right'>
<TabButton
btnStyle='primary'
handler={installAllPatches}
icon='host-patch-update'
labelId='patchUpdateButton'
/>
</Col>
</Row>
<Row>
<Col>
<h3>{_('hostMissingPatches')}</h3>
<SortedTable collection={missingPatches} userData={installPatch} columns={MISSING_PATCH_COLUMNS} />
</Col>
</Row>
</span>
? <TabButton
disabled
handler={installAllPatches}
icon='success'
labelId='hostUpToDate'
/>
: <TabButton
btnStyle='primary'
handler={installAllPatches}
icon='host-patch-update'
labelId='patchUpdateButton'
/>
}
</Col>
</Row>
{!isEmpty(missingPatches) && <Row>
<Col>
<h3>{_('hostMissingPatches')}</h3>
<SortedTable collection={missingPatches} userData={installPatch} columns={MISSING_PATCH_COLUMNS} />
</Col>
</Row>}
<Row>
<Col>
{!isEmpty(poolPatches)
? <span>
<h3>{_('hostInstalledPatches')}</h3>
<SortedTable collection={poolPatches} columns={INSTALLED_PATCH_COLUMNS} />
{!isEmpty(hostPatches)
? (
<span>
<h3>{_('hostAppliedPatches')}</h3>
<SortedTable collection={hostPatches} columns={INSTALLED_PATCH_COLUMNS} />
</span>
: <h4 className='text-xs-center'>{_('patchNothing')}</h4>
) : <h4 className='text-xs-center'>{_('patchNothing')}</h4>
}
</Col>
</Row>

View File

@@ -2,6 +2,7 @@ import _ from 'intl'
import Component from 'base-component'
import Icon from 'icon'
import React from 'react'
import Tooltip from 'tooltip'
import Upgrade from 'xoa-upgrade'
import { Container, Row, Col } from 'grid'
import { Toggle } from 'form'
@@ -95,9 +96,9 @@ export default class HostStats extends Component {
<Row>
<Col mediumSize={6}>
<div className='form-group'>
<label>{_('useCombinedValuesOnStats')}</label>
{' '}
<Toggle value={useCombinedValues} onChange={this.linkState('useCombinedValues')} />
<Tooltip content={_('useStackedValuesOnStats')}>
<Toggle value={useCombinedValues} onChange={this.linkState('useCombinedValues')} />
</Tooltip>
</div>
{selectStatsLoading && (
<div className='text-xs-right'>

View File

@@ -1,108 +1,149 @@
import ActionRowButton from 'action-row-button'
import React from 'react'
import _ from 'intl'
import ActionRowButton from 'action-row-button'
import isEmpty from 'lodash/isEmpty'
import Link from 'link'
import map from 'lodash/map'
import { BlockLink } from 'link'
import { TabButtonLink } from 'tab-button'
import { formatSize } from 'utils'
import React from 'react'
import SortedTable from 'sorted-table'
import Tooltip from 'tooltip'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { connectPbd, disconnectPbd, deletePbd, editSr, isSrShared } from 'xo'
import { connectStore, formatSize } from 'utils'
import { Container, Row, Col } from 'grid'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { TabButtonLink } from 'tab-button'
import { Text } from 'editable'
import { connectPbd, disconnectPbd, deletePbd, editSr } from 'xo'
export default ({
host,
srs,
pbds
}) => <Container>
<Row>
<Col className='text-xs-right'>
<TabButtonLink
icon='add'
labelId='addSrDeviceButton'
to={`/new/sr?host=${host.id}`}
/>
</Col>
</Row>
<Row>
<Col>
{!isEmpty(pbds)
? <span>
<table className='table'>
<thead className='thead-default'>
<tr>
<th>{_('srNameLabel')}</th>
<th>{_('srFormat')}</th>
<th>{_('srSize')}</th>
<th>{_('srUsage')}</th>
<th>{_('srType')}</th>
<th>{_('pdbStatus')}</th>
</tr>
</thead>
<tbody>
{map(pbds, pbd => {
const sr = srs[pbd.SR]
return <BlockLink key={pbd.id} to={`/srs/${sr.id}/general`} tagName='tr'>
<td>
<Text value={sr.name_label} onChange={nameLabel => editSr(sr, { nameLabel })} useLongClick />
</td>
<td>{sr.SR_type}</td>
<td>{formatSize(sr.size)}</td>
<td>
{sr.size > 1 &&
<meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90'></meter>
}
</td>
<td>
{sr.$PBDs.length > 1
? _('srShared')
: _('srNotShared')
}
</td>
<td>
{pbd.attached
? <span>
<span className='tag tag-success'>
{_('pbdStatusConnected')}
</span>
<ButtonGroup className='pull-xs-right'>
<ActionRowButton
btnStyle='default'
icon='disconnect'
handler={disconnectPbd}
handlerParam={pbd}
/>
</ButtonGroup>
</span>
: <span>
<span className='tag tag-default'>
{_('pbdStatusDisconnected')}
</span>
<ButtonGroup className='pull-xs-right'>
<ActionRowButton
btnStyle='default'
icon='connect'
handler={connectPbd}
handlerParam={pbd}
/>
<ActionRowButton
btnStyle='default'
icon='sr-forget'
handler={deletePbd}
handlerParam={pbd}
/>
</ButtonGroup>
</span>
}
</td>
</BlockLink>
})}
</tbody>
</table>
const SR_COLUMNS = [
{
name: _('srName'),
itemRenderer: storage =>
<Link to={`/srs/${storage.id}`}>
<Text
onChange={nameLabel => editSr(storage.id, { nameLabel })}
useLongClick
value={storage.nameLabel}
/>
</Link>,
sortCriteria: 'nameLabel'
},
{
name: _('srFormat'),
itemRenderer: storage => storage.format,
sortCriteria: 'format'
},
{
name: _('srSize'),
itemRenderer: storage => formatSize(storage.size),
sortCriteria: 'size'
},
{
default: true,
name: _('srUsage'),
itemRenderer: storage => storage.size !== 0 &&
<Tooltip content={_('spaceLeftTooltip', {used: storage.usagePercentage, free: formatSize(storage.free)})}>
<meter value={storage.usagePercentage} min='0' max='100' optimum='40' low='80' high='90' />
</Tooltip>,
sortCriteria: storage => storage.usagePercentage,
sortOrder: 'desc'
},
{
name: _('srType'),
itemRenderer: storage => storage.shared ? _('srShared') : _('srNotShared'),
sortCriteria: 'shared'
},
{
name: _('pbdStatus'),
itemRenderer: storage => storage.attached
? <span>
<span className='tag tag-success'>
{_('pbdStatusConnected')}
</span>
: <h4 className='text-xs-center'>{_('pbdNoSr')}</h4>
<ButtonGroup className='pull-xs-right'>
<ActionRowButton
btnStyle='default'
handler={disconnectPbd}
handlerParam={storage.pbdId}
icon='disconnect'
tooltip={_('pbdDisconnect')}
/>
</ButtonGroup>
</span>
: <span>
<span className='tag tag-default'>
{_('pbdStatusDisconnected')}
</span>
<ButtonGroup className='pull-xs-right'>
<ActionRowButton
btnStyle='default'
handler={connectPbd}
handlerParam={storage.pbdId}
icon='connect'
tooltip={_('pbdConnect')}
/>
<ActionRowButton
btnStyle='default'
handler={deletePbd}
handlerParam={storage.pbdId}
icon='sr-forget'
tooltip={_('pbdForget')}
/>
</ButtonGroup>
</span>
}
]
export default connectStore(() => {
const pbds = createGetObjectsOfType('PBD').pick(
(_, props) => props.host.$PBDs
)
const srs = createGetObjectsOfType('SR').pick(
createSelector(
pbds,
pbds => map(pbds, pbd => pbd.SR)
)
)
const storages = createSelector(
pbds,
srs,
(pbds, srs) => map(pbds, pbd => {
const sr = srs[pbd.SR]
const { physical_usage: usage, size } = sr
return {
attached: pbd.attached,
format: sr.SR_type,
free: size > 0 ? size - usage : 0,
id: sr.id,
nameLabel: sr.name_label,
pbdId: pbd.id,
shared: isSrShared(sr),
size: size > 0 ? size : 0,
usagePercentage: size > 0 && Math.round(100 * usage / size)
}
</Col>
</Row>
</Container>
})
)
return { storages }
})(({ host, storages }) =>
<Container>
<Row>
<Col className='text-xs-right'>
<TabButtonLink
icon='add'
labelId='addSrDeviceButton'
to={`/new/sr?host=${host.id}`}
/>
</Col>
</Row>
<Row>
<Col>
{isEmpty(storages)
? <h4 className='text-xs-center'>{_('pbdNoSr')}</h4>
: <SortedTable columns={SR_COLUMNS} collection={storages} />
}
</Col>
</Row>
</Container>
)

View File

@@ -1,10 +1,17 @@
import Component from 'base-component'
import cookies from 'cookies-js'
import Icon from 'icon'
import isArray from 'lodash/isArray'
import map from 'lodash/map'
import React from 'react'
import Shortcuts from 'shortcuts'
import _, { IntlProvider } from 'intl'
import { blockXoaAccess } from 'xoa-updater'
import { connectStore, routes } from 'utils'
import { Notification } from 'notification'
import { ShortcutManager } from 'react-shortcuts'
import { TooltipViewer } from 'tooltip'
import { Container, Row, Col } from 'grid'
// import {
// keyHandler
// } from 'react-key-handler'
@@ -29,6 +36,10 @@ import Vm from './vm'
import VmImport from './vm-import'
import XoaUpdates from './xoa-updates'
import keymap, { help } from '../keymap'
const shortcutManager = new ShortcutManager(keymap)
const CONTAINER_STYLE = {
display: 'flex',
minHeight: '100vh',
@@ -80,6 +91,14 @@ const BODY_STYLE = {
}
})
export default class XoApp extends Component {
static contextTypes = {
router: React.PropTypes.object
}
static childContextTypes = {
shortcuts: React.PropTypes.object.isRequired
}
getChildContext = () => ({ shortcuts: shortcutManager })
displayOpenSourceDisclaimer () {
const previousDisclaimer = cookies.get('previousDisclaimer')
const now = Math.floor(Date.now() / 1e3)
@@ -101,12 +120,60 @@ export default class XoApp extends Component {
}
}
_shortcutsHandler = (command, event) => {
event.preventDefault()
switch (command) {
case 'GO_TO_HOSTS':
this.context.router.push('home?t=host')
break
case 'GO_TO_POOLS':
this.context.router.push('home?t=pool')
break
case 'GO_TO_VMS':
this.context.router.push('home?t=VM')
break
case 'CREATE_VM':
this.context.router.push('vms/new')
break
case 'UNFOCUS':
if (event.target.tagName === 'INPUT') {
event.target.blur()
}
break
case 'HELP':
alert(
<span><Icon icon='shortcuts' />{' '}{_('shortcutModalTitle')}</span>,
<Container>
{map(help, (context, contextKey) => context.name && [
<Row className='m-t-1' key={contextKey}>
<Col>
<h4>{context.name}</h4>
</Col>
</Row>,
...map(context.shortcuts, ({ message, keys }, key) => message &&
<Row key={`${contextKey}_${key}`}>
<Col size={2} className='text-xs-right'>
<strong>
{isArray(keys) ? keys[0] : keys}
</strong>
</Col>
<Col size={10}>{message}</Col>
</Row>
)
])}
</Container>
)
break
}
}
render () {
const { signedUp, trial } = this.props
const blocked = signedUp && blockXoaAccess(trial) // If we are under expired or unstable trial (signed up only)
return <IntlProvider>
<div style={CONTAINER_STYLE}>
<Shortcuts name='XoApp' handler={this._shortcutsHandler} targetNodeSelector='body' stopPropagation={false} />
<Menu ref='menu' />
<div ref='bodyWrapper' style={BODY_WRAPPER_STYLE}>
<div style={BODY_STYLE}>
@@ -115,6 +182,7 @@ export default class XoApp extends Component {
</div>
<Modal />
<Notification />
<TooltipViewer />
</div>
</IntlProvider>
}

View File

@@ -34,7 +34,7 @@ const Jobs = routes('overview', {
scheduling: Scheduling,
'scheduling/:id/edit': SchedulingEdit
})(
({ children }) => <Page header={HEADER}>{children}</Page>
({ children }) => <Page header={HEADER} title='jobsPage' formatTitle>{children}</Page>
)
export default Jobs

View File

@@ -1,4 +1,4 @@
import _ from 'intl'
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import ActionRowButton from 'action-row-button'
import delay from 'lodash/delay'
@@ -15,6 +15,7 @@ import React, { Component } from 'react'
import { error } from 'notification'
import { generateUiSchema } from 'xo-json-schema-input'
import { SelectPlainObject } from 'form'
import { injectIntl } from 'react-intl'
import {
apiMethods,
@@ -81,6 +82,7 @@ const dataToParamVectorItems = function (params, data) {
return items
}
@injectIntl
export default class Jobs extends Component {
constructor (props) {
super(props)
@@ -311,11 +313,13 @@ export default class Jobs extends Component {
job,
jobs
} = this.state
const { formatMessage } = this.props.intl
return <div>
<h1>Jobs</h1>
<h1>{_('jobsPage')}</h1>
<form id='newJobForm'>
<div className='form-group'>
<input type='text' ref='name' className='form-control' placeholder='Name of your Job' required />
<input type='text' ref='name' className='form-control' placeholder={formatMessage(messages.jobNamePlaceholder)} pattern='[^_]+' required />
</div>
<SelectPlainObject ref='method' options={actions} optionKey='method' onChange={this._handleSelectMethod} placeholder={_('jobActionPlaceHolder')} />
{action && <fieldset>
@@ -335,8 +339,8 @@ export default class Jobs extends Component {
<tr>
<th>{_('jobName')}</th>
<th>{_('jobAction')}</th>
<th></th>
<th></th>
<th />
<th />
</tr>
</thead>
<tbody>

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