Compare commits

...

221 Commits

Author SHA1 Message Date
Julien Fontanet
39342cd662 5.3.1 2016-10-27 18:24:31 +02:00
Olivier Lambert
051a3ac122 feat(changelog): modify changelog for 5.3.1 2016-10-27 18:21:25 +02:00
Julien Fontanet
f842a321ba fix(xo): properly sign out on auth failure (#1712)
Fixes #1711
2016-10-27 18:17:20 +02:00
Julien Fontanet
3cd2dd65d3 chore(xo/importConfig): no need to promisify 2016-10-27 17:52:13 +02:00
Pierre Donias
5ce7e0b108 feat(settings/config): import/export XO configuration (#1703)
See #786
2016-10-27 15:08:45 +02:00
Julien Fontanet
71c2058cc8 fix(package): do not test on Node 7 (#1705)
* fix(package): do not test on Node 7
* fix(package): directly depend on chartist 0.9
* fix(package): directly depend on react-overlays 0.6
2016-10-27 11:57:06 +02:00
Greenkeeper
f200d39d23 chore(package): update modular-css to version 0.28.0 (#1707)
https://greenkeeper.io/
2016-10-26 23:34:32 +02:00
Pierre Donias
7932845ac5 fix(xo/exportVm): window.location instead of window.open() (#1704) 2016-10-26 16:52:26 +02:00
Julien Fontanet
94bda6ac9e chore(intl): update all locales (#1698) 2016-10-25 14:48:53 +02:00
fufroma
7a65f80406 feat(intl/messages): more translations (#1684) 2016-10-25 14:12:11 +02:00
Pierre Donias
36ab58dad9 feat(backup/restore): VM-centered UI (#1697)
Fixes #1609
2016-10-25 12:59:01 +02:00
Julien Fontanet
e9be9e3761 chore(package): update react-router to version 3.0.0 2016-10-25 11:21:46 +02:00
Julien Fontanet
b54645c86c chore(package): update d3 to version 4.2.8 2016-10-25 11:21:11 +02:00
Greenkeeper
ab77d8430c chore(package): update promise-toolbox to version 0.7.0 (#1690)
https://greenkeeper.io/
2016-10-24 15:02:02 +02:00
Pierre Donias
c6f683b532 feat(host,pool): edit PIFs VLAN (#1685)
See #1092
2016-10-24 09:52:47 +02:00
Pierre Donias
a2604f5156 fix(xo-app): ensure there is always a page title (#1687) 2016-10-21 18:10:53 +02:00
Pierre Donias
5ae7f683d6 fix(new-vm): fix getting disks in self service (#1688) 2016-10-21 18:00:38 +02:00
fufroma
f953c89979 fix(new/sr): fix username handling (#1683)
Regression was introduced in e79096626a
2016-10-21 11:56:36 +02:00
Julien Fontanet
bb8aab02ea fix(form/Password): generator in controlled mode
Fixes #1678
2016-10-20 17:58:49 +02:00
Julien Fontanet
af0c03ff6a 5.3.0 2016-10-20 16:09:25 +02:00
Olivier Lambert
8859900537 feat(changelog): update for release 2016-10-20 16:07:25 +02:00
Julien Fontanet
130852ab85 fix(xo): pass the refresh function to tap() 2016-10-20 15:13:55 +02:00
fufroma
65fa8f96b4 feat(intl): minor improvements (#1668) 2016-10-20 14:54:06 +02:00
Pierre Donias
0a84e9e363 feat(host/network): configure IP mode (#1671)
Fixes #1651
2016-10-20 13:32:43 +02:00
Pierre Donias
163c69454b fix(modal): disable shortcuts when a modal is open (#1673)
Fixes #1589
2016-10-20 13:30:29 +02:00
Julien Fontanet
49d3fde0f3 fix(JsonSchemaInput/Array): fix items handling
#1663
2016-10-20 13:22:11 +02:00
Pierre Donias
bb67e2254e fix(package): use new syntax of Boostrap classes (#1667) 2016-10-19 16:05:59 +02:00
Pierre Donias
6d2abc4e74 feat(IP pools): can be used in resource sets (#1662)
Fixes #1565
2016-10-19 11:17:44 +02:00
Julien Fontanet
4875450053 fix(home): correctly set default filter when user is loaded
Fixes #1665
2016-10-18 11:57:55 +02:00
Julien Fontanet
19184ca8a0 chore(base-component): disable verbose logs 2016-10-18 11:18:00 +02:00
Julien Fontanet
654c3d324b chore(package): jade → pug 2016-10-18 11:17:35 +02:00
fufroma
c5b4811f16 feat(intl): various new translations (#1659)
Also: favicon.
2016-10-17 14:43:50 +02:00
Julien Fontanet
7a9dc4fd59 fix(package): minor style issues 2016-10-14 14:13:20 +02:00
fufroma
e79096626a feat(intl): more translatable messages (#1641) 2016-10-14 10:46:46 +02:00
fufroma
332d074d32 fix(intl/locales/fr): various fixes (#1638) 2016-10-13 14:14:42 +02:00
Pierre Donias
e511ecd76e fix(vm/copy): inital value of compress should be false (#1652)
Fixes #1645
2016-10-12 09:06:28 +02:00
Greenkeeper
bcfbd5eba9 chore(package): update promise-toolbox to version 0.6.0 (#1650)
https://greenkeeper.io/
2016-10-11 17:32:23 +02:00
Pierre Donias
9fa3db395b fix(self): SizeInput's empty value should be null (#1649) 2016-10-11 15:01:43 +02:00
Julien Fontanet
52a41ceb04 chore(package): update standard to version 8.4.0 2016-10-11 11:39:27 +02:00
Julien Fontanet
e65d67266d style: indent fixes 2016-10-11 10:21:57 +02:00
Julien Fontanet
0d1045821c chore(intl/locales/zh): update skeleton 2016-10-10 16:02:44 +02:00
Pierre Donias
45d526dda2 fix(select-objects): dynamic option height (#1644)
Fixes #1411
2016-10-10 15:47:46 +02:00
Julien Fontanet
e52f998e78 fix(tools/update-locales): remove incorrect import 2016-10-10 10:15:58 +02:00
Julien Fontanet
42ed3b9355 feat(tools/update-locales): create or update locales (#1632)
Replace the existing `tools/create-locale`.
2016-10-10 09:57:08 +02:00
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
137 changed files with 16461 additions and 5352 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,7 +1,8 @@
language: node_js
node_js:
- 'stable'
#- '4' # Disabled for now because npm 2 cannot properly handled broken peer dependencies.
- '6'
#- '4' # npm 3's flat tree is needed because some packages do not
# declare their deps correctly (e.g. chartist-plugin-tooltip)
cache:
directories:

View File

@@ -1,5 +1,182 @@
# ChangeLog
## **5.3.1** (2016-10-27)
### Enhancements
- Improve backup restore view [\#1609](https://github.com/vatesfr/xo-web/issues/1609)
- Move location of NFS mount point [\#1405](https://github.com/vatesfr/xo-web/issues/1405)
- Modify VLAN of an existing network [\#1092](https://github.com/vatesfr/xo-web/issues/1092)
- Ability to export/import XO config [\#786](https://github.com/vatesfr/xo-web/issues/786)
### Bug fixes
- Not properly sign out on auth token expiration [\#1711](https://github.com/vatesfr/xo-web/issues/1711)
- Patches application fails "Found : Moved Temporarily" [\#1701](https://github.com/vatesfr/xo-web/issues/1701)
- Password generation for user creation is not working [\#1678](https://github.com/vatesfr/xo-web/issues/1678)
## **5.3.0** (2016-10-20)
### Enhancements
- Missing favicon [\#1660](https://github.com/vatesfr/xo-web/issues/1660)
- ipPools quota [\#1565](https://github.com/vatesfr/xo-web/issues/1565)
- Dashboard - orphaned VDI [\#1654](https://github.com/vatesfr/xo-web/issues/1654)
- 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)
- Display memory bar in home/host view [\#1616](https://github.com/vatesfr/xo-web/issues/1616)
- 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)
- `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)
- 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
- Display issues on plugin array edition [\#1663](https://github.com/vatesfr/xo-web/issues/1663)
- Import of delta backups fails [\#1656](https://github.com/vatesfr/xo-web/issues/1656)
- Host - Missing IP config for PIF [\#1651](https://github.com/vatesfr/xo-web/issues/1651)
- Remote copy is always activating compression [\#1645](https://github.com/vatesfr/xo-web/issues/1645)
- LB plugin UI problems [\#1630](https://github.com/vatesfr/xo-web/issues/1630)
- Keyboard shortcuts should not work when a modal is open [\#1589](https://github.com/vatesfr/xo-web/issues/1589)
- UI small bug in drop-down lists [\#1411](https://github.com/vatesfr/xo-web/issues/1411)
- md5 delta backup error [\#1672](https://github.com/vatesfr/xo-web/issues/1672)
- Can't edit VIF network [\#1640](https://github.com/vatesfr/xo-web/issues/1640)
- Do not expose shortcuts while console is focused [\#1614](https://github.com/vatesfr/xo-web/issues/1614)
- All users can see VM templates [\#1621](https://github.com/vatesfr/xo-web/issues/1621)
- 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)
- 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)
- 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

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
@@ -249,8 +238,8 @@ function browserify (path, opts) {
gulp.task(function buildPages () {
return pipe(
src('index.jade', { sourcemaps: true }),
require('gulp-jade')(),
src('index.pug', { sourcemaps: true }),
require('gulp-pug')(),
DEVELOPMENT && require('gulp-embedlr')({
port: LIVERELOAD_PORT
}),
@@ -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.1.4",
"version": "5.3.1",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -33,54 +33,62 @@
"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",
"babel-preset-stage-0": "^6.5.0",
"babel-register": "^6.16.3",
"babel-runtime": "^6.6.1",
"babelify": "^7.2.0",
"benchmark": "^2.1.0",
"bootstrap": "github:twbs/bootstrap#v4-dev",
"browserify": "^13.0.0",
"bundle-collapser": "^1.2.1",
"chartist-plugin-legend": "^0.3.1",
"chartist": "^0.9.4",
"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",
"d3": "^4.2.8",
"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": "^6.0.0",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-autoprefixer": "^3.1.0",
"gulp-csso": "^2.0.0",
"gulp-embedlr": "^0.5.2",
"gulp-jade": "^1.1.0",
"gulp-plumber": "^1.1.0",
"gulp-pug": "^3.1.0",
"gulp-refresh": "^1.1.0",
"gulp-sass": "^2.2.0",
"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.25.0",
"modular-css": "^0.28.0",
"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.7.0",
"random-password": "^0.1.2",
"react": "^15.0.0",
"react-addons-shallow-compare": "^15.1.0",
@@ -94,13 +102,15 @@
"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-overlays": "^0.6.0",
"react-redux": "^4.4.0",
"react-router": "^3.0.0-alpha.1",
"react-router": "^3.0.0",
"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",
@@ -108,20 +118,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.4.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",
"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",
@@ -145,6 +156,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

View File

@@ -6,21 +6,30 @@ 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)}>
<ActionButton
key={index}
btnStyle='secondary'
handler={handler}
handlerParam={handlerParam}
icon={icon}
size='large'
/>
</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 || noop}
handlerParam={handlerParam}
icon={icon}
redirectOnSuccess={redirectOnSuccess}
size='large'
/>
</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,41 @@
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'
// Should components logs every renders?
//
// Usually set to process.env.NODE_ENV !== 'production'.
const VERBOSE = false
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)
@@ -14,7 +45,7 @@ export default class BaseComponent extends Component {
this._linkedState = null
if (process.env.NODE_ENV !== 'production') {
if (VERBOSE) {
this.render = invoke(this.render, render => () => {
console.log('render', this.constructor.name)
@@ -24,7 +55,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 +99,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]
})
})
}
@@ -48,7 +121,7 @@ export default class BaseComponent extends Component {
}
}
if (process.env.NODE_ENV !== 'production') {
if (VERBOSE) {
const diff = (name, old, cur) => {
const keys = []

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,22 @@
@value dropzoneColor: #8f8686;
.dropzone {
border-radius: 4px;
border: 2px dashed dropzoneColor;
cursor: pointer;
display: flex;
height: 12em;
margin-bottom: 20px;
width: 100%;
}
.activeDropzone {
background: #f0f0f0;
border-style: solid;
}
.dropzoneText {
color: dropzoneColor;
font-size: 1.2em;
margin: auto;
}

View File

@@ -0,0 +1,20 @@
import Component from 'base-component'
import propTypes from 'prop-types'
import React from 'react'
import ReactDropzone from 'react-dropzone'
import styles from './index.css'
@propTypes({
onDrop: propTypes.func,
message: propTypes.node
})
export default class Dropzone extends Component {
render () {
const { onDrop, message } = this.props
return <ReactDropzone onDrop={onDrop} className={styles.dropzone} activeClassName={styles.activeDropzone}>
<div className={styles.dropzoneText}>{message}</div>
</ReactDropzone>
}
}

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,36 +6,32 @@ 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 getEventValue from '../get-event-value'
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,
SelectResourceSetIp,
SelectSr,
SelectSubject,
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',
cursor: 'context-menu'
}
const LONG_EDITABLE_STYLE = {
cursor: 'context-menu'
}
@propTypes({
alt: propTypes.node.isRequired
@@ -139,7 +136,8 @@ class Editable extends Component {
this._closeEdition()
} catch (error) {
this.setState({
error: isString(error) ? error : error.message,
// `error` may be undefined if the action has been cancelled
error: error !== undefined && (isString(error) ? error : error.message),
saving: false
})
logError(error)
@@ -163,7 +161,7 @@ class Editable extends Component {
const { useLongClick } = props
const success = <Icon icon='success' />
return <span style={useLongClick ? LONG_EDITABLE_STYLE : EDITABLE_STYLE}>
return <span className={classNames(styles.clickToEdit, !useLongClick && styles.shortClick)}>
<span
onClick={!useLongClick && this._openEdition}
onMouseDown={useLongClick && this.__startTimer}
@@ -263,7 +261,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'}
/>
@@ -310,61 +309,66 @@ export class Number extends Component {
}
@propTypes({
labelProp: propTypes.string.isRequired,
options: propTypes.oneOfType([
propTypes.array,
propTypes.object
]).isRequired
]).isRequired,
renderer: propTypes.func
})
export class Select extends Editable {
constructor (props) {
super()
this._defaultValue = findKey(props.options, option => option === props.value)
componentWillReceiveProps (props) {
if (
props.value !== this.props.value ||
props.options !== this.props.options
) {
this.setState({ valueKey: findKey(props.options, option => option === props.value) })
}
}
get value () {
return this.props.options[this._select.value]
return this.props.options[this.state.valueKey]
}
_onChange = event => {
this._save()
this.setState({ valueKey: getEventValue(event) }, this._save)
}
_optionToJsx = (option, index) => {
const { labelProp } = this.props
_optionToJsx = (option, key) => {
const { renderer } = this.props
return <option
key={index}
value={index}
key={key}
value={key}
>
{labelProp ? option[labelProp] : option}
{renderer ? renderer(option) : option}
</option>
}
_onEditionMount = ref => {
this._select = ref
// Seems to work in Google Chrome (not in Firefox)
ref && ref.dispatchEvent(new window.MouseEvent('mousedown'))
}
_renderDisplay () {
return this.props.children ||
<span>{this.props.value[this.props.labelProp]}</span>
const { children, renderer, value } = this.props
return children ||
<span>{renderer ? renderer(value) : value}</span>
}
_renderEdition () {
const { saving } = this.state
const { saving, valueKey } = this.state
const { options } = this.props
return <select
autoFocus
className='form-control'
defaultValue={this._defaultValue}
className={classNames('form-control', styles.select)}
onBlur={this._closeEdition}
onChange={this._onChange}
onKeyDown={this._onKeyDown}
readOnly={saving}
ref={this._onEditionMount}
style={SELECT_STYLE}
value={valueKey}
>
{map(options, this._optionToJsx)}
</select>
@@ -373,9 +377,11 @@ export class Select extends Editable {
const MAP_TYPE_SELECT = {
host: SelectHost,
ip: SelectIp,
network: SelectNetwork,
pool: SelectPool,
remote: SelectRemote,
resourceSetIp: SelectResourceSetIp,
SR: SelectSr,
subject: SelectSubject,
tag: SelectTag,
@@ -385,7 +391,6 @@ const MAP_TYPE_SELECT = {
@propTypes({
labelProp: propTypes.string.isRequired,
predicate: propTypes.func,
value: propTypes.oneOfType([
propTypes.string,
propTypes.object
@@ -407,10 +412,9 @@ export class XoSelect extends Editable {
_renderEdition () {
const {
placeholder,
predicate,
saving,
xoType
xoType,
...props
} = this.props
const Select = MAP_TYPE_SELECT[xoType]
@@ -424,11 +428,10 @@ export class XoSelect extends Editable {
// when this element is clicked.
return <a onBlur={this._closeEdition}>
<Select
{...props}
autoFocus
disabled={saving}
onChange={this._onChange}
placeholder={placeholder}
predicate={predicate}
ref='select'
/>
</a>
@@ -461,15 +464,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

@@ -36,7 +36,16 @@ export class Password extends Component {
}
_generate = () => {
this.refs.field.value = randomPassword(8)
const value = randomPassword(8)
const isControlled = this.props.value !== undefined
if (isControlled) {
this.props.onChange(value)
} else {
this.refs.field.value = value
}
// FIXME: in controlled mode, visibility should only be updated
// when the value prop is changed according to the emitted value.
this.setState({
visible: true
})
@@ -159,86 +168,125 @@ const DEFAULT_UNIT = 'GiB'
readOnly: propTypes.bool,
required: propTypes.bool,
style: propTypes.object,
value: propTypes.number
value: propTypes.oneOfType([
propTypes.number,
propTypes.oneOf([ null ])
])
})
export class SizeInput extends BaseComponent {
constructor (props) {
super(props)
this.state = this._createStateFromBytes(firstDefined(props.value, props.defaultValue, 0))
this.state = this._createStateFromBytes(firstDefined(props.value, props.defaultValue, null))
}
componentWillReceiveProps (newProps) {
const { value } = newProps
if (value == null && value === this.props.value) {
return
}
const { _bytes, _unit, _value } = this
this._bytes = this._unit = this._value = null
if (value === _bytes) {
// Update input value
this.setState({
unit: _unit,
value: _value
})
} else {
componentWillReceiveProps (props) {
const { value } = props
if (value !== undefined && value !== this.props.value) {
this.setState(this._createStateFromBytes(value))
}
}
_createStateFromBytes = bytes => {
const humanSize = bytes && formatSizeRaw(bytes)
_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 {
unit: humanSize && humanSize.value ? humanSize.prefix + 'B' : this.props.defaultUnit || DEFAULT_UNIT,
value: humanSize ? round(humanSize.value, 3) : ''
input: String(round(value, 2)),
unit: `${prefix}B`
}
}
get value () {
const { unit, value } = this.state
return parseSize(value + ' ' + unit)
const { input, unit } = this.state
if (!input) {
return null
}
return parseSize(`${+input} ${unit}`)
}
set value (newValue) {
set value (value) {
if (
process.env.NODE_ENV !== 'production' &&
this.props.value != null
this.props.value !== undefined
) {
throw new Error('cannot set value of controlled SizeInput')
}
this.setState(this._createStateFromBytes(newValue))
this.setState(this._createStateFromBytes(value))
}
_onChange = value =>
this.props.onChange && this.props.onChange(value)
_onChange (input, unit) {
const { onChange } = this.props
_updateValue = event => {
const { value } = event.target
if (this.props.value != null) {
this._value = value
this._unit = this.state.unit
this._bytes = parseSize((value || 0) + ' ' + this.state.unit)
// Empty input equals null.
const bytes = input
? parseSize(`${+input} ${unit}`)
: null
this._onChange(this._bytes)
} else {
this.setState({ value }, () => {
this._onChange(this.value)
})
}
}
_updateUnit = unit => {
if (this.props.value != null) {
this._value = this.state.value
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
this._bytes = parseSize((this.state.value || 0) + ' ' + unit)
this._onChange(this._bytes)
} else {
this.setState({ unit }, () => {
this._onChange(this.value)
})
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 => {
const { input } = this.state
// 0 is always 0, no matter the unit.
if (+input) {
this._onChange(input, unit)
} else {
this.setState({ unit })
}
}
@@ -246,39 +294,30 @@ export class SizeInput extends BaseComponent {
const {
autoFocus,
className,
placeholder,
readOnly,
placeholder,
required,
style
} = this.props
const {
value,
unit
} = this.state
return <span
className={classNames(className, 'input-group')}
style={style}
>
return <span className={classNames('input-group', className)} style={style}>
<input
autoFocus={autoFocus}
className='form-control'
min={0}
onChange={this._updateValue}
disabled={readOnly}
onChange={this._updateNumber}
placeholder={placeholder}
readOnly={readOnly}
required={required}
type='number'
value={value}
type='text'
value={this.state.input}
/>
<span className='input-group-btn'>
<DropdownButton
bsStyle='secondary'
disabled={readOnly}
id='size'
pullRight
title={unit}
disabled={readOnly}
title={this.state.unit}
>
{map(UNITS, unit =>
<MenuItem

View File

@@ -1,8 +1,11 @@
import map from 'lodash/map'
import React, { Component } from 'react'
import ReactSelect from 'react-select'
import sum from 'lodash/sum'
import {
AutoSizer,
VirtualScroll
CellMeasurer,
List
} from 'react-virtualized'
import propTypes from '../prop-types'
@@ -15,15 +18,15 @@ const SELECT_STYLE = {
minWidth: '10em'
}
const MAX_OPTIONS = 5
// See: https://github.com/bvaughn/react-virtualized-select/blob/master/source/VirtualizedSelect/VirtualizedSelect.js
@propTypes({
maxHeight: propTypes.number,
optionHeight: propTypes.number
maxHeight: propTypes.number
})
export default class Select extends Component {
static defaultProps = {
maxHeight: 200,
optionHeight: 40,
optionRenderer: (option, labelKey) => option[labelKey]
}
@@ -32,34 +35,51 @@ export default class Select extends Component {
options,
...otherOptions
}) => {
const {
maxHeight,
optionHeight
} = this.props
const { maxHeight } = this.props
const focusedOptionIndex = options.indexOf(focusedOption)
const height = Math.min(maxHeight, options.length * optionHeight)
let height = options.length > MAX_OPTIONS && maxHeight
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
height={height}
rowCount={options.length}
rowHeight={optionHeight}
rowRenderer={wrappedRowRenderer}
scrollToIndex={focusedOptionIndex}
width={width}
/>
width ? (
<CellMeasurer
cellRenderer={({ rowIndex }) => wrappedRowRenderer({ index: rowIndex })}
columnCount={1}
rowCount={options.length}
// FIXME: 16 px: ugly workaround to take into account the scrollbar
// during the offscreen render to measure the row height
// See https://github.com/bvaughn/react-virtualized/issues/401
width={width - 16}
>
{({ getRowHeight }) => {
if (options.length <= MAX_OPTIONS) {
height = sum(map(options, (_, index) => getRowHeight({ index })))
}
return <List
height={height}
rowCount={options.length}
rowHeight={getRowHeight}
rowRenderer={wrappedRowRenderer}
scrollToIndex={focusedOptionIndex}
width={width}
/>
}}
</CellMeasurer>
) : null
)}
</AutoSizer>
)
@@ -68,8 +88,10 @@ export default class Select extends Component {
_optionRenderer = ({
focusedOption,
focusOption,
key,
labelKey,
option,
style,
selectValue
}) => {
let className = 'Select-option'
@@ -91,7 +113,8 @@ export default class Select extends Component {
className={className}
onClick={!disabled && (() => selectValue(option))}
onMouseOver={!disabled && (() => focusOption(option))}
style={{ height: props.optionHeight }}
style={style}
key={key}
>
{props.optionRenderer(option, labelKey)}
</div>
@@ -102,6 +125,7 @@ export default class Select extends Component {
return (
<ReactSelect
{...this.props}
backspaceToRemoveMessage=''
menuRenderer={this._renderMenu}
menuStyle={SELECT_MENU_STYLE}
style={SELECT_STYLE}

View File

@@ -14,3 +14,7 @@ export const host = {
export const pool = {
homeFilterTags: 'tags:'
}
export const vmTemplate = {
homeFilterTags: 'tags:'
}

View File

@@ -1,4 +1,5 @@
import isEmpty from 'lodash/isEmpty'
import keys from 'lodash/keys'
import map from 'lodash/map'
import React from 'react'
import { Portal } from 'react-overlays'
@@ -19,7 +20,8 @@ import {
} from './selectors'
import {
getHostMissingPatches,
installAllHostPatches
installAllHostPatches,
installAllPatchesOnPool
} from './xo'
// ===================================================================
@@ -84,9 +86,17 @@ class HostsPatchesTable extends Component {
)
)
_installAllMissingPatches = () => (
Promise.all(map(this._getHosts(), this._installAllHostPatches))
)
_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 => {
@@ -164,15 +174,15 @@ class HostsPatchesTable extends Component {
<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
}}
/>
<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()}>

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,12 @@ var forEach = require('lodash/forEach')
var isString = require('lodash/isString')
var messages = {
statusConnecting: 'Connecting',
statusDisconnected: 'Disconnected',
statusLoading: 'Loading…',
errorPageNotFound: 'Page not found',
errorNoSuchItem: 'no such item',
editableLongClickPlaceholder: 'Long click to edit',
editableClickPlaceholder: 'Click to edit',
@@ -17,19 +23,24 @@ var messages = {
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',
@@ -39,6 +50,9 @@ var messages = {
settingsGroupsPage: 'Groups',
settingsAclsPage: 'ACLs',
settingsPluginsPage: 'Plugins',
settingsLogsPage: 'Logs',
settingsIpsPage: 'IPs',
settingsConfigPage: 'Config',
aboutPage: 'About',
newMenu: 'New',
taskMenu: 'Tasks',
@@ -65,9 +79,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!',
@@ -84,11 +105,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',
@@ -112,6 +134,7 @@ var messages = {
homeMore: 'More',
homeMigrateTo: 'Migrate to…',
homeMissingPaths: 'Missing patches',
homePoolMaster: 'Master:',
highAvailability: 'High Availability',
// ----- Forms -----
@@ -134,12 +157,15 @@ var messages = {
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)…',
selectIpPool: 'Select IP pool(s)…',
fillRequiredInformations: 'Fill required informations.',
fillOptionalInformations: 'Fill informations (optional)',
selectTableReset: 'Reset',
@@ -170,6 +196,7 @@ var messages = {
job: 'Job',
jobId: 'Job ID',
jobName: 'Name',
jobNamePlaceholder: 'Name of your job (forbidden: "_")',
jobStart: 'Start',
jobEnd: 'End',
jobDuration: 'Duration',
@@ -194,12 +221,26 @@ 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.',
editBackupVmsTitle: 'VMs',
editBackupSmartStatusTitle: 'VMs statuses',
editBackupSmartResidentOn: 'Resident on',
editBackupSmartTagsTitle: 'VMs Tags',
editBackupTagTitle: 'Tag',
editBackupReportTitle: 'Report',
editBackupReportEnable: 'Enable immediately after creation',
editBackupDepthTitle: 'Depth',
editBackupRemoteTitle: 'Remote',
// ------ New Remote -----
remoteList: 'Remote stores for backup',
@@ -217,10 +258,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',
@@ -239,11 +304,21 @@ var messages = {
newSrInUse: 'in use',
newSrSize: 'Size',
newSrCreate: 'Create',
newSrNamePlaceHolder: 'Storage name',
newSrDescPlaceHolder: 'Storage description',
newSrAddressPlaceHolder: 'Address',
newSrPortPlaceHolder: '[port]',
newSrUsernamePlaceHolder: 'Username',
newSrPasswordPlaceHolder: 'Password',
newSrLvmDevicePlaceHolder: 'Device, e.g /dev/sda…',
newSrLocalPathPlaceHolder: '/path/to/directory',
// ----- 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',
@@ -252,6 +327,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',
@@ -329,6 +405,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 -----
@@ -340,6 +420,7 @@ var messages = {
hostDescription: 'Description',
hostMemory: 'Memory',
noHost: 'No hosts',
memoryLeftTooltip: '{used}% used ({free} free)',
// ----- Pool network tab -----
poolNetworkNameLabel: 'Name',
poolNetworkDescription: 'Description',
@@ -348,6 +429,8 @@ var messages = {
poolNetworkMTU: 'MTU',
poolNetworkPifAttached: 'Connected',
poolNetworkPifDetached: 'Disconnected',
showPifs: 'Show PIFs',
hidePifs: 'Hide PIFs',
// ----- Pool actions ------
addSrLabel: 'Add SR',
addVmLabel: 'Add VM',
@@ -362,6 +445,7 @@ var messages = {
restartHostAgent: 'Restart toolstack',
forceRebootHostLabel: 'Force reboot',
rebootHostLabel: 'Reboot',
rebootUpdateHostLabel: 'Reboot for applying updates',
emergencyModeLabel: 'Emergency mode',
// ----- Host tabs -----
storageTabName: 'Storage',
@@ -390,23 +474,37 @@ var messages = {
hostLicenseExpiry: 'Expiry',
// ----- Host net tabs -----
networkCreateButton: 'Add a network',
networkCreateBondedButton: 'Add a bonded network',
pifDeviceLabel: 'Device',
pifNetworkLabel: 'Network',
pifVlanLabel: 'VLAN',
pifAddressLabel: 'Address',
pifModeLabel: 'Mode',
pifMacLabel: 'MAC',
pifMtuLabel: 'MTU',
pifStatusLabel: 'Status',
pifStatusConnected: 'Connected',
pifStatusDisconnected: 'Disconnected',
pifNoInterface: 'No physical interface detected',
pifInUse: 'This interface is currently in use',
defaultLockingMode: 'Default locking mode',
pifConfigureIp: 'Configure IP address',
configIpErrorTitle: 'Invalid parameters',
configIpErrorMessage: 'IP address and netmask required',
staticIp: 'Static IP address',
netmask: 'Netmask',
dns: 'DNS',
gateway: 'Gateway',
// ----- 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',
@@ -429,11 +527,15 @@ var messages = {
// ----- 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',
@@ -476,6 +578,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',
@@ -488,11 +605,27 @@ var messages = {
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',
vdbBootable: 'Bootable',
vdbReadonly: 'Readonly',
vdbCreate: 'Create',
vdbNamePlaceHolder: 'Disk name',
vdbSizePlaceHolder: 'Size',
saveBootOption: 'Save',
resetBootOption: 'Reset',
// ----- VM network tab -----
vifCreateDeviceButton: 'New device',
@@ -504,8 +637,18 @@ 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',
vifCreate: 'Create',
// ----- VM snapshot tab -----
noSnapshots: 'No snapshots',
@@ -513,6 +656,8 @@ var messages = {
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',
@@ -564,6 +709,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}}',
@@ -587,6 +740,9 @@ var messages = {
srUsageStatePanel: 'Storage Usage',
srTopUsageStatePanel: 'Top 5 SR Usage (in %)',
vmsStates: '{running} running ({halted} halted)',
dashboardStatsButtonRemoveAll: 'Clear selection',
dashboardStatsButtonAddAllHost: 'Add all hosts',
dashboardStatsButtonAddAllVM: 'Add all VMs',
// --- Stats board --
weekHeatmapData: '{value} {date, date, medium}',
@@ -604,10 +760,10 @@ var messages = {
comingSoon: 'Coming soon!',
// ----- Health -----
orphanedVdis: 'Orphaned VDIs',
orphanedVms: 'Orphaned VMs',
orphanedVdis: 'Orphaned snapshot VDIs',
orphanedVms: 'Orphaned VMs snapshot',
noOrphanedObject: 'No orphans',
removeAllOrphanedObject: 'Remove all orphaned VDIs',
removeAllOrphanedObject: 'Remove all orphaned snapshot VDIs',
vmNameLabel: 'Name',
vmNameDescription: 'Description',
vmContainer: 'Resident on',
@@ -618,6 +774,7 @@ var messages = {
alarmObject: 'Issue on',
alarmPool: 'Pool',
alarmRemoveAll: 'Remove all alarms',
spaceLeftTooltip: '{used}% used ({free} left)',
// ----- New VM -----
newVmCreateNewVmOn: 'Create a new VM on {select}',
@@ -630,9 +787,13 @@ 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',
newVmInstallNetworkPlaceHolder: 'e.g: http://httpredir.debian.org/debian',
newVmPvArgsLabel: 'PV Args',
newVmPxeLabel: 'PXE',
newVmInterfacesPanel: 'Interfaces',
@@ -664,12 +825,17 @@ var messages = {
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',
@@ -689,13 +855,16 @@ var messages = {
maxCpus: 'Maximum CPUs',
maxRam: 'Maximum RAM (GiB)',
maxDiskSpace: 'Maximum disk space',
ipPool: 'IP pool',
quantity: 'Quantity',
noResourceSetLimits: 'No limits.',
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:',
@@ -705,26 +874,40 @@ 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',
restoreBackupsInfo: 'Click on a VM to display restore options',
remoteEnabled: 'Enabled',
remoteError: 'Error',
noBackup: 'No backup available',
backupVmNameColumn: 'VM Name',
backupTagColumn: 'Backup Tag',
backupTags: 'Tags',
lastBackupColumn: 'Last Backup',
availableBackupsColumn: 'Available Backups',
restoreColumn: 'Restore',
restoreTip: 'Restore VM',
backupRestoreErrorTitle: 'Missing parameters',
backupRestoreErrorMessage: 'Choose a SR and a backup',
displayBackup: 'Display backups',
importBackupTitle: 'Import VM',
importBackupMessage: 'Starting your backup import',
vmsToBackup: 'VMs to backup',
@@ -733,7 +916,9 @@ var messages = {
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?',
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}}',
@@ -754,10 +939,10 @@ var messages = {
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:',
@@ -773,12 +958,17 @@ var messages = {
migrateVmNetwork: 'Network',
migrateVmNoTargetHost: 'No target host',
migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',
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',
revertVmModalMessage: 'You are about to revert your VM to the snapshot state. This operation is irreversible',
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…',
removeAllOrphanedModalWarning: 'Are you sure you want to remove all orphaned VDIs?',
removeAllOrphanedModalWarning: 'Are you sure you want to remove all orphaned snapshot VDIs?',
removeAllLogsModalTitle: 'Remove all logs',
removeAllLogsModalWarning: 'Are you sure you want to remove all logs?',
definitiveMessageModal: 'This operation is definitive.',
@@ -797,6 +987,11 @@ 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',
@@ -810,8 +1005,14 @@ var messages = {
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',
@@ -819,8 +1020,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',
@@ -842,7 +1053,7 @@ var messages = {
proSupportIncluded: 'Pro support included',
xoAccount: 'Acces your XO Account',
openTicket: 'Report a problem',
openTicketText: 'Problem? Open a ticket !',
openTicketText: 'Problem? Open a ticket!',
// ----- Upgrade Panel -----
upgradeNeeded: 'Upgrade needed',
@@ -850,19 +1061,29 @@ 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',
proxySettingsHostPlaceHolder: 'Host (myproxy.example.org)',
proxySettingsPortPlaceHolder: 'Port (eg: 3128)',
proxySettingsUsernamePlaceHolder: 'Username',
proxySettingsPasswordPlaceHolder: 'Password',
updateRegistrationEmailPlaceHolder: 'Your email account',
updateRegistrationPasswordPlaceHolder: 'Your password',
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',
considerSubscribe: 'Please consider subscribe and try it with all features for free during 15 days on {link}.',
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.',
@@ -918,7 +1139,70 @@ var messages = {
deleteSshKeyConfirmMessage: 'Are you sure you want to delete the SSH key {title}?',
// ----- Usage -----
others: 'Others'
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',
// ----- Settings/ACLs -----
settingsAclsButtonTooltipVM: 'VM',
settingsAclsButtonTooltiphost: 'Hosts',
settingsAclsButtonTooltippool: 'Pool',
settingsAclsButtonTooltipSR: 'SR',
settingsAclsButtonTooltipnetwork: 'Network',
// ----- Config -----
noConfigFile: 'No config file selected',
importTip: 'Try dropping a config file here, or click to select a config file to upload.',
config: 'Config',
importConfig: 'Import',
importConfigSuccess: 'Config file successfully imported',
importConfigError: 'Error while importing config file',
exportConfig: 'Export',
downloadConfig: 'Download current config',
noConfigImportCommunity: 'No config import available for Community Edition'
}
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

@@ -31,7 +31,7 @@ class ArrayItem extends Component {
{cloneElement(children, {
ref: 'input'
})}
<button disabled={children.props.disabled} className='btn btn-danger pull-xs-right' type='button' onClick={this.props.onDelete}>
<button disabled={children.props.disabled} className='btn btn-danger pull-right' type='button' onClick={this.props.onDelete}>
{_('remove')}
</button>
</li>
@@ -53,11 +53,13 @@ class ArrayItem extends Component {
export default class ArrayInput extends Component {
constructor (props) {
super(props)
this._nextChildKey = 0
this.state = {
use: props.required || forceDisplayOptionalAttr(props),
children: this._makeChildren(props)
}
this._nextChildKey = 0
}
get value () {
@@ -91,7 +93,7 @@ export default class ArrayInput extends Component {
})
}
_makeChild (props) {
_makeChild (props, defaultValue) {
const key = String(this._nextChildKey++)
const {
schema: {
@@ -108,21 +110,16 @@ export default class ArrayInput extends Component {
required
schema={items}
uiSchema={props.uiSchema.items}
defaultValue={props.defaultValue}
defaultValue={defaultValue}
/>
</ArrayItem>
)
}
_makeChildren ({ defaultValue, ...props }) {
return map(defaultValue, defaultValue => {
return (
this._makeChild({
...props,
defaultValue
})
)
})
_makeChildren (props) {
return map(props.defaultValue, defaultValue =>
this._makeChild(props, defaultValue)
)
}
componentWillReceiveProps (props) {
@@ -175,7 +172,7 @@ export default class ArrayInput extends Component {
cloneElement(child, { ref: index })
)}
</ul>
<button disabled={disabled} className='btn btn-primary pull-xs-right m-t-1 m-r-1' type='button' onClick={this._handleAdd}>
<button disabled={disabled} className='btn btn-primary pull-right mt-1 mr-1' type='button' onClick={this._handleAdd}>
{_('add')}
</button>
</div>

View File

@@ -29,7 +29,7 @@ class ObjectItem extends Component {
const { props } = this
return (
<div className='p-b-1'>
<div className='pb-1'>
{cloneElement(props.children, {
ref: 'input'
})}

View File

@@ -6,6 +6,10 @@ import React, { Component, cloneElement } from 'react'
import { Button, Modal as ReactModal } from 'react-bootstrap-4/lib'
import propTypes from './prop-types'
import {
disable as disableShortcuts,
enable as enableShortcuts
} from './shortcuts'
let instance
@@ -15,7 +19,7 @@ const modal = (content, onClose) => {
} else if (instance.state.showModal) {
throw new Error('Other modal still open.')
}
instance.setState({ content, onClose, showModal: true })
instance.setState({ content, onClose, showModal: true }, disableShortcuts)
}
export const alert = (title, body) => {
@@ -60,7 +64,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 +86,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>
@@ -141,7 +149,7 @@ export default class Modal extends Component {
}
close () {
this.setState({ showModal: false })
this.setState({ showModal: false }, enableShortcuts)
}
_onHide = () => {

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,5 @@
import React, { Component } from 'react'
import _ from 'intl'
import React from 'react'
import Icon from './icon'
import propTypes from './prop-types'
@@ -55,13 +56,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>
)
}))
@@ -113,6 +113,22 @@ const xoItemToRender = {
<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 => (
@@ -166,7 +182,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}
@@ -199,7 +218,7 @@ const GenericXoItem = connectStore(() => {
})
})(({ xoItem, ...props }) => xoItem
? renderXoItem(xoItem, props)
: <span className='text-muted'>no such item</span>
: <span className='text-muted'>{_('errorNoSuchItem')}</span>
)
export const renderXoItemFromId = (id, props) => <GenericXoItem {...props} id={id} />

View File

@@ -4,7 +4,7 @@ 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 {
Tab,
Tabs
@@ -32,8 +32,10 @@ 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 = (() => {
@@ -52,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 = []
@@ -111,12 +117,12 @@ const TIME_FORMAT = {
// 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' />
// ===================================================================
@@ -140,7 +146,7 @@ export class SchedulePreview extends Component {
<div className='alert alert-info' role='alert'>
{_('cronPattern')} <strong>{cronPattern}</strong>
</div>
<div className='form-inline p-b-1'>
<div className='form-inline pb-1'>
<Range min={MIN_PREVIEWS} max={MAX_PREVIEWS} onChange={this._handleChange} />
</div>
<ul className='list-group'>
@@ -248,7 +254,7 @@ class TableSelect extends Component {
))}
</tbody>
</table>
<button className='btn btn-secondary pull-xs-right' onClick={this._reset}>
<button className='btn btn-secondary pull-right' onClick={this._reset}>
{_('selectTableReset')}
</button>
</div>
@@ -353,14 +359,14 @@ class TimePicker extends Component {
<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>
</Tabs>
<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>
</Tabs>
) : tableSelect
}
</CardBlock>

View File

@@ -5,9 +5,13 @@ import filter from 'lodash/filter'
import flatten from 'lodash/flatten'
import forEach from 'lodash/forEach'
import groupBy from 'lodash/groupBy'
import includes from 'lodash/includes'
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'
@@ -25,13 +29,16 @@ import {
getObject
} from './selectors'
import {
addSubscriptions,
connectStore,
mapPlus,
resolveResourceSets
} from './utils'
import {
isSrWritable,
subscribeCurrentUser,
subscribeGroups,
subscribeIpPools,
subscribeRemotes,
subscribeResourceSets,
subscribeRoles,
@@ -50,6 +57,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,
@@ -82,7 +115,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)
@@ -96,11 +129,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
: ''
}
@@ -202,14 +235,14 @@ 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.
_renderOption = option => (
<span
className={classNames(
!option.disabled && this.props.xoContainers && 'm-l-1'
!option.disabled && this.props.xoContainers && 'ml-1'
)}
>
{renderXoItem(option.xoItem)}
@@ -267,13 +300,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))
}
}
)
}
@@ -296,7 +344,7 @@ const makeSubscriptionSelect = (subscribe, props) => (
{...props}
{...this.props}
xoObjects={this._getFilteredXoObjects()}
xoContainers={this.state.xoContainers}
xoContainers={this.state.xoContainers && this._getFilteredXoContainers()}
/>
)
}
@@ -419,7 +467,7 @@ export const SelectVmTemplate = makeStoreSelect(() => {
xoObjects: getVmTemplatesByPool,
xoContainers: getPools
}
}, { placeholder: _('selectVms') })
}, { placeholder: _('selectVmTemplates') })
// ===================================================================
@@ -609,14 +657,6 @@ export class SelectResourceSetsVmTemplate extends Component {
this.refs.select.value = value
}
componentWillMount () {
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
this.setState({
resourceSets: resolveResourceSets(resourceSets)
})
})
}
_getTemplates = createSelector(
() => this.props.resourceSet,
({ objectsByType }) => {
@@ -648,15 +688,6 @@ export class SelectResourceSetsSr extends Component {
set value (value) {
this.refs.select.value = value
}
componentWillMount () {
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
this.setState({
resourceSets: resolveResourceSets(resourceSets)
})
})
}
_getSrs = createSelector(
() => this.props.resourceSet,
({ objectsByType }) => {
@@ -689,14 +720,6 @@ export class SelectResourceSetsVdi extends Component {
this.refs.select.value = value
}
componentWillMount () {
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
this.setState({
resourceSets: resolveResourceSets(resourceSets)
})
})
}
_getObject (id) {
return getObject(store.getState(), id, true)
}
@@ -738,14 +761,6 @@ export class SelectResourceSetsNetwork extends Component {
this.refs.select.value = value
}
componentWillMount () {
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
this.setState({
resourceSets: resolveResourceSets(resourceSets)
})
})
}
_getNetworks = createSelector(
() => this.props.resourceSet,
({ objectsByType }) => {
@@ -766,3 +781,147 @@ export class SelectResourceSetsNetwork extends Component {
)
}
}
// ===================================================================
// Pass a function to @addSubscriptions to ensure subscribeIpPools and subscribeResourceSets
// are correctly imported before they are called
@addSubscriptions(() => ({
ipPools: subscribeIpPools,
resourceSets: subscribeResourceSets
}))
@propTypes({
containerPredicate: propTypes.func,
predicate: propTypes.func,
resourceSetId: propTypes.string.isRequired
})
export class SelectResourceSetIp extends Component {
get value () {
return this.refs.select.value
}
set value (value) {
this.refs.select.value = value
}
_getResourceSetIpPools = createSelector(
() => this.props.ipPools,
() => this.props.resourceSets,
() => this.props.resourceSetId,
(allIpPools, allResourceSets, resourceSetId) => {
const { ipPools } = allResourceSets[resourceSetId]
return filter(allIpPools, ({ id }) => includes(ipPools, id))
}
)
_getIpPools = createSelector(
() => this.props.ipPools,
() => this.props.containerPredicate,
(ipPools, predicate) => predicate
? filter(ipPools, predicate)
: ipPools
)
_getIps = createSelector(
this._getIpPools,
() => this.props.predicate,
() => this.props.ipPools,
(ipPools, predicate, resolvedIpPools) => {
return flatten(
map(ipPools, ipPool => {
const poolIps = map(ipPool.addresses, (address, ip) => ({
...address,
id: ip,
label: ip,
type: 'ipAddress',
used: !isEmpty(address.vifs)
}))
return predicate ? filter(poolIps, predicate) : poolIps
})
)
}
)
render () {
return (
<GenericSelect
ref='select'
placeholder={_('selectIpPool')}
{...this.props}
xoObjects={this._getIps()}
/>
)
}
}
// ===================================================================
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') })
// ===================================================================
export const SelectIpPool = makeSubscriptionSelect(subscriber => {
const unsubscribeIpPools = subscribeIpPools(ipPools => {
subscriber({
xoObjects: map(sortBy(ipPools, 'name'), ipPool => ({ ...ipPool, type: 'ipPool' }))
})
})
return unsubscribeIpPools
}, { placeholder: _('selectIpPool') })

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,6 +243,12 @@ const _getPermissionsPredicate = invoke(() => {
}
})
export const isAdmin = (...args) => {
const user = getUser(...args)
return user && user.permission === 'admin'
}
// ===================================================================
// Common selector creators.
@@ -320,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 => {
@@ -340,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
}
@@ -360,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 => {
@@ -414,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,8 +1,12 @@
.clickableColumn {
cursor: pointer;
cursor: pointer;
}
.clickableColumn:hover {
color: #fff;
background-color: #96b8d1;
color: #fff;
background-color: #96b8d1;
}
.clickableRow {
cursor: pointer;
}

View File

@@ -1,6 +1,8 @@
import _ from 'intl'
import ceil from 'lodash/ceil'
import classNames from 'classnames'
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'
@@ -108,19 +110,18 @@ class ColumnHead extends Component {
return <th>{name}</th>
}
let className = styles.clickableColumn
if (sortIcon === 'asc' || sortIcon === 'desc') {
className += ' bg-info'
}
const isSelected = sortIcon === 'asc' || sortIcon === 'desc'
return (
<th
className={className}
className={classNames(
styles.clickableColumn,
isSelected && classNames('text-white', 'bg-info')
)}
onClick={this._sort}
>
{name}
<span className='pull-xs-right'>
<span className='pull-right'>
<Icon icon={sortIcon} />
</span>
</th>
@@ -139,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([
@@ -151,6 +153,7 @@ const DEFAULT_ITEMS_PER_PAGE = 10
filters: propTypes.object,
itemsPerPage: propTypes.number,
paginationContainer: propTypes.func,
rowAction: propTypes.func,
rowLink: propTypes.oneOfType([
propTypes.func,
propTypes.string
@@ -161,8 +164,17 @@ 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
}
@@ -252,6 +264,7 @@ export default class SortedTable extends Component {
paginationContainer,
filterContainer,
filters,
rowAction,
rowLink,
userData
} = props
@@ -300,7 +313,7 @@ export default class SortedTable extends Component {
</thead>
<tbody>
{map(this._getVisibleItems(), (item, i) => {
const colums = map(props.columns, (column, key) => (
const columns = map(props.columns, (column, key) => (
<td key={key}>
{column.itemRenderer(item, userData)}
</td>
@@ -313,8 +326,14 @@ export default class SortedTable extends Component {
key={id}
tagName='tr'
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
>{colums}</BlockLink>
: <tr key={id}>{colums}</tr>
>{columns}</BlockLink>
: <tr
className={rowAction && styles.clickableRow}
key={id}
onClick={rowAction && (() => rowAction(item, userData))}
>
{columns}
</tr>
})}
</tbody>
</table>
@@ -324,19 +343,19 @@ export default class SortedTable extends Component {
<Col mediumSize={8}>
{paginationContainer
? (
// Rebuild container function to refresh Portal component.
<Portal container={() => paginationContainer()}>
{paginationInstance}
</Portal>
// Rebuild container function to refresh Portal component.
<Portal container={() => paginationContainer()}>
{paginationInstance}
</Portal>
) : paginationInstance
}
</Col>
<Col mediumSize={4}>
{filterContainer
? (
<Portal container={() => filterContainer()}>
{filterInstance}
</Portal>
<Portal container={() => filterContainer()}>
{filterInstance}
</Portal>
) : filterInstance
}
</Col>

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

@@ -75,7 +75,7 @@ export default class TimezonePicker extends Component {
{_('timezonePickerServerValue')} <strong>{state.serverTimezone}</strong>
</div>
<Select
className='m-b-1'
className='mb-1'
defaultValue={props.defaultValue}
onChange={this._handleChange}
options={state.options}
@@ -86,7 +86,7 @@ export default class TimezonePicker extends Component {
<div className='pull-right'>
<ActionButton
btnStyle='primary'
className='m-r-1'
className='mr-1'
handler={this._useServerTime}
icon='time'
>

View File

@@ -50,3 +50,23 @@ Element.propTypes = {
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,7 +1,7 @@
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'
@@ -13,13 +13,16 @@ 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 * as actions from './store/actions'
import BaseComponent from './base-component'
import invoke from './invoke'
import store from './store'
import { getObject } from './selectors'
export const EMPTY_ARRAY = Object.freeze([ ])
export const EMPTY_OBJECT = Object.freeze({ })
@@ -48,6 +51,8 @@ export const propsEqual = (o1, o2, props) => {
// ===================================================================
// `subscriptions` can be a function if we want to ensure that the subscription
// callbacks have been correctly initialized when there are circular dependencies
export const addSubscriptions = subscriptions => Component => {
class SubscriptionWrapper extends BaseComponent {
constructor () {
@@ -57,7 +62,7 @@ export const addSubscriptions = subscriptions => Component => {
}
componentWillMount () {
this._unsubscribes = map(subscriptions, (subscribe, prop) =>
this._unsubscribes = map(isFunction(subscriptions) ? subscriptions() : subscriptions, (subscribe, prop) =>
subscribe(value => this.setState({ [prop]: value }))
)
}
@@ -180,11 +185,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
@@ -209,7 +215,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
}
@@ -222,10 +228,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' ],
@@ -281,7 +287,7 @@ export const normalizeXenToolsStatus = status => {
// -------------------------------------------------------------------
const _NotFound = () => <h1>Page not found</h1>
const _NotFound = () => <h1>{_('errorPageNotFound')}</h1>
// Decorator to declare routes on a component.
//
@@ -359,38 +365,44 @@ export function rethrow (cb) {
// ===================================================================
export const resolveResourceSets = resourceSets => (
map(resourceSets, resourceSet => {
const { objects, ...attrs } = resourceSet
const resolvedObjects = {}
const resolvedSet = {
...attrs,
missingObjects: [],
objectsByType: resolvedObjects
export const resolveResourceSet = resourceSet => {
if (!resourceSet) {
return
}
const { objects, ipPools, ...attrs } = resourceSet
const resolvedObjects = {}
const resolvedSet = {
...attrs,
missingObjects: [],
objectsByType: resolvedObjects,
ipPools
}
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 state = store.getState()
forEach(objects, id => {
const object = getObject(state, id, true) // true: useResourceSet to bypass permissions
const { type } = object
// Error, missing resource.
if (!object) {
resolvedSet.missingObjects.push(id)
return
}
const { type } = object
if (!resolvedObjects[type]) {
resolvedObjects[type] = [ object ]
} else {
resolvedObjects[type].push(object)
}
})
return resolvedSet
if (!resolvedObjects[type]) {
resolvedObjects[type] = [ object ]
} else {
resolvedObjects[type].push(object)
}
})
)
return resolvedSet
}
export const resolveResourceSets = resourceSets =>
map(resourceSets, resolveResourceSet)
// -------------------------------------------------------------------
@@ -417,3 +429,36 @@ export function buildTemplate (pattern, rules) {
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

@@ -14,6 +14,7 @@ const STYLE = {}
const WIDTH = 120
const HEIGHT = 20
const STROKE_WIDTH = 0.5
// ===================================================================
@@ -26,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) {
@@ -34,15 +35,15 @@ export const CpuSparkLines = propTypes({
}
return (
<Sparklines style={STYLE} data={computeArraysAvg(cpus)} max={100} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#366e98', fill: '#366e98', fillOpacity: 0.5 }} color='#2598d9' />
<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) {
@@ -50,15 +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: 0.5, stroke: '#990822', fill: '#990822', fillOpacity: 0.5 }} color='#cc0066' />
<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) {
@@ -66,15 +67,15 @@ export const XvdSparkLines = propTypes({
}
return (
<Sparklines style={STYLE} data={computeObjectsAvg(xvds)} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#089944', fill: '#089944', fillOpacity: 0.5 }} color='#33cc33' />
<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) {
@@ -82,15 +83,15 @@ export const VifSparkLines = propTypes({
}
return (
<Sparklines style={STYLE} data={computeObjectsAvg(vifs)} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
<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) {
@@ -98,15 +99,15 @@ export const PifSparkLines = propTypes({
}
return (
<Sparklines style={STYLE} data={computeObjectsAvg(pifs)} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
<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) {
@@ -114,8 +115,8 @@ export const LoadSparkLines = propTypes({
}
return (
<Sparklines style={STYLE} data={load} min={0} width={WIDTH} height={HEIGHT}>
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#33cc33', fill: '#33cc33', fillOpacity: 0.5 }} color='#33cc33' />
<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

@@ -378,7 +378,7 @@ export default class XoWeekCharts extends Component {
return (
<div>
<div>
<p className='m-t-1'>
<p className='mt-1'>
{_('weeklyChartsScaleInfo')}
{' '}
<Toggle iconSize={1} icon='scale' className='btn btn-secondary' onChange={this._updateScale} />

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

@@ -9,6 +9,8 @@ import { Toggle } from '../../form'
import { injectIntl } from 'react-intl'
class CopyVmModalBody extends Component {
state = { compress: false }
get value () {
const { state } = this
return {

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,

View File

@@ -39,8 +39,9 @@ export const XEN_DEFAULT_CPU_CAP = 0
// ===================================================================
export const isSrWritable = sr => sr.content_type !== 'iso' && sr.size > 0
export const isSrShared = sr => sr.$PBDs.length > 1
export const isSrWritable = sr => sr && sr.content_type !== 'iso' && sr.size > 0
export const isSrShared = sr => sr && sr.$PBDs.length > 1
export const isVmRunning = vm => vm && vm.power_state === 'Running'
// ===================================================================
@@ -49,6 +50,12 @@ export const signOut = () => {
window.location.reload(true)
}
export const connect = () => {
xo.open(createBackoff()).catch(error => {
logError(error, 'failed to connect to xo-server')
})
}
const xo = invoke(() => {
const token = cookies.get('token')
if (!token) {
@@ -60,13 +67,7 @@ const xo = invoke(() => {
credentials: { token }
})
const connect = () => {
xo.open(createBackoff()).catch(error => {
logError(error, 'failed to connect to xo-server')
})
}
connect()
xo.on('authenticationFailure', signOut)
xo.on('scheduledAttempt', ({ delay }) => {
console.warn('next attempt in %s ms', delay)
})
@@ -75,6 +76,7 @@ const xo = invoke(() => {
return xo
})
connect()
const _signIn = new Promise(resolve => xo.once('authenticated', resolve))
@@ -159,11 +161,20 @@ const createSubscription = cb => {
if (!isEqual(result, cache)) {
cache = result
/* FIXME: Edge case:
* 1) MyComponent has a subscription with subscribers[1]
* 2) subscribers[0] causes the MyComponent unmounting (and thus its unsubscription)
* When subscribers[1] will be executed, it will no longer exist,
* which will throw an error (Uncaught (in promise) TypeError: subscriber is not a function)
*/
forEach(subscribers, subscriber => {
subscriber(result)
})
}
}, ::console.error)
}, error => {
running = false
console.error(error)
})
}
const subscribe = cb => {
@@ -206,6 +217,8 @@ export const subscribeJobs = createSubscription(() => _call('job.getAll'))
export const subscribeJobsLogs = createSubscription(() => _call('log.get', {namespace: 'jobs'}))
export const subscribeApiLogs = createSubscription(() => _call('log.get', {namespace: 'api'}))
export const subscribePermissions = createSubscription(() => _call('acl.getCurrentPermissions'))
export const subscribePlugins = createSubscription(() => _call('plugin.get'))
@@ -244,6 +257,8 @@ export const subscribeRoles = createSubscription(invoke(
sort => () => _call('role.getAll').then(sort)
))
export const subscribeIpPools = createSubscription(() => _call('ipPool.getAll'))
// System ============================================================
export const apiMethods = _call('system.getMethodsInfo')
@@ -269,6 +284,22 @@ const resolveIds = params => {
return params
}
// XO --------------------------------------------------------------------------
export const importConfig = config => (
_call('xo.importConfig').then(({ $sendTo: url }) =>
request.post(url).send(config).then(response => {
if (response.status !== 200) {
throw new Error('config import failed')
}
})
)
)
export const exportConfig = () => (
_call('xo.exportConfig').then(({ $getFrom: url }) => { window.location = `.${url}` })
)
// Server ------------------------------------------------------------
export const addServer = (host, username, password) => (
@@ -277,26 +308,26 @@ export const addServer = (host, username, password) => (
)
)
export const editServer = ({ id }, { host, username, password, readOnly }) => (
_call('server.set', { id, host, username, password, readOnly })::tap(
export const editServer = (server, { host, username, password, readOnly }) => (
_call('server.set', { id: resolveId(server), host, username, password, readOnly })::tap(
subscribeServers.forceRefresh
)
)
export const connectServer = ({ id }) => (
_call('server.connect', { id })::tap(
export const connectServer = server => (
_call('server.connect', { id: resolveId(server) })::tap(
subscribeServers.forceRefresh
)
)
export const disconnectServer = ({ id }) => (
_call('server.disconnect', { id })::tap(
export const disconnectServer = server => (
_call('server.disconnect', { id: resolveId(server) })::tap(
subscribeServers.forceRefresh
)
)
export const removeServer = ({ id }) => (
_call('server.remove', { id })::tap(
export const removeServer = server => (
_call('server.remove', { id: resolveId(server) })::tap(
subscribeServers.forceRefresh
)
)
@@ -307,6 +338,46 @@ export const editPool = (pool, props) => (
_call('pool.set', { id: resolveId(pool), ...props })
)
import AddHostModalBody from './add-host-modal'
export const addHostToPool = (pool, host) => {
if (host) {
return confirm({
title: _('addHostModalTitle'),
body: _('addHostModalMessage', { pool: pool.name_label, host: host.name_label })
}).then(() =>
_call('pool.mergeInto', { source: host.$pool, target: pool.id, force: true })
)
}
return confirm({
title: _('addHostModalTitle'),
body: <AddHostModalBody pool={pool} />
}).then(
params => {
if (!params.host) {
error(_('addHostNoHost'), _('addHostNoHostMessage'))
return
}
_call('pool.mergeInto', { source: params.host.$pool, target: pool.id, force: true })
},
noop
)
}
export const detachHost = host => (
confirm({
icon: 'host-eject',
title: _('detachHostModalTitle'),
body: _('detachHostModalMessage', {host: <strong>{host.name_label}</strong>})
}).then(
() => _call('host.detach', { host: host.id })
)
)
export const setDefaultSr = sr => (
_call('pool.setDefaultSr', {sr: resolveId(sr)})
)
// Host --------------------------------------------------------------
export const editHost = (host, props) => (
@@ -348,7 +419,7 @@ export const restartHostsAgents = hosts => {
title: _('restartHostsAgentsModalTitle', { nHosts }),
body: _('restartHostsAgentsModalMessage', { nHosts })
}).then(
() => map(hosts, host => restartHostAgent(host)),
() => map(hosts, restartHostAgent),
noop
)
}
@@ -413,6 +484,32 @@ export const installAllHostPatches = host => (
_call('host.installAllPatches', { host: resolveId(host) })
)
export const installAllPatchesOnPool = pool => (
_call('pool.installAllPatches', { pool: resolveId(pool) })
)
// Containers --------------------------------------------------------
export const pauseContainer = (vm, container) => (
_call('docker.pause', { vm: resolveId(vm), container })
)
export const restartContainer = (vm, container) => (
_call('docker.restart', { vm: resolveId(vm), container })
)
export const startContainer = (vm, container) => (
_call('docker.start', { vm: resolveId(vm), container })
)
export const stopContainer = (vm, container) => (
_call('docker.stop', { vm: resolveId(vm), container })
)
export const unpauseContainer = (vm, container) => (
_call('docker.unpause', { vm: resolveId(vm), container })
)
// VM ----------------------------------------------------------------
export const startVm = vm => (
@@ -471,12 +568,14 @@ export const restartVm = (vm, force = false) => (
)
)
export const restartVms = (vms, force) => (
export const restartVms = (vms, force = false) => (
confirm({
title: _('restartVmsModalTitle', { vms: vms.length }),
body: _('restartVmsModalMessage', { vms: vms.length })
}).then(
() => map(vms, vmId => restartVm({ id: vmId }, force)),
() => Promise.all(map(vms, vmId =>
_call('vm.restart', { id: resolveId(vmId), force })
)),
noop
)
)
@@ -551,6 +650,16 @@ export const convertVmToTemplate = vm => (
)
)
export const deleteTemplates = templates => (
confirm({
title: _('templateDeleteModalTitle', { templates: templates.length }),
body: _('templateDeleteModalBody', { templates: templates.length })
}).then(
() => Promise.all(map(resolveIds(templates), id => _call('vm.delete', { id, delete_disks: true }))),
noop
)
)
export const snapshotVm = vm => (
_call('vm.snapshot', { id: resolveId(vm) })
)
@@ -565,6 +674,16 @@ export const snapshotVms = vms => (
)
)
export const deleteSnapshot = vm => (
confirm({
title: _('deleteSnapshotModalTitle'),
body: _('deleteSnapshotModalMessage')
}).then(
() => _call('vm.delete', { id: resolveId(vm), delete_disks: true }),
noop
)
)
import MigrateVmModalBody from './migrate-vm-modal'
export const migrateVm = (vm, host) => (
confirm({
@@ -651,7 +770,7 @@ export const deleteVms = vms => (
title: _('deleteVmsModalTitle', { vms: vms.length }),
body: _('deleteVmsModalMessage', { vms: vms.length })
}).then(
() => map(vms, vmId => _call('vm.delete', { id: vmId })),
() => map(vms, vmId => _call('vm.delete', { id: vmId, delete_disks: true })),
noop
)
)
@@ -664,12 +783,13 @@ export const importDeltaBackup = ({remote, file, sr}) => (
_call('vm.importDeltaBackup', resolveIds({remote, filePath: file, sr}))
)
import RevertSnapshotModalBody from './revert-snapshot-modal'
export const revertSnapshot = vm => (
confirm({
title: _('revertVmModalTitle'),
body: _('revertVmModalMessage')
body: <RevertSnapshotModalBody />
}).then(
() => _call('vm.revert', { id: resolveId(vm) }),
snapshotBefore => _call('vm.revert', { id: resolveId(vm), snapshotBefore }),
noop
)
)
@@ -682,12 +802,12 @@ export const fetchVmStats = (vm, granularity) => (
_call('vm.stats', { id: resolveId(vm), granularity })
)
export const importVm = (file, sr) => {
export const importVm = (file, type = 'xva', data = undefined, sr) => {
const { name } = file
info(_('startVmImport'), name)
return _call('vm.import', { sr }).then(({ $sendTo: url }) => {
return _call('vm.import', { type, data, sr: resolveId(sr) }).then(({ $sendTo: url }) => {
const req = request.post(url)
req.send(file)
@@ -701,16 +821,16 @@ export const importVm = (file, sr) => {
})
}
export const importVms = (files, sr) => (
Promise.all(map(files, file =>
importVm(file, sr).catch(noop)
export const importVms = (vms, sr) => (
Promise.all(map(vms, ({ file, type, data }) =>
importVm(file, type, data, sr).catch(noop)
))
)
export const exportVm = vm => {
info(_('startVmExport'), vm.id)
return _call('vm.export', { vm: resolveId(vm) })
.then(({ $getFrom: url }) => window.open(`.${url}`))
.then(({ $getFrom: url }) => { window.location = `.${url}` })
}
export const insertCd = (vm, cd, force = false) => (
@@ -759,7 +879,13 @@ export const editVdi = (vdi, props) => (
)
export const deleteVdi = vdi => (
_call('vdi.delete', { id: resolveId(vdi) })
confirm({
title: _('deleteVdiModalTitle'),
body: _('deleteVdiModalMessage')
}).then(
() => _call('vdi.delete', { id: resolveId(vdi) }),
noop
)
)
export const migrateVdi = (vdi, sr) => (
@@ -790,6 +916,10 @@ export const setBootableVbd = (vbd, bootable) => (
// VIF ---------------------------------------------------------------
export const createVmInterface = (vm, network, mac) => (
_call('vm.createInterface', resolveIds({vm, network, mac}))
)
export const connectVif = vif => (
_call('vif.connect', { id: resolveId(vif) })
)
@@ -802,6 +932,10 @@ export const deleteVif = vif => (
_call('vif.delete', { id: resolveId(vif) })
)
export const setVif = (vif, { network, mac, allowedIpv4Addresses, allowedIpv6Addresses }) => (
_call('vif.set', { id: resolveId(vif), network: resolveId(network), mac, allowedIpv4Addresses, allowedIpv6Addresses })
)
// Network -----------------------------------------------------------
export const editNetwork = (network, props) => (
@@ -815,7 +949,32 @@ export const createNetwork = container => (
title: _('newNetworkCreate'),
body: <CreateNetworkModalBody container={container} />
}).then(
params => _call('network.create', params),
params => {
if (!params.name) {
return error(_('newNetworkNoNameErrorTitle'), _('newNetworkNoNameErrorMessage'))
}
return _call('network.create', params)
},
noop
)
)
export const getBondModes = () =>
_call('network.getBondModes')
import CreateBondedNetworkModalBody from './create-bonded-network-modal'
export const createBondedNetwork = container => (
confirm({
icon: 'network',
title: _('newBondedNetworkCreate'),
body: <CreateBondedNetworkModalBody pool={container.$pool} />
}).then(
params => {
if (!params.name) {
return error(_('newNetworkNoNameErrorTitle'), _('newNetworkNoNameErrorMessage'))
}
return _call('network.createBonded', params)
},
noop
)
)
@@ -830,12 +989,6 @@ export const deleteNetwork = network => (
)
)
// VIF ---------------------------------------------------------------
export const createVmInterface = (vm, network, mac, mtu) => (
_call('vm.createInterface', resolveIds({vm, network, mtu, mac}))
)
// PIF ---------------------------------------------------------------
export const connectPif = pif => (
@@ -868,6 +1021,15 @@ export const deletePif = pif => (
)
)
export const reconfigurePifIp = (pif, { mode, ip, netmask, gateway, dns }) =>
_call('pif.reconfigureIp', { pif: resolveId(pif), mode, ip, netmask, gateway, dns })
export const getIpv4ConfigModes = () =>
_call('pif.getIpv4ConfigurationModes')
export const editPif = (pif, { vlan }) =>
_call('pif.editPif', { pif: resolveId(pif), vlan })
// SR ----------------------------------------------------------------
export const deleteSr = sr => (
@@ -1038,7 +1200,7 @@ export const deleteBackupSchedule = async schedule => {
export const loadPlugin = async id => (
_call('plugin.load', { id })::tap(
subscribePlugins.forceRefresh()
subscribePlugins.forceRefresh
)::rethrow(
err => error(_('pluginError'), JSON.stringify(err.data) || _('unknownPluginError'))
)
@@ -1046,7 +1208,7 @@ export const loadPlugin = async id => (
export const unloadPlugin = id => (
_call('plugin.unload', { id })::tap(
subscribePlugins.forceRefresh()
subscribePlugins.forceRefresh
)::rethrow(
err => error(_('pluginError'), JSON.stringify(err.data) || _('unknownPluginError'))
)
@@ -1093,8 +1255,8 @@ export const createResourceSet = (name, { subjects, objects, limits } = {}) => (
)
)
export const editRessourceSet = (id, { name, subjects, objects, limits } = {}) => (
_call('resourceSet.set', { id, name, subjects, objects, limits })::tap(
export const editResourceSet = (id, { name, subjects, objects, limits, ipPools } = {}) => (
_call('resourceSet.set', { id, name, subjects, objects, limits, ipPools })::tap(
subscribeResourceSets.forceRefresh
)
)
@@ -1104,7 +1266,7 @@ export const deleteResourceSet = async id => {
title: _('deleteResourceSetWarning'),
body: _('deleteResourceSetQuestion')
})
await _call('resourceSet.delete', { id })
await _call('resourceSet.delete', { id: resolveId(id) })
subscribeResourceSets.forceRefresh()
}
@@ -1115,6 +1277,12 @@ export const recomputeResourceSetsLimits = () => (
// Remote ------------------------------------------------------------
export const getRemote = remote => (
_call('remote.get', resolveIds({id: remote}))::rethrow(
err => error(_('getRemote'), err.message || String(err))
)
)
export const createRemote = (name, url) => (
_call('remote.create', {name, url})::tap(
subscribeRemotes.forceRefresh
@@ -1233,6 +1401,14 @@ export const deleteJobsLog = id => (
)
)
// Logs
export const deleteApiLog = id => (
_call('log.delete', {namespace: 'api', id})::tap(
subscribeApiLogs.forceRefresh
)
)
// Acls, users, groups ----------------------------------------------------------
export const addAcl = ({subject, object, action}) => (
@@ -1316,9 +1492,11 @@ export const deleteUser = user => (
confirm({
title: _('deleteUser'),
body: <p>{_('deleteUserConfirm')}</p>
}).then(() => _call('user.delete', resolveIds({id: user})))
::tap(subscribeUsers.forceRefresh)
::rethrow(err => error(_('deleteUser'), err.message || String(err)))
}).then(() =>
_call('user.delete', { id: resolveId(user) })
::tap(subscribeUsers.forceRefresh)
::rethrow(err => error(_('deleteUser'), err.message || String(err)))
)
)
export const editUser = (user, { email, password, permission }) => (
@@ -1347,8 +1525,16 @@ const _setUserPreferences = preferences => (
)
import NewSshKeyModalBody from './new-ssh-key-modal'
export const addSshKey = () => (
confirm({
export const addSshKey = key => {
const { preferences } = xo.user
const otherKeys = preferences && preferences.sshKeys || []
if (key) {
return _setUserPreferences({ sshKeys: [
...otherKeys,
key
]})
}
return confirm({
icon: 'ssh-key',
title: _('newSshKeyModalTitle'),
body: <NewSshKeyModalBody />
@@ -1358,8 +1544,6 @@ export const addSshKey = () => (
error(_('sshKeyErrorTitle'), _('sshKeyErrorMessage'))
return
}
const { preferences } = xo.user
const otherKeys = preferences && preferences.sshKeys || []
return _setUserPreferences({ sshKeys: [
...otherKeys,
newKey
@@ -1367,7 +1551,7 @@ export const addSshKey = () => (
},
noop
)
)
}
export const deleteSshKey = key => (
confirm({
@@ -1462,13 +1646,13 @@ export const setDefaultHomeFilter = (type, name) => {
// Jobs ----------------------------------------------------------
export const deleteJob = job => (
_call('job.delete', resolveIds({id: job}))::tap(
_call('job.delete', { id: resolveId(job) })::tap(
subscribeJobs.forceRefresh
)
)
export const deleteSchedule = schedule => (
_call('schedule.delete', resolveIds({id: schedule}))::tap(
_call('schedule.delete', { id: resolveIds(schedule) })::tap(
subscribeSchedules.forceRefresh
)
)
@@ -1484,3 +1668,25 @@ export const updateSchedule = ({ id, job: jobId, cron, enabled, name, timezone }
subscribeSchedules.forceRefresh
)
)
// IP pools --------------------------------------------------------------------
export const createIpPool = ({ name, ips, networks }) => {
const addresses = {}
forEach(ips, ip => { addresses[ip] = {} })
return _call('ipPool.create', { name, addresses, networks: resolveIds(networks) })::tap(
subscribeIpPools.forceRefresh
)
}
export const deleteIpPool = ipPool => (
_call('ipPool.delete', { id: resolveId(ipPool) })::tap(
subscribeIpPools.forceRefresh
)
)
export const setIpPool = (ipPool, { name, addresses, networks }) => (
_call('ipPool.set', { id: resolveId(ipPool), name, addresses, networks: resolveIds(networks) })::tap(
subscribeIpPools.forceRefresh
)
)

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,7 +144,12 @@ export default class MigrateVmModalBody extends BaseComponent {
}
}
_getObject (id) {
return getObject(store.getState(), id)
}
_selectHost = host => {
// No host selected
if (!host) {
this.setState({
host: undefined,
@@ -126,20 +157,40 @@ export default class MigrateVmModalBody extends BaseComponent {
})
return
}
const intraPool = this.props.vm.$pool === host.$pool
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: undefined,
mapVdisSrs: doNotMigrateVdis ? undefined : mapValues(vdis, vdi => defaultSr),
mapVifsNetworks: undefined,
migrationNetwork: undefined
})
return
}
const { networks, pools, pifs, vdis, vifs } = this.props
// Inter-pool
const { networks, pifs, vifs } = this.props
const defaultMigrationNetworkId = find(pifs, pif => pif.$host === host.id && pif.management).$network
const defaultSr = pools[host.$pool].default_SR
const defaultNetwork = invoke(() => {
// First PIF with an IP.
@@ -158,6 +209,7 @@ export default class MigrateVmModalBody extends BaseComponent {
})
this.setState({
doNotMigrateVdis: false,
host,
intraPool,
mapVdisSrs: mapValues(vdis, vdi => defaultSr),
@@ -171,6 +223,7 @@ export default class MigrateVmModalBody extends BaseComponent {
render () {
const { vdis, vifs, networks } = this.props
const {
doNotMigrateVdis,
host,
intraPool,
mapVdisSrs,
@@ -190,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>
@@ -199,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>
@@ -242,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

@@ -87,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
@@ -261,7 +280,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
<Col size={6}>
<SelectNetwork
onChange={this._selectMigrationNetwork}
predicate={this._getNetworkPredicate()}
predicate={this._getMigrationNetworkPredicate()}
value={migrationNetworkId}
/>
</Col>
@@ -290,7 +309,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
<SelectNetwork
disabled={smartVifMapping}
onChange={this._selectNetwork}
predicate={this._getNetworkPredicate()}
predicate={this._getTargetNetworkPredicate()}
value={networkId}
/>
</Col>

View File

@@ -27,7 +27,7 @@ export default class NewSshKeyModalBody extends BaseComponent {
} = this.state
return <div>
<div className='p-b-1'>
<div className='pb-1'>
<SingleLineRow>
<Col size={4}>{_('title')}</Col>
<Col size={8}>
@@ -40,7 +40,7 @@ export default class NewSshKeyModalBody extends BaseComponent {
</Col>
</SingleLineRow>
</div>
<div className='p-b-1'>
<div className='pb-1'>
<SingleLineRow>
<Col size={4}>{_('key')}</Col>
<Col size={8}>

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

BIN
src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

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;
@@ -116,6 +124,10 @@
@extend .fa;
@extend .fa-key;
}
&-ip {
@extend .fa;
@extend .fa-map-marker;
}
&-shown {
@extend .fa;
@@ -163,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;
@@ -279,10 +303,6 @@
@extend .fa;
@extend .fa-camera;
}
&-export {
@extend .fa;
@extend .fa-download;
}
&-fast-clone {
@extend .fa;
@extend .fa-code-fork;
@@ -381,6 +401,10 @@
@extend .fa;
@extend .fa-trash;
}
&-migrate {
@extend .fa;
@extend .fa-share;
}
}
// Host
&-host {
@@ -500,6 +524,10 @@
@extend .fa;
@extend .fa-sort;
}
&-reset {
@extend .fa;
@extend .fa-undo;
}
&-save {
@extend .fa;
@extend .fa-floppy-o;
@@ -537,6 +565,10 @@
@extend .fa;
@extend .fa-file-archive-o;
}
&-export {
@extend .fa;
@extend .fa-download;
}
&-schedule {
@extend .fa;
@extend .fa-clock-o;
@@ -604,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;
@@ -676,6 +700,14 @@
@extend .fa;
@extend .fa-puzzle-piece;
}
&-logs {
@extend .fa;
@extend .fa-list;
}
&-config {
@extend .fa;
@extend .fa-file-o;
}
}
&-menu-about {
@extend .fa;
@@ -749,6 +781,10 @@
@extend .fa;
@extend .icon-debian;
}
&-docker {
@extend .fa;
@extend .icon-docker;
}
&-fedora {
@extend .fa;
@extend .icon-fedora;

View File

@@ -44,6 +44,11 @@ html.no-js(
href = 'modules.css'
)
link(
rel = 'shortcut icon'
href = 'favicon.ico'
)
//- Styles required for a proper display while loading.
style.
html, body, #xo-app { height: 100% }

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,37 +194,6 @@ $select-input-height: 40px; // Bootstrap input height
background: $gray-lighter;
}
// MEMORY/DISK BAR STYLE =======================================================
.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;
}
// 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

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

@@ -17,7 +17,7 @@ const HEADER = <Container>
<h2><Icon icon='backup' /> {_('backupPage')}</h2>
</Col>
<Col mediumSize={9}>
<NavTabs className='pull-xs-right'>
<NavTabs className='pull-right'>
<NavLink to={'/backup/overview'}><Icon icon='menu-backup-overview' /> {_('backupOverviewPage')}</NavLink>
<NavLink to={'/backup/new'}><Icon icon='menu-backup-new' /> {_('backupNewPage')}</NavLink>
<NavLink to={'/backup/restore'}><Icon icon='menu-backup-restore' /> {_('backupRestorePage')}</NavLink>

View File

@@ -2,25 +2,30 @@ 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, Row, Col } from 'grid'
import { error } from 'notification'
import { generateUiSchema } from 'xo-json-schema-input'
import { confirm } from 'modal'
import {
createJob,
createSchedule,
getRemote,
setJob,
updateSchedule
} from 'xo'
// ===================================================================
// FIXME: missing most of translation. Can't be done in a dumb way, some of the word are keyword for XO-Server parameters...
const NO_SMART_SCHEMA = {
type: 'object',
@@ -31,8 +36,8 @@ const NO_SMART_SCHEMA = {
type: 'string',
'xo:type': 'vm'
},
title: 'VMs',
description: 'Choose VMs to backup.'
title: _('editBackupVmsTitle'),
description: 'Choose VMs to backup.' // FIXME: can't translate
}
},
required: [ 'vms' ]
@@ -43,10 +48,10 @@ const SMART_SCHEMA = {
type: 'object',
properties: {
status: {
default: 'All',
enum: [ 'All', 'Running', 'Halted' ],
title: 'VMs statuses',
description: 'The statuses of VMs to backup.'
default: 'All', // FIXME: can't translate
enum: [ 'All', 'Running', 'Halted' ], // FIXME: can't translate
title: _('editBackupSmartStatusTitle'),
description: 'The statuses of VMs to backup.' // FIXME: can't translate
},
pools: {
type: 'array',
@@ -54,7 +59,7 @@ const SMART_SCHEMA = {
type: 'string',
'xo:type': 'pool'
},
title: 'Resident on'
title: _('editBackupSmartResidentOn')
},
tags: {
type: 'array',
@@ -62,8 +67,8 @@ const SMART_SCHEMA = {
type: 'string',
'xo:type': 'tag'
},
title: 'VMs Tags',
description: 'VMs which contains at least one of these tags. Not used if empty.'
title: _('editBackupSmartTagsTitle'),
description: 'VMs which contains at least one of these tags. Not used if empty.' // FIXME: can't translate
}
},
required: [ 'status', 'pools' ]
@@ -77,17 +82,17 @@ const COMMON_SCHEMA = {
properties: {
tag: {
type: 'string',
title: 'Tag',
description: 'Back-up tag.'
title: _('editBackupTagTitle'),
description: 'Back-up tag.' // FIXME: can't translate
},
_reportWhen: {
enum: [ 'never', 'always', 'failure' ],
title: 'Report',
description: 'When to send reports.'
enum: [ 'never', 'always', 'failure' ], // FIXME: can't translate
title: _('editBackupReportTitle'),
description: 'When to send reports.' // FIXME: can't translate
},
enabled: {
type: 'boolean',
title: 'Enable immediately after creation'
title: _('editBackupReportEnable')
}
},
required: [ 'tag', 'vms', '_reportWhen' ]
@@ -95,14 +100,14 @@ const COMMON_SCHEMA = {
const DEPTH_PROPERTY = {
type: 'integer',
title: 'Depth',
description: 'How many backups to rollover.'
title: _('editBackupDepthTitle'),
description: 'How many backups to rollover.' // FIXME: can't translate
}
const REMOTE_PROPERTY = {
type: 'string',
'xo:type': 'remote',
title: 'Remote'
title: _('editBackupRemoteTitle')
}
const BACKUP_SCHEMA = {
@@ -302,7 +307,7 @@ export default class New extends Component {
}
}
_handleSubmit = () => {
_handleSubmit = async () => {
const {
enabled,
...callArgs
@@ -367,10 +372,39 @@ export default class New extends Component {
}))
}
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, { cron: this.state.cronPattern, enabled, timezone })
})
return createSchedule(await createJob(job), { cron: this.state.cronPattern, enabled, timezone })
}
_handleReset = () => {
@@ -411,29 +445,28 @@ export default class New extends Component {
return process.env.XOA_PLAN > 1
? (
<Wizard>
<Section icon='backup' title={this.props.job ? 'editVmBackup' : 'newVmBackup'}>
<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) =>
<Wizard>
<Section icon='backup' title={this.props.job ? 'editVmBackup' : 'newVmBackup'}>
<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>
)}
</select>
</fieldset>
<form id='form-new-vm-backup'>
{backupInfo && <div>
<GenericInput
label={<span><Icon icon={backupInfo.icon} /> {_(backupInfo.label)}</span>}
ref='backupInput'
@@ -472,51 +505,50 @@ export default class New extends Component {
uiSchema={NO_SMART_UI_SCHEMA}
/>
}
</div>
)}
</form>
</Col>
</Row>
</Container>
</Section>
<Section icon='schedule' title='schedule'>
<Scheduler
cronPattern={cronPattern}
onChange={this._updateCronPattern}
timezone={timezone}
/>
</Section>
<Section icon='preview' title='preview' summary>
<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>)
</div>}
</form>
</Col>
</Row>
</Container>
</Section>
<Section icon='schedule' title='schedule'>
<Scheduler
cronPattern={cronPattern}
onChange={this._updateCronPattern}
timezone={timezone}
/>
</Section>
<Section icon='preview' title='preview' summary>
<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-right pt-1'>
<ActionButton
btnStyle='primary'
className='btn-lg mr-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>
</Col>
</Row>
</Container>
</Section>
</Wizard>
)
: <Container><Upgrade place='newBackup' available={2} /></Container>
}

View File

@@ -134,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 ? (
@@ -154,14 +154,14 @@ 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'>
<Link className='btn btn-sm btn-primary m-r-1' to={`/backup/${schedule.id}/edit`}>
<fieldset className='pull-right'>
<Link className='btn btn-sm btn-primary mr-1' to={`/backup/${schedule.id}/edit`}>
<Icon icon='edit' />
</Link>
<ButtonGroup>

View File

@@ -1,21 +1,21 @@
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import ActionRowButton from 'action-row-button'
import Component from 'base-component'
import filter from 'lodash/filter'
import find from 'lodash/find'
import forEach from 'lodash/forEach'
import Link from 'link'
import groupBy from 'lodash/groupBy'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import mapValues from 'lodash/mapValues'
import moment from 'moment'
import orderBy from 'lodash/orderBy'
import React, { Component } from 'react'
import React from 'react'
import reduce from 'lodash/reduce'
import size from 'lodash/size'
import SortedTable from 'sorted-table'
import Tooltip from 'tooltip'
import Upgrade from 'xoa-upgrade'
import { confirm } from 'modal'
import { connectStore } from 'utils'
import { Container } from 'grid'
import { connectStore, addSubscriptions, noop } from 'utils'
import { Container, Row, Col } from 'grid'
import { createGetObjectsOfType } from 'selectors'
import { FormattedDate, injectIntl } from 'react-intl'
import { info, error } from 'notification'
@@ -33,49 +33,125 @@ import {
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>
const VM_COLUMNS = [
{
name: _('backupVmNameColumn'),
itemRenderer: ({ last }) => last.name,
sortCriteria: ({ last }) => last.name
},
{
name: _('backupTags'),
itemRenderer: ({ tagsByRemote }) => <Container>
{map(tagsByRemote, ({ tags, remoteName }) => <Row>
<Col mediumSize={3}><strong>{remoteName}</strong></Col>
<Col mediumSize={9}>{tags.join(', ')}</Col>
</Row>)}
</Container>
},
{
name: _('lastBackupColumn'),
itemRenderer: ({ last }) => <FormattedDate value={last.date} month='long' day='numeric' year='numeric' hour='2-digit' minute='2-digit' second='2-digit' />,
sortCriteria: ({ last }) => last.date,
sortOrder: 'desc'
},
{
name: _('availableBackupsColumn'),
itemRenderer: ({ simpleCount, deltaCount }) => <span>
{!!simpleCount && <span>{_('simpleBackup')} <span className='tag tag-pill tag-primary'>{simpleCount}</span></span>}
{!!simpleCount && !!deltaCount && ', '}
{!!deltaCount && <span>{_('delta')} <span className='tag tag-pill tag-primary'>{deltaCount}</span></span>}
</span>
}
]
const openImportModal = ({ backups }) => confirm({
title: _('importBackupModalTitle', {name: backups[0].name}),
body: <ImportModalBody vmName={backups[0].name} backups={backups} />
}).then(doImport)
const doImport = ({ backup, sr, start }) => {
if (!sr || !backup) {
error(_('backupRestoreErrorTitle'), _('backupRestoreErrorMessage'))
return
}
const importMethods = {
delta: importDeltaBackup,
simple: importBackup
}
info(_('importBackupTitle'), _('importBackupMessage'))
try {
const importPromise = importMethods[backup.type]({remote: backup.remoteId, sr, file: backup.path}).then(id => {
return id
})
if (start) {
importPromise.then(id => startVm({id}))
}
} catch (err) {
error('VM import', err.message || String(err))
}
}
@connectStore(() => ({
writableSrs: createGetObjectsOfType('SR').filter(
[ isSrWritable ]
).sort()
}), { withRef: true })
class _ModalBody extends Component {
get value () {
return this.state
}
render () {
const { backups, intl } = this.props
return <div>
<SelectSr onChange={this.linkState('sr')} predicate={isSrWritable} />
<br />
<SelectPlainObject
onChange={this.linkState('backup')}
optionKey='path'
optionRenderer={backupOptionRenderer}
options={backups}
placeholder={intl.formatMessage(messages.importBackupModalSelectBackup)}
/>
<br />
<Toggle onChange={this.linkState('start')} /> {_('importBackupModalStart')}
</div>
}
}
const ImportModalBody = injectIntl(_ModalBody, {withRef: true})
@connectStore(() => ({
writableSrs: createGetObjectsOfType('SR').filter(
[ isSrWritable ]
).sort()
}))
@addSubscriptions({
rawRemotes: subscribeRemotes
})
export default class Restore extends Component {
constructor (props) {
super(props)
this.state = {
remotes: []
componentWillReceiveProps ({ rawRemotes }) {
let filteredRemotes
if ((filteredRemotes = filter(rawRemotes, 'enabled')) !== filter(this.props.rawRemotes, 'enabled')) {
this._listAll(filteredRemotes).catch(noop)
}
}
componentWillMount () {
this.componentWillUnmount = subscribeRemotes(rawRemotes => {
const { remotes } = this.state
this.setState({
remotes: orderBy(map(rawRemotes, r => {
r = {...r}
const older = find(remotes, {id: r.id})
older && older.backupInfoByVm && (r.backupInfoByVm = older.backupInfoByVm)
return r
}), ['name'])
})
})
}
_listAll = async remotes => {
const remotesFiles = await Promise.all(map(remotes, remote => listRemote(remote.id)))
const backupInfoByVm = {}
forEach(remotesFiles, (remoteFiles, index) => {
const remote = remotes[index]
_list = async id => {
const files = await listRemote(id)
const { remotes } = this.state
const remote = find(remotes, {id})
if (remote) {
const backupInfoByVm = {}
forEach(files, file => {
forEach(remoteFiles, file => {
let backup
const deltaInfo = /^vm_delta_(.*)_([^\/]+)\/([^_]+)_(.*)$/.exec(file)
if (deltaInfo) {
@@ -108,182 +184,39 @@ export default class Restore extends Component {
backupInfoByVm[backup.name].push(backup)
}
})
for (let vm in backupInfoByVm) {
const bks = backupInfoByVm[vm]
backupInfoByVm[vm] = {
last: reduce(bks, (last, b) => b.date > last.date ? b : last),
simpleCount: reduce(bks, (sum, b) => b.type === 'simple' ? ++sum : sum, 0),
deltaCount: reduce(bks, (sum, b) => b.type === 'delta' ? ++sum : sum, 0)
}
})
forEach(backupInfoByVm, (backups, vm) => {
backupInfoByVm[vm] = {
backups,
last: reduce(backups, (last, b) => b.date > last.date ? b : last),
tagsByRemote: mapValues(groupBy(backups, 'remoteId'), (backups, remoteId) =>
({ remoteName: find(remotes, remote => remote.id === remoteId).name, tags: map(backups, 'tag') })
),
simpleCount: reduce(backups, (sum, b) => b.type === 'simple' ? ++sum : sum, 0),
deltaCount: reduce(backups, (sum, b) => b.type === 'delta' ? ++sum : sum, 0)
}
remote.backupInfoByVm = map(backupInfoByVm)
}
this.setState({remotes})
})
this.setState({ backupInfoByVm })
}
render () {
const {
remotes
} = this.state
const { backupInfoByVm } = this.state
if (!backupInfoByVm) {
return <h2>{_('statusLoading')}</h2>
}
return process.env.XOA_PLAN > 1
? <Container>
<h2>{_('restoreBackups')}</h2>
{!remotes.length && <span>{_('noRemotes')}</span>}
{map(remotes, (r, key) =>
<div key={key}>
<Link to='/settings/remotes'>{r.name}</Link>
{' '}
{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} />
</span>
{r.backupInfoByVm && <div>
<br />
{isEmptyRemote(r)
? <span>{_('noBackup')}</span>
: <SortedTable collection={r.backupInfoByVm} columns={BK_COLUMNS} />
}
</div>}
<hr />
{isEmpty(backupInfoByVm)
? _('noBackup')
: <div>
<em><Icon icon='info' /> {_('restoreBackupsInfo')}</em>
<SortedTable collection={backupInfoByVm} columns={VM_COLUMNS} rowAction={openImportModal} defaultColumn={2} />
</div>
)}
}
</Container>
: <Container><Upgrade place='restoreBackup' available={2} /></Container>
}
}
const openImportModal = backup => confirm({
title: _('importBackupModalTitle', {name: backup.name}),
body: <ImportModalBody vmName={backup.name} remoteId={backup.remoteId} />
}).then(doImport)
const doImport = ({ backup, remoteId, sr, start }) => {
if (!sr || !backup) {
error('Missing Parameters', 'Choose a SR and a backup')
return
}
const importMethods = {
delta: importDeltaBackup,
simple: importBackup
}
notifyImportStart()
try {
const importPromise = importMethods[backup.type]({remote: remoteId, sr, file: backup.path}).then(id => {
return id
})
if (start) {
importPromise.then(id => startVm({id}))
}
} catch (err) {
error('VM import', err.message || String(err))
}
}
const BK_COLUMNS = [
{
name: _('backupVmNameColumn'),
itemRenderer: info => info.last.name,
sortCriteria: info => info.last.name
},
{
name: _('backupTagColumn'),
itemRenderer: info => info.last.tag,
sortCriteria: info => info.last.tag
},
{
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,
sortOrder: 'desc'
},
{
name: _('availableBackupsColumn'),
itemRenderer: info => <span>
{!!info.simpleCount && <span>{_('simpleBackup')} <span className='tag tag-pill tag-primary'>{info.simpleCount}</span></span>}
{' '}
{!!info.deltaCount && <span>{_('delta')} <span className='tag tag-pill tag-primary'>{info.deltaCount}</span></span>}
</span>
},
{
name: _('restoreColumn'),
itemRenderer: info => <Tooltip content={_('restoreTip')}><ActionRowButton icon='menu-backup-restore' btnStyle='success' handler={openImportModal} handlerParam={info.last} /></Tooltip>
}
]
const notifyImportStart = () => info(_('importBackupTitle'), _('importBackupMessage'))
@connectStore(() => ({
writableSrs: createGetObjectsOfType('SR').filter(
[ isSrWritable ]
).sort()
}), { withRef: true })
class _ModalBody extends Component {
constructor (props) {
super(props)
this.state = {}
const { vmName, remoteId } = props
if (remoteId) {
listRemote(remoteId)
.then(files => {
const options = []
forEach(files, file => {
let backup
const deltaInfo = /^vm_delta_(.*)_([^\/]+)\/([^_]+)_(.*)$/.exec(file)
if (deltaInfo) {
const [ , tag, , date, name ] = deltaInfo
if (name !== vmName) {
return
}
backup = {
type: 'delta',
date: parseDate(date),
path: file,
tag
}
} else {
const backupInfo = /^([^_]+)_([^_]+)_(.*)\.xva$/.exec(file)
if (backupInfo) {
const [ , date, tag, name ] = backupInfo
if (name !== vmName) {
return
}
backup = {
type: 'simple',
date: parseDate(date),
path: file,
tag
}
}
}
options.push(backup)
})
this.setState({options})
})
}
}
get value () {
const { sr, backup, start } = this.refs
const { remoteId } = this.props
return {
sr: sr.value,
backup: backup.value,
start: start.value,
remoteId
}
}
render () {
return <div>
<SelectSr ref='sr' predicate={isSrWritable} />
<br />
<SelectPlainObject ref='backup' options={this.state.options} optionKey='path' optionRenderer={backupOptionRenderer} placeholder={this.props.intl.formatMessage(messages.importBackupModalSelectBackup)} />
<br />
<Toggle ref='start' /> {_('importBackupModalStart')}
</div>
}
}
const ImportModalBody = injectIntl(_ModalBody, {withRef: true})

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,8 +66,12 @@ 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>,
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'
}
@@ -230,6 +236,8 @@ export default class Health extends Component {
)
)
_getSrUrl = sr => `srs/${sr.id}`
render () {
return process.env.XOA_PLAN > 3
? <Container>
@@ -244,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

@@ -17,7 +17,7 @@ const HEADER = <Container>
<h2><Icon icon='menu-dashboard' /> {_('dashboardPage')}</h2>
</Col>
<Col mediumSize={9}>
<NavTabs className='pull-xs-right'>
<NavTabs className='pull-right'>
<NavLink to={'/dashboard/overview'}><Icon icon='menu-dashboard-overview' /> {_('overviewDashboardPage')}</NavLink>
<NavLink to={'/dashboard/visualizations'}><Icon icon='menu-dashboard-visualization' /> {_('overviewVisualizationDashboardPage')}</NavLink>
<NavLink to={'/dashboard/stats'}><Icon icon='menu-dashboard-stats' /> {_('overviewStatsDashboardPage')}</NavLink>

View File

@@ -295,16 +295,19 @@ class SelectMetric extends Component {
<Container>
<Row>
<Col mediumSize={6}>
<SelectHostVm
multi
onChange={this._handleSelection}
predicate={predicate}
value={objects}
/>
<div className='btn-group m-t-1' role='group'>
<div className='form-group'>
<SelectHostVm
multi
onChange={this._handleSelection}
predicate={predicate}
value={objects}
/>
</div>
<div className='btn-group mt-1' role='group'>
<button
className='btn btn-secondary'
onClick={this._resetSelection}
tooltip={_('dashboardStatsButtonRemoveAll')}
type='button'
>
<Icon icon='remove' />
@@ -312,6 +315,7 @@ class SelectMetric extends Component {
<button
className='btn btn-secondary'
onClick={this._selectAllHosts}
tooltip={_('dashboardStatsButtonAddAllHost')}
type='button'
>
<Icon icon='host' />
@@ -319,6 +323,7 @@ class SelectMetric extends Component {
<button
className='btn btn-secondary'
onClick={this._selectAllVms}
tooltip={_('dashboardStatsButtonAddAllVM')}
type='button'
>
<Icon icon='vm' />
@@ -336,9 +341,9 @@ class SelectMetric extends Component {
<Col mediumSize={6}>
{metricsState === METRICS_LOADING
? (
<div>
<Icon icon='loading' /> {_('metricsLoading')}
</div>
<div>
<Icon icon='loading' /> {_('metricsLoading')}
</div>
) : (metricsState === METRICS_LOADED &&
<select className='form-control' onChange={this._handleSelectedMetric}>
{_('noSelectedMetric', message => <option value=''>{message}</option>)}
@@ -386,7 +391,7 @@ class MetricViewer extends Component {
<Container>
<Row>
<Col>
{map(objects, object => renderXoItem(object, { className: 'm-r-1' }))}
{map(objects, object => renderXoItem(object, { className: 'mr-1' }))}
</Col>
</Row>
<Row>

View File

@@ -21,6 +21,7 @@ import {
// ===================================================================
// Columns order is defined by the attributes declaration order.
// FIXME translation
const DATA_LABELS = {
nVCpus: 'vCPUs number',
ram: 'RAM quantity',

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

@@ -10,18 +10,22 @@ 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 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,
@@ -31,8 +35,7 @@ import {
snapshotVms,
startVms,
stopHosts,
stopVms,
subscribeCurrentUser
stopVms
} from 'xo'
import { Container, Row, Col } from 'grid'
import {
@@ -41,8 +44,8 @@ import {
SelectTag
} from 'select-objects'
import {
addSubscriptions,
connectStore,
firstDefined,
noop
} from 'utils'
import {
@@ -52,7 +55,8 @@ import {
createGetObjectsOfType,
createPager,
createSelector,
createSort
createSort,
getUser
} from 'selectors'
import {
Button,
@@ -67,6 +71,7 @@ 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
@@ -75,10 +80,10 @@ const OPTIONS = {
defaultFilter: 'power_state:running ',
filters: homeFilters.host,
mainActions: [
{ handler: stopHosts, icon: 'host-stop' },
{ handler: restartHostsAgents, icon: 'host-restart-agent' },
{ handler: emergencyShutdownHosts, icon: 'host-emergency-shutdown' },
{ handler: restartHosts, icon: 'host-reboot' }
{ 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,
@@ -93,11 +98,11 @@ const OPTIONS = {
defaultFilter: 'power_state:running ',
filters: homeFilters.VM,
mainActions: [
{ handler: stopVms, icon: 'vm-stop' },
{ handler: startVms, icon: 'vm-start' },
{ handler: restartVms, icon: 'vm-reboot' },
{ handler: migrateVms, icon: 'vm-migrate' },
{ handler: copyVms, icon: 'vm-copy' }
{ 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,
@@ -136,20 +141,32 @@ const OPTIONS = {
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'),
@@ -161,7 +178,8 @@ const DEFAULT_TYPE = 'VM'
areObjectsFetched,
items: createGetObjectsOfType(type),
noServersConnected,
type
type,
user: getUser
}
})
export default class Home extends Component {
@@ -182,6 +200,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)
@@ -196,7 +217,7 @@ export default class Home extends Component {
pathname,
query: { ...query, t: type, s: undefined }
})
this._focusFilterInput()
this.setState({ highlighted: undefined })
}
_getDefaultFilter (props = this.props) {
@@ -218,16 +239,19 @@ export default class Home extends Component {
}
// Filter defined.
return homeFilters[type][filterName] ||
filters[type][filterName] ||
let tmp
return firstDefined(
(tmp = homeFilters[type]) && tmp[filterName],
(tmp = filters[type]) && tmp[filterName],
defaultFilter
)
}
_initFilter (props) {
const filter = this._getFilter(props)
// If filter is null, set a default filter.
if (filter == null || (this.props.user == null && props.user != null)) {
if (filter == null) {
const defaultFilter = this._getDefaultFilter(props)
if (defaultFilter != null) {
@@ -253,7 +277,6 @@ export default class Home extends Component {
const { filterInput } = this.refs
if (filterInput && filterInput.value !== filter) {
filterInput.value = filter
filterInput.focus()
}
}
@@ -308,7 +331,8 @@ 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 })
@@ -385,8 +409,6 @@ export default class Home extends Component {
this._updateMasterCheckbox()
}
_focusFilterInput = () => this.refs.filterInput.focus()
_addCustomFilter = () => {
return addCustomFilter(
this._getType(),
@@ -405,6 +427,38 @@ export default class Home extends Component {
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 { type } = this.props
const { filters } = OPTIONS[type]
@@ -414,15 +468,7 @@ export default class Home extends Component {
<Row className={styles.itemRowHeader}>
<Col mediumSize={3}>
<DropdownButton id='typeMenu' bsStyle='info' title={TYPES[this._getType()]}>
<MenuItem onClick={() => this._setType('VM')}>
VM
</MenuItem>
<MenuItem onClick={() => this._setType('host')}>
Host
</MenuItem>
<MenuItem onClick={() => this._setType('pool')}>
Pool
</MenuItem>
{this._typesDropdownItems}
</DropdownButton>
</Col>
<Col mediumSize={6}>
@@ -447,7 +493,6 @@ export default class Home extends Component {
</div>
)}
<input
autoFocus
className='form-control'
defaultValue={this._getFilter()}
onChange={this._onFilterChange}
@@ -538,23 +583,25 @@ 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>
@@ -562,19 +609,15 @@ export default class Home extends Component {
const filteredItems = this._getFilteredItems()
const visibleItems = this._getVisibleItems()
const { activePage, sortBy } = this.state
const items = {
'VM': VmItem,
'host': HostItem,
'pool': PoolItem
}
const { activePage, sortBy, highlighted } = this.state
const { type } = props
const Item = items[type] || items[DEFAULT_TYPE]
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}>
@@ -597,108 +640,104 @@ export default class Home extends Component {
</span>
</Col>
<Col mediumSize={8} className='text-xs-right hidden-sm-down'>
{this.state.displayActions
? (
<div>
{mainActions && (
<div className='btn-group'>
{map(mainActions, (action, key) => (
<ActionButton
btnStyle='secondary'
key={key}
{...action}
handlerParam={selectedItemsIds}
/>
))}
{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>
)}
{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)}
) : <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>
) : <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._focusFilterInput()
}}>
{this._tick(_sortBy === sortBy)}
{_sortBy === sortBy
? <strong>{_(labelId)}</strong>
: _(labelId)
}
</MenuItem>
))}
</DropdownButton>
</div>
}
</div>
}
</Col>
<Col smallsize={1} mediumSize={1} className='text-xs-right'>
<button className='btn btn-secondary'
@@ -707,15 +746,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 mt-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,8 +121,8 @@ 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' />
{' '}
@@ -132,12 +131,17 @@ export default class PoolItem extends Component {
{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='mr-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

@@ -107,7 +107,7 @@ export default class VmItem extends Component {
</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
@@ -97,16 +99,7 @@ const isRunning = host => host && host.power_state === 'Running'
}))
)
const getPbds = createGetObjectsOfType('PBD').pick(
createSelector(getHost, host => host.$PBDs)
)
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)
@@ -118,17 +111,20 @@ const isRunning = host => host && host.power_state === 'Running'
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),
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()
@@ -182,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)
}
@@ -234,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}>
@@ -252,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>
@@ -264,7 +267,7 @@ export default class Host extends Component {
render () {
const { host, pool } = this.props
if (!host) {
return <h1>Loading</h1>
return <h1>{_('statusLoading')}</h1>
}
const childProps = assign(pick(this.props, [
'host',

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,6 +3,7 @@ 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 { addTag, removeTag } from 'xo'
import { BlockLink } from 'link'
@@ -10,6 +11,7 @@ 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,70 +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.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}>
<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>
<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

@@ -1,17 +1,209 @@
import _ from 'intl'
import ActionRowButton from 'action-row-button'
import Component from 'base-component'
import React from 'react'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import pick from 'lodash/pick'
import SingleLineRow from 'single-line-row'
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 { confirm } from 'modal'
import { connectStore, noop } from 'utils'
import { Container, Row, Col } from 'grid'
import { createGetObjectsOfType } from 'selectors'
import { error } from 'notification'
import { Select, Number } from 'editable'
import { Toggle } from 'form'
import {
connectPif,
createNetwork,
deletePif,
disconnectPif,
editNetwork,
editPif,
getIpv4ConfigModes,
reconfigurePifIp
} from 'xo'
export default ({
const EDIT_BUTTON_STYLE = { color: '#999', cursor: 'pointer' }
const _toggleDefaultLockingMode = (component, tooltip) => tooltip
? <Tooltip content={tooltip}>
{component}
</Tooltip>
: component
class ConfigureIpModal extends Component {
constructor (props) {
super(props)
const { pif } = props
if (pif) {
this.state = pick(pif, ['ip', 'netmask', 'dns', 'gateway'])
}
}
get value () {
return this.state
}
render () {
const { ip, netmask, dns, gateway } = this.state
return <div>
<SingleLineRow>
<Col size={6}>{_('staticIp')}</Col>
<Col size={6}>
<input className='form-control' onChange={this.linkState('ip')} value={ip} />
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('netmask')}</Col>
<Col size={6}>
<input className='form-control' onChange={this.linkState('netmask')} value={netmask} />
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('dns')}</Col>
<Col size={6}>
<input className='form-control' onChange={this.linkState('dns')} value={dns} />
</Col>
</SingleLineRow>
&nbsp;
<SingleLineRow>
<Col size={6}>{_('gateway')}</Col>
<Col size={6}>
<input className='form-control' onChange={this.linkState('gateway')} value={gateway} />
</Col>
</SingleLineRow>
</div>
}
}
@connectStore(() => ({
vifsByNetwork: createGetObjectsOfType('VIF').groupBy('$network')
}))
class PifItem extends Component {
componentWillMount () {
getIpv4ConfigModes().then(configModes =>
this.setState({ configModes })
)
}
_configIp = mode => {
if (mode === 'Static') {
return confirm({
icon: 'ip',
title: _('pifConfigureIp'),
body: <ConfigureIpModal pif={this.props.pif} />
}).then(
params => {
if (!params.ip || !params.netmask) {
error(_('configIpErrorTitle'), _('configIpErrorMessage'))
return
}
return reconfigurePifIp(this.props.pif, { mode, ...params })
},
noop
)
}
return reconfigurePifIp(this.props.pif, { mode })
}
_onEditIp = () => this._configIp('Static')
_editPif = vlan =>
editPif(this.props.pif, { vlan })
render () {
const { networks, pif, vifsByNetwork } = this.props
const { configModes } = this.state
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
? 'None'
: <Number value={pif.vlan} onChange={this._editPif}>
{pif.vlan}
</Number>
}
</td>
<td>
{pif.ip}
{' '}
{pif.ip && <a className='hidden-md-down' onClick={this._onEditIp} style={EDIT_BUTTON_STYLE}>
<Icon icon='edit' size='1' fixedWidth />
</a>}
</td>
<td>
<Select
onChange={this._configIp}
options={configModes}
value={pif.mode}
>
{pif.mode}
</Select>
</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')}
</span>
: <span className='tag tag-default'>
{_('pifStatusDisconnected')}
</span>
}
</td>
<td>
<ButtonGroup className='pull-right'>
<ActionRowButton
btnStyle='default'
disabled={pif.attached && (pif.management || pif.disallowUnplug)}
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}
handler={deletePif}
handlerParam={{ pif }}
icon='delete'
tooltip={_('deletePif')}
/>
</ButtonGroup>
</td>
</tr>
}
}
export default (({
host,
networks,
pifs
pifs,
vifsByNetwork
}) => <Container>
<Row>
<Col className='text-xs-right'>
@@ -35,54 +227,16 @@ export default ({
<th>{_('pifNetworkLabel')}</th>
<th>{_('pifVlanLabel')}</th>
<th>{_('pifAddressLabel')}</th>
<th>{_('pifModeLabel')}</th>
<th>{_('pifMacLabel')}</th>
<th>{_('pifMtuLabel')}</th>
<th>{_('defaultLockingMode')}</th>
<th>{_('pifStatusLabel')}</th>
<th />
</tr>
</thead>
<tbody>
{map(pifs, pif =>
<tr key={pif.id}>
<td>{pif.device}</td>
<td>{networks[pif.$network].name_label}</td>
<td>{pif.vlan === -1
? 'None'
: pif.vlan}
</td>
<td>{pif.ip} ({pif.mode})</td>
<td><pre>{pif.mac}</pre></td>
<td>{pif.mtu}</td>
<td>
{pif.attached
? <span className='tag tag-success'>
{_('pifStatusConnected')}
</span>
: <span className='tag tag-default'>
{_('pifStatusDisconnected')}
</span>
}
</td>
<td>
<ButtonGroup className='pull-xs-right'>
<ActionRowButton
btnStyle='default'
disabled={pif.attached && (pif.management || pif.disallowUnplug)}
icon={pif.attached ? 'disconnect' : 'connect'}
handler={pif.attached ? disconnectPif : connectPif}
handlerParam={pif}
/>
<ActionRowButton
btnStyle='default'
disabled={pif.physical || pif.disallowUnplug || pif.management}
icon='delete'
handler={deletePif}
handlerParam={{ pif }}
/>
</ButtonGroup>
</td>
</tr>
)}
{map(pifs, pif => <PifItem pif={pif} networks={networks} />)}
</tbody>
</table>
</span>
@@ -90,4 +244,4 @@ export default ({
}
</Col>
</Row>
</Container>
</Container>)

View File

@@ -5,9 +5,11 @@ import React, { Component } from 'react'
import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import Upgrade from 'xoa-upgrade'
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 = [
{
@@ -60,6 +62,7 @@ const INSTALLED_PATCH_COLUMNS = [
sortCriteria: patch => patch.poolPatch.description
},
{
default: true,
name: _('patchApplied'),
itemRenderer: patch => {
const time = patch.time * 1000
@@ -81,44 +84,53 @@ const INSTALLED_PATCH_COLUMNS = [
}
]
@connectStore(() => ({
needsRestart: createDoesHostNeedRestart((_, props) => props.host)
}))
export default class HostPatches extends Component {
render () {
const { hostPatches, missingPatches, installAllPatches, installPatch } = this.props
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(hostPatches)
? (
<span>
<h3>{_('hostAppliedPatches')}</h3>
<SortedTable collection={hostPatches} columns={INSTALLED_PATCH_COLUMNS} defaultColumn={2} />
</span>
<span>
<h3>{_('hostAppliedPatches')}</h3>
<SortedTable collection={hostPatches} columns={INSTALLED_PATCH_COLUMNS} />
</span>
) : <h4 className='text-xs-center'>{_('patchNothing')}</h4>
}
</Col>

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-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-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,11 +1,18 @@
import Component from 'base-component'
import cookies from 'cookies-js'
import DocumentTitle from 'react-document-title'
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'
@@ -30,6 +37,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',
@@ -81,6 +92,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)
@@ -102,22 +121,72 @@ 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='mt-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}>
<Menu ref='menu' />
<div ref='bodyWrapper' style={BODY_WRAPPER_STYLE}>
<div style={BODY_STYLE}>
{blocked ? <XoaUpdates /> : this.props.children}
<DocumentTitle title='Xen Orchestra'>
<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}>
{blocked ? <XoaUpdates /> : this.props.children}
</div>
</div>
<Modal />
<Notification />
<TooltipViewer />
</div>
<TooltipViewer />
<Modal />
<Notification />
</div>
</DocumentTitle>
</IntlProvider>
}
}

View File

@@ -18,7 +18,7 @@ const HEADER = <Container>
<h2><Icon icon='jobs' /> {_('jobsPage')}</h2>
</Col>
<Col mediumSize={9}>
<NavTabs className='pull-xs-right'>
<NavTabs className='pull-right'>
<NavLink to={'/jobs/overview'}><Icon icon='menu-jobs-overview' /> {_('jobsOverviewPage')}</NavLink>
<NavLink to={'/jobs/new'}><Icon icon='menu-jobs-new' /> {_('jobsNewPage')}</NavLink>
<NavLink to={'/jobs/scheduling'}><Icon icon='menu-jobs-schedule' /> {_('jobsSchedulingPage')}</NavLink>

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>

View File

@@ -120,7 +120,7 @@ export default class Overview extends Component {
? <Container>
<Card>
<CardHeader>
<Icon icon='schedule' /> Schedules
<Icon icon='schedule' /> {_('backupSchedules')}
</CardHeader>
<CardBlock>
{schedules.length ? (
@@ -141,20 +141,20 @@ export default class Overview extends Component {
<tr key={key}>
<td>
{this._getScheduleLabel(schedule)}
<Link className='btn btn-sm btn-primary m-r-1' to={`/jobs/schedule/${schedule.id}/edit`}>
<Link className='btn btn-sm btn-primary mr-1' to={`/jobs/schedule/${schedule.id}/edit`}>
<Icon icon='edit' />
</Link>
</td>
<td>
{this._getJobLabel(job)}
<Link className='btn btn-sm btn-primary m-r-1' to={`/jobs/${job.id}/edit`}>
<Link className='btn btn-sm btn-primary mr-1' to={`/jobs/${job.id}/edit`}>
<Icon icon='edit' />
</Link>
</td>
<td className='hidden-xs-down'>{schedule.cron}</td>
<td>
{this._getScheduleToggle(schedule)}
<fieldset className='pull-xs-right'>
<fieldset className='pull-right'>
<ButtonGroup>
<ActionRowButton
icon='delete'

View File

@@ -1,4 +1,4 @@
import _ from 'intl'
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import find from 'lodash/find'
import Icon from 'icon'
@@ -8,6 +8,7 @@ import Upgrade from 'xoa-upgrade'
import React, { Component } from 'react'
import Scheduler, { SchedulePreview } from 'scheduling'
import { error } from 'notification'
import { injectIntl } from 'react-intl'
import { SelectPlainObject, Toggle } from 'form'
import {
@@ -22,6 +23,7 @@ const JOB_KEY = 'genericTask'
const DEFAULT_CRON_PATTERN = '0 0 * * *'
@injectIntl
export default class Schedules extends Component {
constructor (props) {
super(props)
@@ -140,13 +142,13 @@ export default class Schedules extends Component {
timezone
} = this.state
return <div>
<h1>Schedules</h1>
<h1>{_('jobSchedules')}</h1>
<form id='newScheduleForm'>
<div className='form-group'>
<input type='text' ref='name' className='form-control' placeholder='Name of your schedule' required />
<input type='text' ref='name' className='form-control' placeholder={this.props.intl.formatMessage(messages.jobScheduleNamePlaceHolder)} required />
</div>
<div className='form-group'>
<SelectPlainObject ref='job' options={map(jobs)} optionKey='id' placeholder='Select a Job' />
<SelectPlainObject ref='job' options={map(jobs)} optionKey='id' placeholder={this.props.intl.formatMessage(messages.jobScheduleJobPlaceHolder)} />
</div>
{!schedule &&
<div className='form-group'>
@@ -181,7 +183,7 @@ export default class Schedules extends Component {
<th>{_('job')}</th>
<th className='hidden-xs-down'>{_('jobScheduling')}</th>
<th className='hidden-xs-down'>{_('jobTimezone')}</th>
<th></th>
<th />
</tr>
</thead>
<tbody>

View File

@@ -11,7 +11,9 @@ import propTypes from 'prop-types'
import React, { Component } from 'react'
import renderXoItem from 'render-xo-item'
import SortedTable from 'sorted-table'
import Tooltip from 'tooltip'
import { alert, confirm } from 'modal'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { connectStore } from 'utils'
import { createGetObject } from 'selectors'
import { FormattedDate } from 'react-intl'
@@ -87,10 +89,6 @@ const Log = props => <ul className='list-group'>
const showCalls = log => alert(<span>{_('job')} {log.jobId}</span>, <Log log={log} />)
const LOG_COLUMNS = [
{
name: '',
itemRenderer: log => <ActionRowButton icon='preview' handler={showCalls} handlerParam={log} />
},
{
name: _('jobId'),
itemRenderer: log => log.jobId,
@@ -103,13 +101,14 @@ const LOG_COLUMNS = [
},
{
name: _('jobStart'),
itemRenderer: log => log.start && <FormattedDate value={new Date(log.start)} month='long' day='numeric' year='numeric' hour='2-digit' minute='2-digit' second='2-digit' />,
itemRenderer: log => log.start && <FormattedDate value={new Date(log.start)} month='short' day='numeric' year='numeric' hour='2-digit' minute='2-digit' second='2-digit' />,
sortCriteria: log => log.start,
sortOrder: 'desc'
},
{
default: true,
name: _('jobEnd'),
itemRenderer: log => log.end && <FormattedDate value={new Date(log.end)} month='long' day='numeric' year='numeric' hour='2-digit' minute='2-digit' second='2-digit' />,
itemRenderer: log => log.end && <FormattedDate value={new Date(log.end)} month='short' day='numeric' year='numeric' hour='2-digit' minute='2-digit' second='2-digit' />,
sortCriteria: log => log.end,
sortOrder: 'desc'
},
@@ -132,7 +131,10 @@ const LOG_COLUMNS = [
}
{' '}
<span className='pull-right'>
<ActionRowButton btnStyle='default' handler={deleteJobsLog} handlerParam={log.logKey} icon='delete' />
<ButtonGroup>
<Tooltip content={_('logDisplayDetails')}><ActionRowButton icon='preview' handler={showCalls} handlerParam={log} /></Tooltip>
<Tooltip content={_('remove')}><ActionRowButton btnStyle='default' handler={deleteJobsLog} handlerParam={log.logKey} icon='delete' /></Tooltip>
</ButtonGroup>
</span>
</span>,
sortCriteria: log => log.hasErrors && ' ' || log.status

View File

@@ -9,13 +9,19 @@ import React from 'react'
import Tooltip from 'tooltip'
import { Button } from 'react-bootstrap-4/lib'
import { connectStore, noop, getXoaPlan } from 'utils'
import { signOut, subscribePermissions, subscribeResourceSets } from 'xo'
import { UpdateTag } from '../xoa-updates'
import {
connect,
signOut,
subscribePermissions,
subscribeResourceSets
} from 'xo'
import {
createFilter,
createGetObjectsOfType,
createSelector,
getLang,
getStatus,
getUser
} from 'selectors'
@@ -31,6 +37,8 @@ import styles from './index.css'
[ task => task.status === 'pending' ]
),
pools: createGetObjectsOfType('pool'),
nHosts: createGetObjectsOfType('host').count(),
status: getStatus,
user: getUser
}), {
withRef: true
@@ -88,16 +96,18 @@ export default class Menu extends Component {
}
render () {
const { nTasks, user } = this.props
const { nTasks, status, user, pools, nHosts } = this.props
const isAdmin = user && user.permission === 'admin'
const noOperatablePools = this._getNoOperatablePools()
const noResourceSets = isEmpty(this.state.resourceSets)
/* eslint-disable object-property-newline */
const items = [
{ to: '/home', icon: 'menu-home', label: 'homePage', subMenu: [
{ to: '/home?t=VM', icon: 'vm', label: 'homeVmPage' },
{ to: '/home?t=host', icon: 'host', label: 'homeHostPage' },
{ to: '/home?t=pool', icon: 'pool', label: 'homePoolPage' }
nHosts !== 0 && { to: '/home?t=host', icon: 'host', label: 'homeHostPage' },
!isEmpty(pools) && { to: '/home?t=pool', icon: 'pool', label: 'homePoolPage' },
isAdmin && { to: '/home?t=VM-template', icon: 'template', label: 'homeTemplatePage' }
]},
{ to: '/dashboard/overview', icon: 'menu-dashboard', label: 'dashboardPage', subMenu: [
{ to: '/dashboard/overview', icon: 'menu-dashboard-overview', label: 'overviewDashboardPage' },
@@ -105,10 +115,7 @@ export default class Menu extends Component {
{ to: '/dashboard/stats', icon: 'menu-dashboard-stats', label: 'overviewStatsDashboardPage' },
{ to: '/dashboard/health', icon: 'menu-dashboard-health', label: 'overviewHealthDashboardPage' }
]},
isAdmin && { to: '/self/dashboard', icon: 'menu-self-service', label: 'selfServicePage', subMenu: [
{ to: '/self/dashboard', icon: 'menu-self-service-dashboard', label: 'selfServiceDashboardPage' },
{ to: '/self/admin', icon: 'menu-self-service-admin', label: 'selfServiceAdminPage' }
]},
isAdmin && { to: '/self', icon: 'menu-self-service', label: 'selfServicePage' },
{ to: '/backup/overview', icon: 'menu-backup', label: 'backupPage', subMenu: [
{ to: '/backup/overview', icon: 'menu-backup-overview', label: 'backupOverviewPage' },
{ to: '/backup/new', icon: 'menu-backup-new', label: 'backupNewPage' },
@@ -121,7 +128,10 @@ export default class Menu extends Component {
{ to: '/settings/groups', icon: 'menu-settings-groups', label: 'settingsGroupsPage' },
{ to: '/settings/acls', icon: 'menu-settings-acls', label: 'settingsAclsPage' },
{ to: '/settings/remotes', icon: 'menu-backup-remotes', label: 'backupRemotesPage' },
{ to: '/settings/plugins', icon: 'menu-settings-plugins', label: 'settingsPluginsPage' }
{ to: '/settings/plugins', icon: 'menu-settings-plugins', label: 'settingsPluginsPage' },
{ to: '/settings/logs', icon: 'menu-settings-logs', label: 'settingsLogsPage' },
{ to: '/settings/ips', icon: 'ip', label: 'settingsIpsPage' },
{ to: '/settings/config', icon: 'menu-settings-config', label: 'settingsConfigPage' }
]},
{ to: '/jobs/overview', icon: 'menu-jobs', label: 'jobsPage', subMenu: [
{ to: '/jobs/overview', icon: 'menu-jobs-overview', label: 'jobsOverviewPage' },
@@ -137,6 +147,7 @@ export default class Menu extends Component {
!noOperatablePools && { to: '/vms/import', icon: 'menu-new-import', label: 'newImport' }
]}
]
/* eslint-enable object-property-newline */
return <div className={classNames(
'xo-menu',
@@ -166,7 +177,7 @@ export default class Menu extends Component {
{+process.env.XOA_PLAN === 5
? <span>
<span className={classNames(styles.hiddenCollapsed, 'text-warning')}>
<Icon icon='alarm' size='lg' fixedWidth /> No support
<Icon icon='alarm' size='lg' fixedWidth /> {_('noSupport')}
</span>
<span className={classNames(styles.hiddenUncollapsed, 'text-warning')}>
<Icon icon='alarm' size='lg' fixedWidth />
@@ -175,7 +186,7 @@ export default class Menu extends Component {
: +process.env.XOA_PLAN === 1
? <span>
<span className={classNames(styles.hiddenCollapsed, 'text-warning')}>
<Icon icon='info' size='lg' fixedWidth /> Free upgrade!
<Icon icon='info' size='lg' fixedWidth /> {_('freeUpgrade')}
</span>
<span className={classNames(styles.hiddenUncollapsed, 'text-warning')}>
<Icon icon='info' size='lg' fixedWidth />
@@ -200,15 +211,24 @@ export default class Menu extends Component {
<span className={styles.hiddenCollapsed}>{' '}{_('signOut')}</span>
</Button>
</li>
<li className='nav-item'>
<Link className='nav-link' style={{display: 'flex'}} to={'/user'}>
<div style={{margin: 'auto'}}>
<Tooltip content={user ? user.email : ''}>
<Icon icon='user' size='lg' />
</Tooltip>
</div>
<li className='nav-item xo-menu-item'>
<Link className='nav-link text-xs-center' to={'/user'}>
<Tooltip content={_('editUserProfile', { username: user ? user.email : '' })}>
<Icon icon='user' size='lg' />
</Tooltip>
</Link>
</li>
<li>&nbsp;</li>
<li>&nbsp;</li>
{status === 'connecting'
? <li className='nav-item text-xs-center'>{_('statusConnecting')}</li>
: status === 'disconnected' &&
<li className='nav-item text-xs-center xo-menu-item'>
<Button className='nav-link' onClick={connect}>
<Icon icon='alarm' size='lg' fixedWidth /> {_('statusDisconnected')}
</Button>
</li>
}
</ul>
</div>
}

View File

@@ -54,14 +54,13 @@
.configDrive {
display: flex;
flex-direction: column;
background-color: #eee;
padding: 1em;
margin-bottom: 0.5em;
}
.configDriveToggle {
margin-left: auto;
margin-right: auto;
margin: auto;
}
.refreshNames {
@@ -71,3 +70,11 @@
.customConfig {
resize: both;
}
.fixedWidth {
width: 20em;
}
.tags {
font-size: 1.5em;
}

File diff suppressed because it is too large Load Diff

View File

@@ -465,7 +465,7 @@ export default class New extends Component {
<input
id='srName'
className='form-control'
placeholder='storage name'
placeholder={formatMessage(messages.newSrNamePlaceHolder)}
ref='name'
onBlur={this._handleNameChange}
required
@@ -475,7 +475,7 @@ export default class New extends Component {
<input
id='srDescription'
className='form-control'
placeholder='storage description'
placeholder={formatMessage(messages.newSrDescPlaceHolder)}
ref='description'
onBlur={this._handleDescriptionChange}
required
@@ -509,7 +509,7 @@ export default class New extends Component {
<input
id='srServer'
className='form-control'
placeholder='address'
placeholder={formatMessage(messages.newSrAddressPlaceHolder)}
ref='server'
required
type='text'
@@ -547,7 +547,7 @@ export default class New extends Component {
<input
id='srServer'
className='form-control'
placeholder='address'
placeholder={formatMessage(messages.newSrAddressPlaceHolder)}
ref='server'
required
type='text'
@@ -556,7 +556,7 @@ export default class New extends Component {
<input
id='srServer'
className='form-control'
placeholder='[port]'
placeholder={formatMessage(messages.newSrPortPlaceHolder)}
ref='port'
type='text'
/>
@@ -568,14 +568,14 @@ export default class New extends Component {
<input
id='srServerUser'
className='form-control'
placeholder='user'
placeholder={formatMessage(messages.newSrUsernamePlaceHolder)}
ref='username'
required
type='text'
/>
<label>{_('newSrPassword')}</label>
<Password
placeholder='password'
placeholder={formatMessage(messages.newSrPasswordPlaceHolder)}
ref='password'
required
/>
@@ -607,7 +607,7 @@ export default class New extends Component {
<input
id='srServer'
className='form-control'
placeholder='address'
placeholder={formatMessage(messages.newSrAddressPlaceHolder)}
ref='server'
required
type='text'
@@ -616,14 +616,14 @@ export default class New extends Component {
<input
id='srServerUser'
className='form-control'
placeholder='user'
placeholder={formatMessage(messages.newSrUsernamePlaceHolder)}
ref='username'
required
type='text'
/>
<label>{_('newSrPassword')}</label>
<Password
placeholder='password'
placeholder={formatMessage(messages.newSrPasswordPlaceHolder)}
ref='password'
required
/>
@@ -635,7 +635,7 @@ export default class New extends Component {
<input
id='srDevice'
className='form-control'
placeholder='Device, e.g /dev/sda...'
placeholder={formatMessage(messages.newSrLvmDevicePlaceHolder)}
ref='device'
required
type='text'
@@ -648,7 +648,7 @@ export default class New extends Component {
<input
id='srPath'
className='form-control'
placeholder=''
placeholder={formatMessage(messages.newSrLocalPathPlaceHolder)}
ref='localPath'
required
type='text'

View File

@@ -7,28 +7,33 @@ import styles from './index.css'
const Page = ({
children,
collapsedHeader,
formatTitle,
header,
intl,
title = 'Xen Orchestra'
title
}) => {
const { formatMessage } = intl
return (
<DocumentTitle title={formatTitle ? formatMessage(messages[title]) : title}>
<div className={styles.container}>
<nav className={'page-header ' + styles.header}>
{header}
</nav>
<div className={styles.content}>
{children}
</div>
</div>
const content = <div className={styles.container}>
{!collapsedHeader && <nav className={'page-header ' + styles.header}>
{header}
</nav>}
<div className={styles.content}>
{children}
</div>
</div>
return title
? <DocumentTitle title={formatTitle ? formatMessage(messages[title]) : title}>
{content}
</DocumentTitle>
)
: content
}
Page.propTypes = {
children: React.PropTypes.node,
collapsedHeader: React.PropTypes.bool,
formatTitle: React.PropTypes.bool,
header: React.PropTypes.node,
title: React.PropTypes.string

View File

@@ -1,5 +1,8 @@
import ActionBar from 'action-bar'
import React from 'react'
import {
addHostToPool
} from 'xo'
const NOT_IMPLEMENTED = () => {
throw new Error('not implemented')
@@ -11,17 +14,17 @@ const PoolActionBar = ({ pool }) => (
{
icon: 'add-sr',
label: 'addSrLabel',
handler: NOT_IMPLEMENTED // TODO add sr
redirectOnSuccess: `new/sr?host=${pool.master}`
},
{
icon: 'add-vm',
label: 'addVmLabel',
handler: NOT_IMPLEMENTED // TODO add VM
redirectOnSuccess: `vms/new?pool=${pool.id}`
},
{
icon: 'add-host',
label: 'addHostLabel',
handler: NOT_IMPLEMENTED // TODO add host
handler: addHostToPool
},
{
icon: 'disconnect',

View File

@@ -95,6 +95,9 @@ import TabStorage from './tab-storage'
})
export default class Pool extends Component {
_setNameDescription = nameDescription => editPool(this.props.pool, { name_description: nameDescription })
_setNameLabel = nameLabel => editPool(this.props.pool, { name_label: nameLabel })
header () {
const { pool } = this.props
if (!pool) {
@@ -108,13 +111,13 @@ export default class Pool extends Component {
{' '}
<Text
value={pool.name_label}
onChange={nameLabel => editPool(pool, { nameLabel })}
onChange={this._setNameLabel}
/>
</h2>
<span>
<Text
value={pool.name_description}
onChange={nameDescription => editPool(pool, { nameDescription })}
onChange={this._setNameDescription}
/>
</span>
</Col>
@@ -144,7 +147,7 @@ export default class Pool extends Component {
render () {
const { pool } = this.props
if (!pool) {
return <h1>Loading</h1>
return <h1>{_('statusLoading')}</h1>
}
const childProps = assign(pick(this.props, [
'hosts',

View File

@@ -1,8 +1,15 @@
import _ from 'intl'
import find from 'lodash/find'
import Icon from 'icon'
import map from 'lodash/map'
import React from 'react'
import sumBy from 'lodash/sumBy'
import Tags from 'tags'
import { addTag, removeTag } from 'xo'
import Link, { BlockLink } from 'link'
import { Container, Row, Col } from 'grid'
import Usage, { UsageElement } from 'usage'
import { formatSize } from 'utils'
export default ({
hosts,
@@ -10,20 +17,49 @@ export default ({
pool,
srs
}) => <Container>
<br />
<Row className='text-xs-center'>
<Col mediumSize={4}>
<h2>{hosts.length}x <Icon icon='host' size='lg' /></h2>
<BlockLink to={`/pools/${pool.id}/hosts`}><h2>{hosts.length}x <Icon icon='host' size='lg' /></h2></BlockLink>
</Col>
<Col mediumSize={4}>
<h2>{srs.length}x <Icon icon='sr' size='lg' /></h2>
<BlockLink to={`/pools/${pool.id}/storage`}><h2>{srs.length}x <Icon icon='sr' size='lg' /></h2></BlockLink>
</Col>
<Col mediumSize={4}>
<h2>{nVms}x <Icon icon='vm' size='lg' /></h2>
<BlockLink to={`/home?s=$pool:${pool.id}`}><h2>{nVms}x <Icon icon='vm' size='lg' /></h2></BlockLink>
</Col>
</Row>
<br />
<Row>
<Col className='text-xs-center'>
<h5>{_('poolTitleRamUsage')}</h5>
</Col>
</Row>
<Row>
<Col smallOffset={1} mediumSize={10}>
<Usage total={sumBy(hosts, 'memory.size')}>
{map(hosts, host => <UsageElement
tooltip={host.name_label}
key={host.id}
value={host.memory.usage}
href={`#/hosts/${host.id}`}
/>)}
</Usage>
</Col>
</Row>
<Row>
<Col className='text-xs-center'>
<h5>{_('poolRamUsage', {used: formatSize(sumBy(hosts, 'memory.usage')), total: formatSize(sumBy(hosts, 'memory.size'))})}</h5>
</Col>
</Row>
<Row className='text-xs-center'>
<Col>
<h2 className='text-xs-center'>
{_('poolMaster')} <Link to={`/hosts/${pool.master}`}>{find(hosts, host => host.id === pool.master).name_label}</Link>
</Col>
</Row>
<Row className='text-xs-center'>
<Col>
<h2>
<Tags labels={pool.tags} onDelete={tag => removeTag(pool.id, tag)} onAdd={tag => addTag(pool.id, tag)} />
</h2>
</Col>

View File

@@ -3,17 +3,24 @@ import isEmpty from 'lodash/isEmpty'
import Link from 'link'
import React from 'react'
import SortedTable from 'sorted-table'
import store from 'store'
import Tooltip from 'tooltip'
import { Container, Row, Col } from 'grid'
import { editHost } from 'xo'
import { Text } from 'editable'
import { formatSize } from 'utils'
import { getObject } from 'selectors'
const HOST_COLUMNS = [
{
name: _('hostNameLabel'),
itemRenderer: host => (
<Link to={`/hosts/${host.id}`}>
<Text value={host.name_label} onChange={value => editHost(host, { name_label: value })} useLongClick />
</Link>
<span>
<Link to={`/hosts/${host.id}`}>
<Text value={host.name_label} onChange={value => editHost(host, { name_label: value })} useLongClick />
</Link>
{host.id === getObject(store.getState(), host.$pool).master && <span className='tag tag-pill tag-info'>{_('pillMaster')}</span>}
</span>
),
sortCriteria: 'name_label'
},
@@ -24,7 +31,10 @@ const HOST_COLUMNS = [
},
{
name: _('hostMemory'),
itemRenderer: ({ memory }) => <meter value={memory.usage} min='0' max={memory.size}></meter>,
itemRenderer: ({ memory }) =>
<Tooltip content={_('memoryLeftTooltip', {used: Math.round((memory.usage / memory.size) * 100), free: formatSize(memory.size - memory.usage)})}>
<meter value={memory.usage} min='0' max={memory.size} />
</Tooltip>,
sortCriteria: ({ memory }) => memory.usage / memory.size,
sortOrder: 'desc'
}

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