Compare commits
221 Commits
v5.1.4
...
xo-web/v5.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39342cd662 | ||
|
|
051a3ac122 | ||
|
|
f842a321ba | ||
|
|
3cd2dd65d3 | ||
|
|
5ce7e0b108 | ||
|
|
71c2058cc8 | ||
|
|
f200d39d23 | ||
|
|
7932845ac5 | ||
|
|
94bda6ac9e | ||
|
|
7a65f80406 | ||
|
|
36ab58dad9 | ||
|
|
e9be9e3761 | ||
|
|
b54645c86c | ||
|
|
ab77d8430c | ||
|
|
c6f683b532 | ||
|
|
a2604f5156 | ||
|
|
5ae7f683d6 | ||
|
|
f953c89979 | ||
|
|
bb8aab02ea | ||
|
|
af0c03ff6a | ||
|
|
8859900537 | ||
|
|
130852ab85 | ||
|
|
65fa8f96b4 | ||
|
|
0a84e9e363 | ||
|
|
163c69454b | ||
|
|
49d3fde0f3 | ||
|
|
bb67e2254e | ||
|
|
6d2abc4e74 | ||
|
|
4875450053 | ||
|
|
19184ca8a0 | ||
|
|
654c3d324b | ||
|
|
c5b4811f16 | ||
|
|
7a9dc4fd59 | ||
|
|
e79096626a | ||
|
|
332d074d32 | ||
|
|
e511ecd76e | ||
|
|
bcfbd5eba9 | ||
|
|
9fa3db395b | ||
|
|
52a41ceb04 | ||
|
|
e65d67266d | ||
|
|
0d1045821c | ||
|
|
45d526dda2 | ||
|
|
e52f998e78 | ||
|
|
42ed3b9355 | ||
|
|
563b4cb1ec | ||
|
|
45bad231cf | ||
|
|
d76bd2484b | ||
|
|
445b60bb63 | ||
|
|
3214e0e41e | ||
|
|
c61230e145 | ||
|
|
fac6a29226 | ||
|
|
7a8f414748 | ||
|
|
9f450d282e | ||
|
|
31787067e3 | ||
|
|
1a769b23e2 | ||
|
|
ae002abafc | ||
|
|
31a25d9c16 | ||
|
|
356295c361 | ||
|
|
d10681b6d1 | ||
|
|
0602410aa8 | ||
|
|
1112768adc | ||
|
|
86b599df89 | ||
|
|
88f7661172 | ||
|
|
29c96c0119 | ||
|
|
d8c6e54c68 | ||
|
|
df053eb016 | ||
|
|
d1715f7711 | ||
|
|
240282c72d | ||
|
|
9e8dd6ea21 | ||
|
|
32806a20c9 | ||
|
|
34dcfbbf49 | ||
|
|
91fec43866 | ||
|
|
aa2d196a79 | ||
|
|
180ca458ad | ||
|
|
aa881c60e7 | ||
|
|
5b6966042d | ||
|
|
dc859da0cd | ||
|
|
151eb6cbd6 | ||
|
|
16db591bbf | ||
|
|
05a55e5eb2 | ||
|
|
dcd84b2b8f | ||
|
|
4a89119f0a | ||
|
|
bc1c30a7bf | ||
|
|
33cffbf28b | ||
|
|
a18b68116c | ||
|
|
d5acf15bca | ||
|
|
84f970af68 | ||
|
|
969f636bb7 | ||
|
|
6939aee20a | ||
|
|
ab2a02a555 | ||
|
|
70038e0764 | ||
|
|
e730ef5e11 | ||
|
|
835ad5aaf1 | ||
|
|
ac645c8617 | ||
|
|
b801fdbab2 | ||
|
|
bf495953e2 | ||
|
|
45b165deec | ||
|
|
09169578e8 | ||
|
|
43b2366927 | ||
|
|
f015a69eec | ||
|
|
99568508dd | ||
|
|
e8515344dd | ||
|
|
edc873a570 | ||
|
|
1a03e96ab2 | ||
|
|
89e0bb4f0a | ||
|
|
7d0fd60908 | ||
|
|
6b20523df4 | ||
|
|
e9a612647e | ||
|
|
28404ef149 | ||
|
|
a5f8230def | ||
|
|
39171de5de | ||
|
|
5aa5a0acbc | ||
|
|
a4518e630a | ||
|
|
94975f5ea6 | ||
|
|
7e98838d96 | ||
|
|
e8c9c196ff | ||
|
|
db314a238f | ||
|
|
2c85a6d4ab | ||
|
|
b683e14e80 | ||
|
|
ba45095fa8 | ||
|
|
b8e5ffa9f7 | ||
|
|
b4bff9e032 | ||
|
|
0c461bc4e2 | ||
|
|
a33b2a5294 | ||
|
|
298e1c4471 | ||
|
|
1c70cdc10b | ||
|
|
160e4bb530 | ||
|
|
e69ba8dd96 | ||
|
|
e55f4c3eb2 | ||
|
|
1a3272b980 | ||
|
|
7bed5e025a | ||
|
|
29d22c0598 | ||
|
|
a38c7c34ac | ||
|
|
8d690ce4ff | ||
|
|
2569568a03 | ||
|
|
2c6ff6b5b8 | ||
|
|
1257f01027 | ||
|
|
fad6830863 | ||
|
|
66262bb20b | ||
|
|
4abb0754c7 | ||
|
|
78c53bf3ad | ||
|
|
810d666d84 | ||
|
|
67699f0bb6 | ||
|
|
46274948c0 | ||
|
|
28e3a842ef | ||
|
|
6d90f1d45d | ||
|
|
09642c347d | ||
|
|
2d0e06f785 | ||
|
|
a5bc8497cf | ||
|
|
4bcb65c518 | ||
|
|
25361fa7eb | ||
|
|
889a265000 | ||
|
|
3122f6dcd5 | ||
|
|
16aa2e8085 | ||
|
|
074d51a670 | ||
|
|
2122a79132 | ||
|
|
26dbc585ba | ||
|
|
4b3cfbd424 | ||
|
|
035191a2cc | ||
|
|
06a40180a1 | ||
|
|
aaf4c5dff7 | ||
|
|
0c83bc2b0e | ||
|
|
2d412fd8db | ||
|
|
443e2bec25 | ||
|
|
d5e1323d82 | ||
|
|
7f0b77cc89 | ||
|
|
0169cff66c | ||
|
|
0fd1424a41 | ||
|
|
6280d56f32 | ||
|
|
9f2a77872f | ||
|
|
b571c18e9a | ||
|
|
49863d6e4d | ||
|
|
48cc7bb647 | ||
|
|
442d42d8dc | ||
|
|
9501ebacfc | ||
|
|
23f9fa46f8 | ||
|
|
1bd0f37fd4 | ||
|
|
ed74ded923 | ||
|
|
b732410b74 | ||
|
|
a51f2b7fcf | ||
|
|
fe12bbb60d | ||
|
|
8882df7939 | ||
|
|
185a554cd9 | ||
|
|
230e0dc2a5 | ||
|
|
f5b69fdfdc | ||
|
|
01dc0d8f1e | ||
|
|
8035886a3c | ||
|
|
0ab5f4b13f | ||
|
|
a1bc98def8 | ||
|
|
868cf6140b | ||
|
|
4b3473f480 | ||
|
|
7bc782cc62 | ||
|
|
e625a53e4a | ||
|
|
b31185d96d | ||
|
|
09d75e972f | ||
|
|
f33568951b | ||
|
|
8d8c442be5 | ||
|
|
f890b8ea7a | ||
|
|
1b80b3929c | ||
|
|
4f946293f6 | ||
|
|
36788cde2b | ||
|
|
1547c99e5a | ||
|
|
5c9606dad8 | ||
|
|
fdcb1dccf5 | ||
|
|
12812b8c23 | ||
|
|
0098497255 | ||
|
|
6562d2de7f | ||
|
|
1f0e88cdb0 | ||
|
|
197da91ef3 | ||
|
|
cbd59789e2 | ||
|
|
190ecf3d74 | ||
|
|
15b8f6bca2 | ||
|
|
5b406d731b | ||
|
|
4be9e67ac4 | ||
|
|
d047421685 | ||
|
|
f6f415a421 | ||
|
|
edfaaebac0 | ||
|
|
67df22a1bf | ||
|
|
7dc59a00f6 | ||
|
|
6214fe4c2e | ||
|
|
21610c3e0a |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -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.*
|
||||
|
||||
@@ -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:
|
||||
|
||||
177
CHANGELOG.md
177
CHANGELOG.md
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
40
gulpfile.js
40
gulpfile.js
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
53
package.json
53
package.json
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -35,8 +35,8 @@ class DebugAsync extends Component {
|
||||
|
||||
return <pre>
|
||||
{'Promise { '}
|
||||
{status === 'rejected' && '<rejected> '}
|
||||
{toString(value)}
|
||||
{status === 'rejected' && '<rejected> '}
|
||||
{toString(value)}
|
||||
{' }'}
|
||||
</pre>
|
||||
}
|
||||
|
||||
22
src/common/dropzone/index.css
Normal file
22
src/common/dropzone/index.css
Normal 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;
|
||||
}
|
||||
20
src/common/dropzone/index.js
Normal file
20
src/common/dropzone/index.js
Normal 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>
|
||||
}
|
||||
}
|
||||
13
src/common/editable/index.css
Normal file
13
src/common/editable/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.clickToEdit * {
|
||||
cursor: context-menu !important;
|
||||
}
|
||||
.shortClick {
|
||||
border-bottom: 1px dashed #ccc;
|
||||
}
|
||||
|
||||
.select {
|
||||
padding: 0px;
|
||||
}
|
||||
.size {
|
||||
width: 10rem;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -14,3 +14,7 @@ export const host = {
|
||||
export const pool = {
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const vmTemplate = {
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
@@ -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()}>
|
||||
|
||||
@@ -66,7 +66,7 @@ export class IntlProvider extends Component {
|
||||
locale={lang}
|
||||
messages={locales[lang]}
|
||||
>
|
||||
{children}
|
||||
{children}
|
||||
</IntlProvider_>
|
||||
}
|
||||
}
|
||||
|
||||
0
src/common/intl/locales/.index-modules
Normal file
0
src/common/intl/locales/.index-modules
Normal 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
@@ -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
126
src/common/ip.js
Normal 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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
})}
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
15
src/common/react-novnc.js
vendored
15
src/common/react-novnc.js
vendored
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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') })
|
||||
|
||||
@@ -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
35
src/common/shortcuts.js
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
.clickableColumn {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickableColumn:hover {
|
||||
color: #fff;
|
||||
background-color: #96b8d1;
|
||||
color: #fff;
|
||||
background-color: #96b8d1;
|
||||
}
|
||||
|
||||
.clickableRow {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
: []
|
||||
|
||||
@@ -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'
|
||||
>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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} />
|
||||
|
||||
37
src/common/xo/add-host-modal/index.js
Normal file
37
src/common/xo/add-host-modal/index.js
Normal 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>
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
109
src/common/xo/create-bonded-network-modal/index.js
Normal file
109
src/common/xo/create-bonded-network-modal/index.js
Normal 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>
|
||||
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('newNetworkName')}</Col>
|
||||
<Col size={6}>
|
||||
<input
|
||||
className='form-control'
|
||||
onChange={this.linkState('name')}
|
||||
type='text'
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('newNetworkDescription')}</Col>
|
||||
<Col size={6}>
|
||||
<input
|
||||
className='form-control'
|
||||
onChange={this.linkState('description')}
|
||||
type='text'
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
|
||||
<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>
|
||||
|
||||
<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 })
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
23
src/common/xo/revert-snapshot-modal/index.js
Normal file
23
src/common/xo/revert-snapshot-modal/index.js
Normal 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>
|
||||
}
|
||||
}
|
||||
@@ -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')}
|
||||
<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')}
|
||||
<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
BIN
src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -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;
|
||||
|
||||
@@ -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% }
|
||||
@@ -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
29
src/keymap.js
Normal 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}`)
|
||||
}))
|
||||
}))
|
||||
@@ -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
62
src/usage.scss
Normal 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;
|
||||
}
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
{container && host.id === container.master && <span className='tag tag-pill tag-info'>{_('pillMaster')}</span>}
|
||||
|
||||
{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 />
|
||||
@@ -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' />
|
||||
{' '} {' '}
|
||||
{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} />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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%'}}>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
92
src/xo-app/home/template-item.js
Normal file
92
src/xo-app/home/template-item.js
Normal 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} />
|
||||
|
||||
<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 />
|
||||
</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>
|
||||
}
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('netmask')}</Col>
|
||||
<Col size={6}>
|
||||
<input className='form-control' onChange={this.linkState('netmask')} value={netmask} />
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('dns')}</Col>
|
||||
<Col size={6}>
|
||||
<input className='form-control' onChange={this.linkState('dns')} value={dns} />
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
|
||||
<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>)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> </li>
|
||||
<li> </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>
|
||||
}
|
||||
|
||||
@@ -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
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user