Compare commits
273 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
87550b0189 | ||
|
|
b7c42d0a08 | ||
|
|
c15ad299ac | ||
|
|
48c56cd602 | ||
|
|
7957f621ef | ||
|
|
38ddbfdc9c | ||
|
|
3d2aae81da | ||
|
|
2227b9d061 | ||
|
|
12aab5fa8c | ||
|
|
7323e6e117 | ||
|
|
6f36869609 | ||
|
|
4a12419162 | ||
|
|
bf91938aa6 | ||
|
|
bd70bd2b45 | ||
|
|
bb26c8e449 | ||
|
|
93c7a01e62 | ||
|
|
9c2359e8ee | ||
|
|
5b9000012e | ||
|
|
bf00b4e8e3 | ||
|
|
ee7787f4ae | ||
|
|
0b88e743c9 | ||
|
|
f07a947580 | ||
|
|
0b8a9eedbc | ||
|
|
8d24e596ac | ||
|
|
c2378a44cd | ||
|
|
023f7fdef1 | ||
|
|
5d7a64bc28 | ||
|
|
8661957a97 | ||
|
|
7a15d265b7 | ||
|
|
2736881975 | ||
|
|
44a85f4e0c | ||
|
|
52a6e42e7e | ||
|
|
3dbe058d4e | ||
|
|
620139efc1 | ||
|
|
71464ac2e3 | ||
|
|
4a65489d39 | ||
|
|
65d7eac590 | ||
|
|
02bbc01dc4 | ||
|
|
3066237c86 | ||
|
|
53f3c0bef1 | ||
|
|
823c91b457 | ||
|
|
3bd7e20411 | ||
|
|
24d4610b04 | ||
|
|
b16097767a | ||
|
|
2ff74ffd39 | ||
|
|
f0bb464136 | ||
|
|
4767830386 | ||
|
|
ce23d4f164 | ||
|
|
c1380d1256 | ||
|
|
ed9a848858 | ||
|
|
5e4e15fc12 | ||
|
|
0dea952a2a | ||
|
|
a1818dd525 | ||
|
|
659e336f66 | ||
|
|
058f7ecd9f | ||
|
|
831d9cb49f | ||
|
|
a5d059b0b1 | ||
|
|
4c3b959869 | ||
|
|
d81a169a39 | ||
|
|
0d47332526 | ||
|
|
539d136936 | ||
|
|
4c28b5775d | ||
|
|
fe6f351f84 | ||
|
|
5dbeccf92f | ||
|
|
56bba1d84b | ||
|
|
af05d362b4 | ||
|
|
268ccf9a36 | ||
|
|
e77d4fafaa | ||
|
|
b88b99e342 | ||
|
|
f862d0df5b | ||
|
|
dac954155c | ||
|
|
cf9deceb15 | ||
|
|
72aed98088 | ||
|
|
ec92eddde8 | ||
|
|
e30b5ab6c3 | ||
|
|
0a5d26b001 | ||
|
|
7e4b881041 | ||
|
|
27a6af414f | ||
|
|
ba6204f811 | ||
|
|
d17b1050ad | ||
|
|
b70bc86f71 | ||
|
|
42b08633e9 | ||
|
|
bc898e1afd | ||
|
|
48d5f34ae6 | ||
|
|
67b8b15cd8 | ||
|
|
09d80afa69 | ||
|
|
c0d95304f6 | ||
|
|
5a0d67a9f6 | ||
|
|
08305b4b93 | ||
|
|
04d5612946 | ||
|
|
3dcb6f1f61 | ||
|
|
4e7684e38b | ||
|
|
a692b7571f | ||
|
|
a098618efa | ||
|
|
71381e75f1 | ||
|
|
05b345db4a |
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.*
|
||||
|
||||
265
CHANGELOG.md
265
CHANGELOG.md
@@ -1,5 +1,270 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.2.5** (2016-10-07)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Stats in home/host view when expanded [\#1634](https://github.com/vatesfr/xo-web/issues/1634)
|
||||
- Bar for used and total RAM on home pool view [\#1625](https://github.com/vatesfr/xo-web/issues/1625)
|
||||
- Can't translate some text [\#1624](https://github.com/vatesfr/xo-web/issues/1624)
|
||||
- Dynamic RAM allocation at creation time [\#1603](https://github.com/vatesfr/xo-web/issues/1603)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Do not expose shortcuts while console is focused [\#1614](https://github.com/vatesfr/xo-web/issues/1614)
|
||||
|
||||
## **5.2.4** (2016-10-04)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Display memory bar in home/host view [\#1616](https://github.com/vatesfr/xo-web/issues/1616)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- All users can see VM templates [\#1621](https://github.com/vatesfr/xo-web/issues/1621)
|
||||
|
||||
## **5.2.3** (2016-10-03)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Improve keyboard navigation [\#1578](https://github.com/vatesfr/xo-web/issues/1578)
|
||||
- Strongly suggest to install the guest tools [\#1575](https://github.com/vatesfr/xo-web/issues/1575)
|
||||
- Missing tooltip [\#1568](https://github.com/vatesfr/xo-web/issues/1568)
|
||||
- Emphasize already used ips in ipPools [\#1566](https://github.com/vatesfr/xo-web/issues/1566)
|
||||
- Change "missing feature message" for non-admins [\#1564](https://github.com/vatesfr/xo-web/issues/1564)
|
||||
- Allow VIF edition [\#1446](https://github.com/vatesfr/xo-web/issues/1446)
|
||||
- Disable browser autocomplete on credentials on the Update page [\#1304](https://github.com/vatesfr/xo-web/issues/1304)
|
||||
- keyboard shortcuts [\#1279](https://github.com/vatesfr/xo-web/issues/1279)
|
||||
- Add network bond creation [\#876](https://github.com/vatesfr/xo-web/issues/876)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Profile page is broken [\#1612](https://github.com/vatesfr/xo-web/issues/1612)
|
||||
- SR delete should redirect to home [\#1611](https://github.com/vatesfr/xo-web/issues/1611)
|
||||
- Delta VHD backup checksum is invalidated by chaining [\#1606](https://github.com/vatesfr/xo-web/issues/1606)
|
||||
- VM with long description break on 2 lines [\#1580](https://github.com/vatesfr/xo-web/issues/1580)
|
||||
- Network status on VM edition [\#1573](https://github.com/vatesfr/xo-web/issues/1573)
|
||||
- VM template deletion fails [\#1571](https://github.com/vatesfr/xo-web/issues/1571)
|
||||
- Template edition - "no such object" [\#1569](https://github.com/vatesfr/xo-web/issues/1569)
|
||||
- missing links / element not displayed as links [\#1567](https://github.com/vatesfr/xo-web/issues/1567)
|
||||
- Backup restore stalled on some SMB shares [\#1412](https://github.com/vatesfr/xo-web/issues/1412)
|
||||
- Wrong bond display [\#1156](https://github.com/vatesfr/xo-web/issues/1156)
|
||||
|
||||
## **5.2.2** (2016-09-21)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- `pool.setDefaultSr\(\)` should not require `pool` param [\#1558](https://github.com/vatesfr/xo-web/issues/1558)
|
||||
- Select default SR [\#1554](https://github.com/vatesfr/xo-web/issues/1554)
|
||||
- No error message when I exceed my resource set quota [\#1541](https://github.com/vatesfr/xo-web/issues/1541)
|
||||
- Hide some buttons for self service VMs [\#1539](https://github.com/vatesfr/xo-web/issues/1539)
|
||||
- Add Job ID to backup schedules [\#1534](https://github.com/vatesfr/xo-web/issues/1534)
|
||||
- Correct name for VM selector with templates [\#1530](https://github.com/vatesfr/xo-web/issues/1530)
|
||||
- Help text when no matches for a filter [\#1517](https://github.com/vatesfr/xo-web/issues/1517)
|
||||
- Icon or tooltip to allow VDI migration in VM disk view [\#1512](https://github.com/vatesfr/xo-web/issues/1512)
|
||||
- Create a snapshot before restoring one [\#1445](https://github.com/vatesfr/xo-web/issues/1445)
|
||||
- Auto power on setting at creation time [\#1444](https://github.com/vatesfr/xo-web/issues/1444)
|
||||
- local remotes should be avoided if possible [\#1441](https://github.com/vatesfr/xo-web/issues/1441)
|
||||
- Self service edition unclear [\#1429](https://github.com/vatesfr/xo-web/issues/1429)
|
||||
- Avoid "\_" char in job tag name [\#1414](https://github.com/vatesfr/xo-web/issues/1414)
|
||||
- Display message if host reboot needed to apply patches [\#1352](https://github.com/vatesfr/xo-web/issues/1352)
|
||||
- Color code on host PIF stats can be misleading [\#1265](https://github.com/vatesfr/xo-web/issues/1265)
|
||||
- Sign in page is not rendered correctly [\#1161](https://github.com/vatesfr/xo-web/issues/1161)
|
||||
- Template management [\#1091](https://github.com/vatesfr/xo-web/issues/1091)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Multiple reboot selection doesn't work [\#1562](https://github.com/vatesfr/xo-web/issues/1562)
|
||||
- Server logs should be displayed in reverse chonological order [\#1547](https://github.com/vatesfr/xo-web/issues/1547)
|
||||
- Cannot create resource sets without limits [\#1537](https://github.com/vatesfr/xo-web/issues/1537)
|
||||
- UI - Weird display when editing long VM desc [\#1528](https://github.com/vatesfr/xo-web/issues/1528)
|
||||
- Useless iso selector in host console [\#1527](https://github.com/vatesfr/xo-web/issues/1527)
|
||||
- Pool and Host dummy welcome message [\#1519](https://github.com/vatesfr/xo-web/issues/1519)
|
||||
- Bug on Network VM tab [\#1518](https://github.com/vatesfr/xo-web/issues/1518)
|
||||
- Link to home with filter in query does not work [\#1513](https://github.com/vatesfr/xo-web/issues/1513)
|
||||
- VHD merge fails with "RangeError: index out of range" on SMB remote [\#1511](https://github.com/vatesfr/xo-web/issues/1511)
|
||||
- DR: previous VDIs are not removed [\#1510](https://github.com/vatesfr/xo-web/issues/1510)
|
||||
- DR: previous copies not removed when same number as depth [\#1509](https://github.com/vatesfr/xo-web/issues/1509)
|
||||
- Empty Saved Search doesn't load when set to default filter [\#1354](https://github.com/vatesfr/xo-web/issues/1354)
|
||||
- Removing a user/group should delete its ACLs [\#899](https://github.com/vatesfr/xo-web/issues/899)
|
||||
- OVA Import - XO stuck during import [\#1551](https://github.com/vatesfr/xo-web/issues/1551)
|
||||
|
||||
## **5.2.1** (2016-09-13)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- On pool view: collapse network list [\#1461](https://github.com/vatesfr/xo-web/issues/1461)
|
||||
- Alert when trying to reboot/halt the pool master XS [\#1458](https://github.com/vatesfr/xo-web/issues/1458)
|
||||
- Adding tooltip on Home page [\#1456](https://github.com/vatesfr/xo-web/issues/1456)
|
||||
- Docker container management functionality missing from v5 [\#1442](https://github.com/vatesfr/xo-web/issues/1442)
|
||||
- bad error message - delete snapshot [\#1433](https://github.com/vatesfr/xo-web/issues/1433)
|
||||
- Create tag during VM creation [\#1431](https://github.com/vatesfr/xo-web/issues/1431)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- SMB remote empty domain fails [\#1499](https://github.com/vatesfr/xo-web/issues/1499)
|
||||
- Can't edit a remote password [\#1498](https://github.com/vatesfr/xo-web/issues/1498)
|
||||
- Issue in VM create with CoreOS [\#1493](https://github.com/vatesfr/xo-web/issues/1493)
|
||||
- Overlapping months in backup view [\#1488](https://github.com/vatesfr/xo-web/issues/1488)
|
||||
- No line break for SSH key in user view [\#1475](https://github.com/vatesfr/xo-web/issues/1475)
|
||||
- Create VIF UI issues [\#1472](https://github.com/vatesfr/xo-web/issues/1472)
|
||||
|
||||
## **5.2.0** (2016-09-09)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- IP management [\#1350](https://github.com/vatesfr/xo-web/issues/1350), [\#988](https://github.com/vatesfr/xo-web/issues/988), [\#1427](https://github.com/vatesfr/xo-web/issues/1427) and [\#240](https://github.com/vatesfr/xo-web/issues/240)
|
||||
- Update reverse proxy example [\#1474](https://github.com/vatesfr/xo-web/issues/1474)
|
||||
- Improve log view [\#1467](https://github.com/vatesfr/xo-web/issues/1467)
|
||||
- Backup Reports: e-mail subject [\#1463](https://github.com/vatesfr/xo-web/issues/1463)
|
||||
- Backup Reports: report the error [\#1462](https://github.com/vatesfr/xo-web/issues/1462)
|
||||
- Vif selector: select management network by default [\#1425](https://github.com/vatesfr/xo-web/issues/1425)
|
||||
- Display when browser disconnected to server [\#1417](https://github.com/vatesfr/xo-web/issues/1417)
|
||||
- Tooltip on OS icon in VM view [\#1416](https://github.com/vatesfr/xo-web/issues/1416)
|
||||
- Display pool master [\#1407](https://github.com/vatesfr/xo-web/issues/1407)
|
||||
- Missing tooltips in VM creation view [\#1402](https://github.com/vatesfr/xo-web/issues/1402)
|
||||
- Handle VDB disconnect and connect [\#1397](https://github.com/vatesfr/xo-web/issues/1397)
|
||||
- Eject host from a pool [\#1395](https://github.com/vatesfr/xo-web/issues/1395)
|
||||
- Improve pool general view [\#1393](https://github.com/vatesfr/xo-web/issues/1393)
|
||||
- Improve patching system [\#1392](https://github.com/vatesfr/xo-web/issues/1392)
|
||||
- Pool name modification [\#1390](https://github.com/vatesfr/xo-web/issues/1390)
|
||||
- Confirmation dialog before destroying VDIs [\#1388](https://github.com/vatesfr/xo-web/issues/1388)
|
||||
- Tooltips for meter object [\#1387](https://github.com/vatesfr/xo-web/issues/1387)
|
||||
- New Host assistant [\#1374](https://github.com/vatesfr/xo-web/issues/1374)
|
||||
- New VM assistant [\#1373](https://github.com/vatesfr/xo-web/issues/1373)
|
||||
- New SR assistant [\#1372](https://github.com/vatesfr/xo-web/issues/1372)
|
||||
- Direct access to VDI listing from dashboard's SR usage breakdown [\#1371](https://github.com/vatesfr/xo-web/issues/1371)
|
||||
- Can't set a network name at pool level [\#1368](https://github.com/vatesfr/xo-web/issues/1368)
|
||||
- Change a few mouse over descriptions [\#1363](https://github.com/vatesfr/xo-web/issues/1363)
|
||||
- Hide network install in VM create if template is HVM [\#1362](https://github.com/vatesfr/xo-web/issues/1362)
|
||||
- SR space left during VM creation [\#1358](https://github.com/vatesfr/xo-web/issues/1358)
|
||||
- Add destination SR on migration modal in VM view [\#1357](https://github.com/vatesfr/xo-web/issues/1357)
|
||||
- Ability to create a new VM from a snapshot [\#1353](https://github.com/vatesfr/xo-web/issues/1353)
|
||||
- Missing explanation/confirmation on Snapshot Page [\#1349](https://github.com/vatesfr/xo-web/issues/1349)
|
||||
- Log view: expose API errors in the web UI [\#1344](https://github.com/vatesfr/xo-web/issues/1344)
|
||||
- Registration on update page [\#1341](https://github.com/vatesfr/xo-web/issues/1341)
|
||||
- Add export snapshot button [\#1336](https://github.com/vatesfr/xo-web/issues/1336)
|
||||
- Use saved SSH keys in VM create CloudConfig [\#1319](https://github.com/vatesfr/xo-web/issues/1319)
|
||||
- Collapse header in console view [\#1268](https://github.com/vatesfr/xo-web/issues/1268)
|
||||
- Two max concurrent jobs in parallel [\#915](https://github.com/vatesfr/xo-web/issues/915)
|
||||
- Handle OVA import via the web UI [\#709](https://github.com/vatesfr/xo-web/issues/709)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Bug on VM console when header is hidden [\#1485](https://github.com/vatesfr/xo-web/issues/1485)
|
||||
- Disks not removed when deleting multiple VMs [\#1484](https://github.com/vatesfr/xo-web/issues/1484)
|
||||
- Do not display VDI disconnect button when a VM is not running [\#1470](https://github.com/vatesfr/xo-web/issues/1470)
|
||||
- Do not display VIF disconnect button when a VM is not running [\#1468](https://github.com/vatesfr/xo-web/issues/1468)
|
||||
- Error on migration if no default SR \(even when not used\) [\#1466](https://github.com/vatesfr/xo-web/issues/1466)
|
||||
- DR issue while rotating old backup [\#1464](https://github.com/vatesfr/xo-web/issues/1464)
|
||||
- Giving resource set to end-user ends with error [\#1448](https://github.com/vatesfr/xo-web/issues/1448)
|
||||
- Error thrown when cancelling out of Delete User confirmation dialog [\#1439](https://github.com/vatesfr/xo-web/issues/1439)
|
||||
- Wrong month label shown in Backup and Job scheduler [\#1438](https://github.com/vatesfr/xo-web/issues/1438)
|
||||
- Bug on Self service creation/edition [\#1428](https://github.com/vatesfr/xo-web/issues/1428)
|
||||
- ISO selection during VM create is not mounted after [\#1415](https://github.com/vatesfr/xo-web/issues/1415)
|
||||
- Hosts general view: bad link for storage [\#1408](https://github.com/vatesfr/xo-web/issues/1408)
|
||||
- Backup Schedule - "Month" and "Day of Week" display error [\#1404](https://github.com/vatesfr/xo-web/issues/1404)
|
||||
- Migrate dialog doesn't present all available VIF's in new UI interface [\#1403](https://github.com/vatesfr/xo-web/issues/1403)
|
||||
- NFS mount issues [\#1396](https://github.com/vatesfr/xo-web/issues/1396)
|
||||
- Select component color [\#1391](https://github.com/vatesfr/xo-web/issues/1391)
|
||||
- SR created with local path shouldn't be shared [\#1389](https://github.com/vatesfr/xo-web/issues/1389)
|
||||
- Disks (VBD) are attached to VM in RO mode instead of RW even if RO is unchecked [\#1386](https://github.com/vatesfr/xo-web/issues/1386)
|
||||
- Re-connection issues between server and XS hosts [\#1384](https://github.com/vatesfr/xo-web/issues/1384)
|
||||
- Meter object style with Chrome 52 [\#1383](https://github.com/vatesfr/xo-web/issues/1383)
|
||||
- Editing a rolling snapshot job seems to fail [\#1376](https://github.com/vatesfr/xo-web/issues/1376)
|
||||
- Dashboard SR usage and total inverted [\#1370](https://github.com/vatesfr/xo-web/issues/1370)
|
||||
- XenServer connection issue with host while using VGPUs [\#1369](https://github.com/vatesfr/xo-web/issues/1369)
|
||||
- Job created with v4 are not correctly displayed in v5 [\#1366](https://github.com/vatesfr/xo-web/issues/1366)
|
||||
- CPU accounting in resource set [\#1365](https://github.com/vatesfr/xo-web/issues/1365)
|
||||
- Tooltip stay displayed when a button change state [\#1360](https://github.com/vatesfr/xo-web/issues/1360)
|
||||
- Failure on host reboot [\#1351](https://github.com/vatesfr/xo-web/issues/1351)
|
||||
- Editing Backup Jobs Without Compression, Slider Always Set To On [\#1339](https://github.com/vatesfr/xo-web/issues/1339)
|
||||
- Month Selection on Backup Screen Wrong [\#1338](https://github.com/vatesfr/xo-web/issues/1338)
|
||||
- Delta backup fail when removed VDIs [\#1333](https://github.com/vatesfr/xo-web/issues/1333)
|
||||
|
||||
## **5.1.0** (2016-07-26)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Improve backups timezone UI [\#1314](https://github.com/vatesfr/xo-web/issues/1314)
|
||||
- HOME view submenus [\#1306](https://github.com/vatesfr/xo-web/issues/1306)
|
||||
- Ability for a user to save SSH keys [\#1299](https://github.com/vatesfr/xo-web/issues/1299)
|
||||
- \[ACLs\] Ability to select all hosts/VMs [\#1296](https://github.com/vatesfr/xo-web/issues/1296)
|
||||
- Improve scheduling UI [\#1295](https://github.com/vatesfr/xo-web/issues/1295)
|
||||
- Plugins: Predefined configurations [\#1289](https://github.com/vatesfr/xo-web/issues/1289)
|
||||
- Button to recompute resource sets limits [\#1287](https://github.com/vatesfr/xo-web/issues/1287)
|
||||
- Credit scheduler CAP and weight configuration [\#1283](https://github.com/vatesfr/xo-web/issues/1283)
|
||||
- Migration form problem on the /v5/vms/\_\_UUID\_\_ page when doing xenmotion inside a pool [\#1254](https://github.com/vatesfr/xo-web/issues/1254)
|
||||
- /v5/\#/pools/\_\_UUID\_\_: patch table improvement [\#1246](https://github.com/vatesfr/xo-web/issues/1246)
|
||||
- /v5/\#/hosts/\_\_UUID\_\_: patch list improvements ? [\#1245](https://github.com/vatesfr/xo-web/issues/1245)
|
||||
- F\*cking patches, how do they work? [\#1236](https://github.com/vatesfr/xo-web/issues/1236)
|
||||
- Change Default Filter [\#1235](https://github.com/vatesfr/xo-web/issues/1235)
|
||||
- Add a property on jobs to know their state [\#1232](https://github.com/vatesfr/xo-web/issues/1232)
|
||||
- Spanish translation [\#1231](https://github.com/vatesfr/xo-web/issues/1231)
|
||||
- Home: "Filter" input and keyboard focus [\#1228](https://github.com/vatesfr/xo-web/issues/1228)
|
||||
- Display xenserver version [\#1225](https://github.com/vatesfr/xo-web/issues/1225)
|
||||
- Plugin config: presets & defaults [\#1222](https://github.com/vatesfr/xo-web/issues/1222)
|
||||
- Allow halted VM migration [\#1216](https://github.com/vatesfr/xo-web/issues/1216)
|
||||
- Missing confirm dialog on critical button [\#1211](https://github.com/vatesfr/xo-web/issues/1211)
|
||||
- Backup logs are not sortable [\#1196](https://github.com/vatesfr/xo-web/issues/1196)
|
||||
- Page title with the name of current object [\#1185](https://github.com/vatesfr/xo-web/issues/1185)
|
||||
- Existing VIF management [\#1176](https://github.com/vatesfr/xo-web/issues/1176)
|
||||
- Do not display fast clone option is there isn't template disks [\#1172](https://github.com/vatesfr/xo-web/issues/1172)
|
||||
- UI issue when adding a user [\#1159](https://github.com/vatesfr/xo-web/issues/1159)
|
||||
- Combined values on stats [\#1158](https://github.com/vatesfr/xo-web/issues/1158)
|
||||
- Parallel coordinates graph [\#1157](https://github.com/vatesfr/xo-web/issues/1157)
|
||||
- VM creation on self-service as user [\#1155](https://github.com/vatesfr/xo-web/issues/1155)
|
||||
- VM copy bulk action on home view [\#1154](https://github.com/vatesfr/xo-web/issues/1154)
|
||||
- Better VDI map [\#1151](https://github.com/vatesfr/xo-web/issues/1151)
|
||||
- Missing tooltips on buttons [\#1150](https://github.com/vatesfr/xo-web/issues/1150)
|
||||
- Patching from pool view [\#1149](https://github.com/vatesfr/xo-web/issues/1149)
|
||||
- Missing patches in dashboard [\#1148](https://github.com/vatesfr/xo-web/issues/1148)
|
||||
- Improve tasks view [\#1147](https://github.com/vatesfr/xo-web/issues/1147)
|
||||
- Home bulk VM migration [\#1146](https://github.com/vatesfr/xo-web/issues/1146)
|
||||
- LDAP plugin clear password field [\#1145](https://github.com/vatesfr/xo-web/issues/1145)
|
||||
- Cron default behavior [\#1144](https://github.com/vatesfr/xo-web/issues/1144)
|
||||
- Modal for migrate on home [\#1143](https://github.com/vatesfr/xo-web/issues/1143)
|
||||
- /v5/\#/srs/\_\_UUID\_\_: UI improvements [\#1142](https://github.com/vatesfr/xo-web/issues/1142)
|
||||
- /v5/\#/pools/: some name should be links [\#1141](https://github.com/vatesfr/xo-web/issues/1141)
|
||||
- create the page /v5/\#/pools/ [\#1140](https://github.com/vatesfr/xo-web/issues/1140)
|
||||
- Dashboard: add links to different part of XOA [\#1139](https://github.com/vatesfr/xo-web/issues/1139)
|
||||
- /v5/\#/dashboard/overview: add link on the "Top 5 SR Usage" graph [\#1135](https://github.com/vatesfr/xo-web/issues/1135)
|
||||
- /v5/\#/backup/overview: display the error when there is one returned by xenserver on failed job. [\#1134](https://github.com/vatesfr/xo-web/issues/1134)
|
||||
- /v5/: add an option to set the number of element displayed in tables [\#1133](https://github.com/vatesfr/xo-web/issues/1133)
|
||||
- Updater refresh page after update [\#1131](https://github.com/vatesfr/xo-web/issues/1131)
|
||||
- /v5/\#/settings/plugins [\#1130](https://github.com/vatesfr/xo-web/issues/1130)
|
||||
- /v5/\#/new/sr: layout issue [\#1129](https://github.com/vatesfr/xo-web/issues/1129)
|
||||
- v5 /v5/\#/vms/new: layout issue [\#1128](https://github.com/vatesfr/xo-web/issues/1128)
|
||||
- v5 user page missing style [\#1127](https://github.com/vatesfr/xo-web/issues/1127)
|
||||
- Remote helper/tester [\#1075](https://github.com/vatesfr/xo-web/issues/1075)
|
||||
- Generate uiSchema from custom schema properties [\#951](https://github.com/vatesfr/xo-web/issues/951)
|
||||
- Customizing VM names generation during batch creation [\#949](https://github.com/vatesfr/xo-web/issues/949)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Plugins: Don't use `default` attributes in presets list [\#1288](https://github.com/vatesfr/xo-web/issues/1288)
|
||||
- CPU weight must be an integer [\#1286](https://github.com/vatesfr/xo-web/issues/1286)
|
||||
- Overview of self service is always empty [\#1282](https://github.com/vatesfr/xo-web/issues/1282)
|
||||
- SR attach/creation issue [\#1281](https://github.com/vatesfr/xo-web/issues/1281)
|
||||
- Self service resources not modified after a VM deletion [\#1276](https://github.com/vatesfr/xo-web/issues/1276)
|
||||
- Scheduled jobs seems use GMT since 5.0 [\#1258](https://github.com/vatesfr/xo-web/issues/1258)
|
||||
- Can't create a VM with disks on 2 different SRs [\#1257](https://github.com/vatesfr/xo-web/issues/1257)
|
||||
- Graph display bug [\#1247](https://github.com/vatesfr/xo-web/issues/1247)
|
||||
- /v5/#/hosts/__UUID__: Patch list not limited to the current pool [\#1244](https://github.com/vatesfr/xo-web/issues/1244)
|
||||
- Replication issues [\#1233](https://github.com/vatesfr/xo-web/issues/1233)
|
||||
- VM creation install method disabled fields [\#1198](https://github.com/vatesfr/xo-web/issues/1198)
|
||||
- Update icon shouldn't be displayed when menu is collapsed [\#1188](https://github.com/vatesfr/xo-web/issues/1188)
|
||||
- /v5/ : Load average graph axis issue [\#1167](https://github.com/vatesfr/xo-web/issues/1167)
|
||||
- Some remote can't be opened [\#1164](https://github.com/vatesfr/xo-web/issues/1164)
|
||||
- Bulk action for hosts in home and pool view [\#1153](https://github.com/vatesfr/xo-web/issues/1153)
|
||||
- New Vif [\#1138](https://github.com/vatesfr/xo-web/issues/1138)
|
||||
- Missing SRs [\#1123](https://github.com/vatesfr/xo-web/issues/1123)
|
||||
- Continuous replication email alert does not obey per job setting [\#1121](https://github.com/vatesfr/xo-web/issues/1121)
|
||||
- Safari XO5 issue [\#1120](https://github.com/vatesfr/xo-web/issues/1120)
|
||||
- ACLs shoud be available in Enterprise Edition [\#1118](https://github.com/vatesfr/xo-web/issues/1118)
|
||||
- SR edit name or description doesn't work [\#1116](https://github.com/vatesfr/xo-web/issues/1116)
|
||||
- Bad RRD parsing for VIFs [\#969](https://github.com/vatesfr/xo-web/issues/969)
|
||||
|
||||
## **5.0.0** (2016-06-24)
|
||||
|
||||
### Enhancements
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
36
gulpfile.js
36
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
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
52
package.json
52
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.0.5",
|
||||
"version": "5.2.5",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -33,11 +33,13 @@
|
||||
"devDependencies": {
|
||||
"ansi_up": "^1.3.0",
|
||||
"asap": "^2.0.4",
|
||||
"ava": "^0.15.0",
|
||||
"babel-eslint": "^6.0.0",
|
||||
"ava": "^0.16.0",
|
||||
"babel-eslint": "^7.0.0",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-plugin-transform-react-constant-elements": "^6.5.0",
|
||||
"babel-plugin-transform-react-inline-elements": "^6.6.5",
|
||||
"babel-plugin-transform-react-jsx-self": "^6.11.0",
|
||||
"babel-plugin-transform-react-jsx-source": "^6.9.0",
|
||||
"babel-plugin-transform-runtime": "^6.6.0",
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
@@ -48,17 +50,18 @@
|
||||
"bootstrap": "github:twbs/bootstrap#v4-dev",
|
||||
"browserify": "^13.0.0",
|
||||
"bundle-collapser": "^1.2.1",
|
||||
"chartist-plugin-legend": "^0.3.1",
|
||||
"chartist-plugin-legend": "^0.5.0",
|
||||
"chartist-plugin-tooltip": "0.0.11",
|
||||
"classnames": "^2.2.3",
|
||||
"connect": "^3.4.1",
|
||||
"cookies-js": "^1.2.2",
|
||||
"d3": "^4.0.0-alpha.50",
|
||||
"dependency-check": "^2.5.1",
|
||||
"event-to-promise": "^0.7.0",
|
||||
"font-awesome": "^4.5.0",
|
||||
"font-mfizz": "github:fizzed/font-mfizz",
|
||||
"get-stream": "^2.3.0",
|
||||
"ghooks": "^1.1.1",
|
||||
"globby": "^5.0.0",
|
||||
"globby": "^6.0.0",
|
||||
"gulp": "github:gulpjs/gulp#4.0",
|
||||
"gulp-autoprefixer": "^3.1.0",
|
||||
"gulp-csso": "^2.0.0",
|
||||
@@ -67,19 +70,23 @@
|
||||
"gulp-plumber": "^1.1.0",
|
||||
"gulp-refresh": "^1.1.0",
|
||||
"gulp-sass": "^2.2.0",
|
||||
"gulp-uglify": "^1.5.3",
|
||||
"gulp-uglify": "^2.0.0",
|
||||
"gulp-watch": "^4.3.5",
|
||||
"human-format": "^0.6.0",
|
||||
"index-modules": "0.0.0",
|
||||
"is-ip": "^1.0.0",
|
||||
"jsonrpc-websocket-client": "0.0.1-5",
|
||||
"later": "^1.2.0",
|
||||
"lodash": "^4.6.1",
|
||||
"loose-envify": "^1.1.0",
|
||||
"make-error": "^1.2.1",
|
||||
"marked": "^0.3.5",
|
||||
"modular-css": "^0.22.1",
|
||||
"modular-css": "^0.27.1",
|
||||
"moment": "^2.13.0",
|
||||
"moment-timezone": "^0.5.4",
|
||||
"notifyjs": "^2.0.1",
|
||||
"novnc-node": "^0.5.3",
|
||||
"promise-toolbox": "^0.4.0",
|
||||
"promise-toolbox": "^0.5.0",
|
||||
"random-password": "^0.1.2",
|
||||
"react": "^15.0.0",
|
||||
"react-addons-shallow-compare": "^15.1.0",
|
||||
@@ -89,16 +96,18 @@
|
||||
"react-debounce-input": "^2.4.0",
|
||||
"react-dnd": "^2.1.4",
|
||||
"react-dnd-html5-backend": "^2.1.2",
|
||||
"react-document-title": "^2.0.2",
|
||||
"react-dom": "^15.0.0",
|
||||
"react-dropzone": "^3.5.0",
|
||||
"react-intl": "^2.0.1",
|
||||
"react-key-handler": "^0.2.0",
|
||||
"react-key-handler": "^0.3.0",
|
||||
"react-notify": "^2.0.1",
|
||||
"react-redux": "^4.4.0",
|
||||
"react-router": "^3.0.0-alpha.1",
|
||||
"react-select": "^1.0.0-beta13",
|
||||
"react-shortcuts": "^1.0.7",
|
||||
"react-sparklines": "^1.5.0",
|
||||
"react-virtualized": "^7.4.0",
|
||||
"react-virtualized": "^8.0.8",
|
||||
"readable-stream": "^2.0.6",
|
||||
"redux": "^3.3.1",
|
||||
"redux-devtools": "^3.1.1",
|
||||
@@ -106,20 +115,21 @@
|
||||
"redux-devtools-log-monitor": "^1.0.5",
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.2.1",
|
||||
"serve-static": "^1.10.2",
|
||||
"standard": "^7.0.0",
|
||||
"standard": "^8.2.0",
|
||||
"superagent": "^2.0.0",
|
||||
"vinyl": "^1.1.1",
|
||||
"tar-stream": "^1.5.2",
|
||||
"vinyl": "^2.0.0",
|
||||
"watchify": "^3.7.0",
|
||||
"xo-acl-resolver": "^0.2.1",
|
||||
"xo-lib": "^0.8.0-1",
|
||||
"xml2js": "^0.4.17",
|
||||
"xo-acl-resolver": "^0.2.2",
|
||||
"xo-lib": "^0.8.0",
|
||||
"xo-remote-parser": "^0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"benchmarks": "./tools/run-benchmarks.js 'src/**/*.bench.js'",
|
||||
"build": "npm run build-indexes && NODE_ENV=production gulp build",
|
||||
"build-indexes": "./tools/generate-index src/common/intl/locales",
|
||||
"dev": "npm run build-indexes && gulp build server",
|
||||
"build-indexes": "index-modules --auto src",
|
||||
"dev": "npm run build-indexes && NODE_ENV=development gulp build",
|
||||
"dev-test": "ava --watch",
|
||||
"lint": "standard",
|
||||
"posttest": "npm run lint",
|
||||
@@ -143,6 +153,12 @@
|
||||
},
|
||||
"babel": {
|
||||
"env": {
|
||||
"development": {
|
||||
"plugins": [
|
||||
"transform-react-jsx-self",
|
||||
"transform-react-jsx-source"
|
||||
]
|
||||
},
|
||||
"production": {
|
||||
"plugins": [
|
||||
"transform-react-constant-elements",
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
$ct-series-colors: (
|
||||
$brand-success,
|
||||
$brand-primary,
|
||||
#60bd68,
|
||||
#f17cb0,
|
||||
#b2912f,
|
||||
#b276b2,
|
||||
#decf3f,
|
||||
#f15854,
|
||||
#4d4d4d,
|
||||
#dda458,
|
||||
#eacf7d,
|
||||
#86797d,
|
||||
#b276b2,
|
||||
#f15854,
|
||||
#b2912f,
|
||||
#decf3f,
|
||||
#dda458,
|
||||
#60bd68,
|
||||
#4d4d4d,
|
||||
#eacf7d,
|
||||
#b2c326,
|
||||
#6188e2,
|
||||
#a748ca
|
||||
@@ -22,6 +22,11 @@ $ct-series-colors: (
|
||||
@import "../node_modules/chartist/dist/scss/settings/_chartist-settings";
|
||||
@import "../node_modules/chartist/dist/scss/chartist";
|
||||
|
||||
.ct-chart {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
// Line in charts with only 2px in width
|
||||
.ct-line {
|
||||
stroke-width: 2px;
|
||||
@@ -55,7 +60,6 @@ $ct-series-colors: (
|
||||
|
||||
// Arrow!
|
||||
&:before {
|
||||
position: absolute;
|
||||
bottom: -14px;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
@@ -80,28 +84,27 @@ $ct-series-colors: (
|
||||
// CHARTIST LEGEND =============================================================
|
||||
|
||||
.ct-legend {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
margin-bottom: -1em;
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
padding-left: 1.4em;
|
||||
padding-left: 0.5em;
|
||||
list-style-type: none;
|
||||
display: inline;
|
||||
display: inline-block;
|
||||
margin-right: 0.5em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
li:before {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
content: '';
|
||||
border: 3px solid transparent;
|
||||
border-radius: 2px;
|
||||
margin-top: 0.5em;
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
|
||||
li.inactive:before {
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
// import _ from 'intl' TODO: fix tooltip
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
// import Tooltip from 'tooltip' TODO: fix tooltip
|
||||
import Tooltip from 'tooltip'
|
||||
import {
|
||||
ButtonGroup
|
||||
} from 'react-bootstrap-4/lib'
|
||||
import {
|
||||
noop
|
||||
} from 'utils'
|
||||
|
||||
const ActionBar = ({ actions, param }) => (
|
||||
<ButtonGroup>
|
||||
{map(actions, ({ handler, handlerParam = param, label, icon }, index) => (
|
||||
/* <Tooltip key={index} content={_(label)}> TODO: fix tooltip */
|
||||
{map(actions, (button, index) => {
|
||||
if (!button) {
|
||||
return
|
||||
}
|
||||
|
||||
const { handler, handlerParam = param, label, icon, redirectOnSuccess } = button
|
||||
return <Tooltip key={index} content={_(label)}>
|
||||
<ActionButton
|
||||
key={index}
|
||||
btnStyle='secondary'
|
||||
handler={handler}
|
||||
handler={handler || noop}
|
||||
handlerParam={handlerParam}
|
||||
icon={icon}
|
||||
redirectOnSuccess={redirectOnSuccess}
|
||||
size='large'
|
||||
/>
|
||||
/* </Tooltip> */
|
||||
))}
|
||||
</Tooltip>
|
||||
})}
|
||||
</ButtonGroup>
|
||||
)
|
||||
ActionBar.propTypes = {
|
||||
@@ -28,7 +37,8 @@ ActionBar.propTypes = {
|
||||
React.PropTypes.shape({
|
||||
label: React.PropTypes.string.isRequired,
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
handler: React.PropTypes.func
|
||||
handler: React.PropTypes.func,
|
||||
redirectOnSuccess: React.PropTypes.string
|
||||
})
|
||||
).isRequired,
|
||||
display: React.PropTypes.oneOf(['icon', 'text', 'both'])
|
||||
|
||||
@@ -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,36 @@
|
||||
import clone from 'lodash/clone'
|
||||
import includes from 'lodash/includes'
|
||||
import isArray from 'lodash/isArray'
|
||||
import forEach from 'lodash/forEach'
|
||||
import map from 'lodash/map'
|
||||
import { Component } from 'react'
|
||||
|
||||
import getEventValue from './get-event-value'
|
||||
import invoke from './invoke'
|
||||
import shallowEqual from './shallow-equal'
|
||||
|
||||
const cowSet = (object, path, value, depth) => {
|
||||
if (depth >= path.length) {
|
||||
return value
|
||||
}
|
||||
|
||||
object = clone(object)
|
||||
const prop = path[depth]
|
||||
object[prop] = cowSet(object[prop], path, value, depth + 1)
|
||||
return object
|
||||
}
|
||||
|
||||
const get = (object, path, depth) => {
|
||||
if (depth >= path.length) {
|
||||
return object
|
||||
}
|
||||
|
||||
const prop = path[depth++]
|
||||
return isArray(object) && prop === '*'
|
||||
? map(object, value => get(value, path, depth))
|
||||
: get(object[prop], path, depth)
|
||||
}
|
||||
|
||||
export default class BaseComponent extends Component {
|
||||
constructor (props, context) {
|
||||
super(props, context)
|
||||
@@ -24,7 +50,42 @@ export default class BaseComponent extends Component {
|
||||
}
|
||||
|
||||
// See https://preactjs.com/guide/linked-state
|
||||
linkState (name) {
|
||||
linkState (name, targetPath) {
|
||||
const key = targetPath
|
||||
? `${name}##${targetPath}`
|
||||
: name
|
||||
|
||||
let linkedState = this._linkedState
|
||||
let cb
|
||||
if (!linkedState) {
|
||||
linkedState = this._linkedState = {}
|
||||
} else if ((cb = linkedState[key])) {
|
||||
return cb
|
||||
}
|
||||
|
||||
let getValue
|
||||
if (targetPath) {
|
||||
const path = targetPath.split('.')
|
||||
getValue = event => get(getEventValue(event), path, 0)
|
||||
} else {
|
||||
getValue = getEventValue
|
||||
}
|
||||
|
||||
if (includes(name, '.')) {
|
||||
const path = name.split('.')
|
||||
return (linkedState[key] = event => {
|
||||
this.setState(cowSet(this.state, path, getValue(event), 0))
|
||||
})
|
||||
}
|
||||
|
||||
return (linkedState[key] = event => {
|
||||
this.setState({
|
||||
[name]: getValue(event)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
toggleState (name) {
|
||||
let linkedState = this._linkedState
|
||||
let cb
|
||||
if (!linkedState) {
|
||||
@@ -33,9 +94,16 @@ export default class BaseComponent extends Component {
|
||||
return cb
|
||||
}
|
||||
|
||||
return (linkedState[name] = event => {
|
||||
if (includes(name, '.')) {
|
||||
const path = name.split('.')
|
||||
return (linkedState[path] = event => {
|
||||
this.setState(cowSet(this.state, path, !get(this.state, path, 0), 0))
|
||||
})
|
||||
}
|
||||
|
||||
return (linkedState[name] = () => {
|
||||
this.setState({
|
||||
[name]: getEventValue(event)
|
||||
[name]: !this.state[name]
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
3
src/common/combobox/index.css
Normal file
3
src/common/combobox/index.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.button {
|
||||
border-radius: 0px;
|
||||
};
|
||||
101
src/common/combobox/index.js
Normal file
101
src/common/combobox/index.js
Normal file
@@ -0,0 +1,101 @@
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import size from 'lodash/size'
|
||||
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
import { ensureArray } from '../utils'
|
||||
import {
|
||||
DropdownButton,
|
||||
MenuItem
|
||||
} from 'react-bootstrap-4/lib'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@propTypes({
|
||||
defaultValue: propTypes.any,
|
||||
disabled: propTypes.bool,
|
||||
options: propTypes.oneOfType([
|
||||
propTypes.arrayOf(propTypes.string),
|
||||
propTypes.number,
|
||||
propTypes.objectOf(propTypes.string),
|
||||
propTypes.string
|
||||
]),
|
||||
onChange: propTypes.func,
|
||||
placeholder: propTypes.string,
|
||||
required: propTypes.bool,
|
||||
step: propTypes.any,
|
||||
type: propTypes.string,
|
||||
value: propTypes.any
|
||||
})
|
||||
export default class Combobox extends Component {
|
||||
static defaultProps = {
|
||||
type: 'text'
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
}
|
||||
|
||||
_handleChange = event => {
|
||||
const { onChange } = this.props
|
||||
|
||||
if (onChange) {
|
||||
onChange(event.target.value)
|
||||
}
|
||||
}
|
||||
|
||||
_setText (value) {
|
||||
this.refs.input.value = value
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const options = ensureArray(props.options)
|
||||
|
||||
const Input = (
|
||||
<input
|
||||
className='form-control'
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
options={options}
|
||||
onChange={this._handleChange}
|
||||
placeholder={props.placeholder}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
step={props.step}
|
||||
type={props.type}
|
||||
value={props.value}
|
||||
/>
|
||||
)
|
||||
|
||||
if (!size(options)) {
|
||||
return Input
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='input-group'>
|
||||
<div className='input-group-btn'>
|
||||
<DropdownButton
|
||||
bsStyle='secondary'
|
||||
className={styles.button}
|
||||
disabled={props.disabled}
|
||||
id='selectInput'
|
||||
title=''
|
||||
>
|
||||
{map(options, option => (
|
||||
<MenuItem key={option} onClick={() => this._setText(option)}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</DropdownButton>
|
||||
</div>
|
||||
{Input}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
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,16 +6,17 @@ import map from 'lodash/map'
|
||||
import pick from 'lodash/pick'
|
||||
import React from 'react'
|
||||
|
||||
import _ from './intl'
|
||||
import Component from './base-component'
|
||||
import Icon from './icon'
|
||||
import logError from './log-error'
|
||||
import propTypes from './prop-types'
|
||||
import Tooltip from './tooltip'
|
||||
import { formatSize } from './utils'
|
||||
import { SizeInput } from './form'
|
||||
import _ from '../intl'
|
||||
import Component from '../base-component'
|
||||
import Icon from '../icon'
|
||||
import logError from '../log-error'
|
||||
import propTypes from '../prop-types'
|
||||
import Tooltip from '../tooltip'
|
||||
import { formatSize } from '../utils'
|
||||
import { SizeInput } from '../form'
|
||||
import {
|
||||
SelectHost,
|
||||
SelectIp,
|
||||
SelectNetwork,
|
||||
SelectPool,
|
||||
SelectRemote,
|
||||
@@ -23,12 +25,11 @@ import {
|
||||
SelectTag,
|
||||
SelectVm,
|
||||
SelectVmTemplate
|
||||
} from './select-objects'
|
||||
} from '../select-objects'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
const LONG_CLICK = 400
|
||||
const SELECT_STYLE = { padding: '0px' }
|
||||
const SIZE_STYLE = { width: '10rem' }
|
||||
const EDITABLE_STYLE = { borderBottom: '1px dashed #ccc' }
|
||||
|
||||
@propTypes({
|
||||
alt: propTypes.node.isRequired
|
||||
@@ -157,7 +158,7 @@ class Editable extends Component {
|
||||
const { useLongClick } = props
|
||||
|
||||
const success = <Icon icon='success' />
|
||||
return <span style={useLongClick ? null : EDITABLE_STYLE}>
|
||||
return <span className={classNames(styles.clickToEdit, !useLongClick && styles.shortClick)}>
|
||||
<span
|
||||
onClick={!useLongClick && this._openEdition}
|
||||
onMouseDown={useLongClick && this.__startTimer}
|
||||
@@ -257,7 +258,8 @@ export class Text extends Editable {
|
||||
readOnly={saving}
|
||||
ref='input'
|
||||
style={{
|
||||
width: `${value.length + 1}ex`
|
||||
width: `${value.length + 1}ex`,
|
||||
maxWidth: '50ex'
|
||||
}}
|
||||
type={this._isPassword ? 'password' : 'text'}
|
||||
/>
|
||||
@@ -271,20 +273,34 @@ export class Password extends Text {
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
value: propTypes.number.isRequired
|
||||
nullable: propTypes.bool,
|
||||
value: propTypes.number
|
||||
})
|
||||
export class Number extends Component {
|
||||
get value () {
|
||||
return +this.refs.input.value
|
||||
}
|
||||
|
||||
_onChange = value => this.props.onChange(+value)
|
||||
_onChange = value => {
|
||||
if (value === '') {
|
||||
if (this.props.nullable) {
|
||||
value = null
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
value = +value
|
||||
}
|
||||
|
||||
this.props.onChange(value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { value } = this.props
|
||||
return <Text
|
||||
{...this.props}
|
||||
onChange={this._onChange}
|
||||
value={String(this.props.value)}
|
||||
value={value === null ? '' : String(value)}
|
||||
/>
|
||||
}
|
||||
}
|
||||
@@ -337,14 +353,13 @@ export class Select extends Editable {
|
||||
|
||||
return <select
|
||||
autoFocus
|
||||
className='form-control'
|
||||
className={classNames('form-control', styles.select)}
|
||||
defaultValue={this._defaultValue}
|
||||
onBlur={this._closeEdition}
|
||||
onChange={this._onChange}
|
||||
onKeyDown={this._onKeyDown}
|
||||
readOnly={saving}
|
||||
ref={this._onEditionMount}
|
||||
style={SELECT_STYLE}
|
||||
>
|
||||
{map(options, this._optionToJsx)}
|
||||
</select>
|
||||
@@ -353,6 +368,7 @@ export class Select extends Editable {
|
||||
|
||||
const MAP_TYPE_SELECT = {
|
||||
host: SelectHost,
|
||||
ip: SelectIp,
|
||||
network: SelectNetwork,
|
||||
pool: SelectPool,
|
||||
remote: SelectRemote,
|
||||
@@ -441,15 +457,18 @@ export class Size extends Editable {
|
||||
const { value } = this.props
|
||||
|
||||
return <span
|
||||
// SizeInput uses `input-group` which makes it behave as a block element (display: table).
|
||||
// `form-inline` to use it as an inline element
|
||||
className='form-inline'
|
||||
onBlur={this._closeEditionIfUnfocused}
|
||||
onFocus={this._focus}
|
||||
onKeyDown={this._onKeyDown}
|
||||
>
|
||||
<SizeInput
|
||||
autoFocus
|
||||
className={styles.size}
|
||||
ref='input'
|
||||
readOnly={saving}
|
||||
style={SIZE_STYLE}
|
||||
defaultValue={value}
|
||||
/>
|
||||
</span>
|
||||
@@ -1,8 +1,10 @@
|
||||
import BaseComponent from 'base-component'
|
||||
import classNames from 'classnames'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import randomPassword from 'random-password'
|
||||
import React from 'react'
|
||||
import round from 'lodash/round'
|
||||
import {
|
||||
DropdownButton,
|
||||
MenuItem
|
||||
@@ -11,6 +13,7 @@ import {
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
import {
|
||||
firstDefined,
|
||||
formatSizeRaw,
|
||||
parseSize
|
||||
} from '../utils'
|
||||
@@ -96,12 +99,9 @@ export class Range extends Component {
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
const { onChange } = this.props
|
||||
this.state.value = +value
|
||||
|
||||
if (onChange) {
|
||||
onChange(value)
|
||||
}
|
||||
this.setState({
|
||||
value: +value
|
||||
})
|
||||
}
|
||||
|
||||
_handleChange = event => {
|
||||
@@ -158,72 +158,156 @@ const DEFAULT_UNIT = 'GiB'
|
||||
placeholder: propTypes.string,
|
||||
readOnly: propTypes.bool,
|
||||
required: propTypes.bool,
|
||||
style: propTypes.object
|
||||
style: propTypes.object,
|
||||
value: propTypes.oneOfType([
|
||||
propTypes.number,
|
||||
propTypes.oneOf([ null ])
|
||||
])
|
||||
})
|
||||
export class SizeInput extends Component {
|
||||
export class SizeInput extends BaseComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
const humanSize = props.defaultValue && formatSizeRaw(props.defaultValue)
|
||||
this._defaultValue = humanSize && humanSize.value
|
||||
this.state = { unit: humanSize ? humanSize.prefix + 'B' : props.defaultUnit || DEFAULT_UNIT }
|
||||
this.state = this._createStateFromBytes(firstDefined(props.value, props.defaultValue, null))
|
||||
}
|
||||
|
||||
componentWillReceiveProps (newProps) {
|
||||
this.value = newProps.defaultValue
|
||||
componentWillReceiveProps (props) {
|
||||
const { value } = props
|
||||
if (value !== undefined && value !== this.props.value) {
|
||||
this.setState(this._createStateFromBytes(value))
|
||||
}
|
||||
}
|
||||
|
||||
_createStateFromBytes (bytes) {
|
||||
if (bytes === this._bytes) {
|
||||
return {
|
||||
input: this._input,
|
||||
unit: this._unit
|
||||
}
|
||||
}
|
||||
|
||||
if (bytes === null) {
|
||||
return {
|
||||
input: '',
|
||||
unit: this.props.defaultUnit || DEFAULT_UNIT
|
||||
}
|
||||
}
|
||||
|
||||
const { prefix, value } = formatSizeRaw(bytes)
|
||||
return {
|
||||
input: String(round(value, 2)),
|
||||
unit: `${prefix}B`
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
const value = this.refs.value.value
|
||||
return value ? parseSize(value + ' ' + this.state.unit) : undefined
|
||||
const { input, unit } = this.state
|
||||
|
||||
if (!input) {
|
||||
return null
|
||||
}
|
||||
|
||||
return parseSize(`${+input} ${unit}`)
|
||||
}
|
||||
|
||||
set value (newValue) {
|
||||
const humanSize = newValue && formatSizeRaw(newValue)
|
||||
this.refs.value.value = humanSize ? humanSize.value : ''
|
||||
this.setState({ unit: humanSize ? humanSize.prefix + 'B' : DEFAULT_UNIT })
|
||||
set value (value) {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
this.props.value !== undefined
|
||||
) {
|
||||
throw new Error('cannot set value of controlled SizeInput')
|
||||
}
|
||||
this.setState(this._createStateFromBytes(value))
|
||||
}
|
||||
|
||||
_onChange = () =>
|
||||
this.props.onChange && this.props.onChange(this.value)
|
||||
_onChange (input, unit) {
|
||||
const { onChange } = this.props
|
||||
|
||||
// Empty input equals null.
|
||||
const bytes = input
|
||||
? parseSize(`${+input} ${unit}`)
|
||||
: null
|
||||
|
||||
const isControlled = this.props.value !== undefined
|
||||
if (isControlled) {
|
||||
// Store input and unit for this change to update correctly on new
|
||||
// props.
|
||||
this._bytes = bytes
|
||||
this._input = input
|
||||
this._unit = unit
|
||||
} else {
|
||||
this.setState({ input, unit })
|
||||
|
||||
// onChange is optional in uncontrolled mode.
|
||||
if (!onChange) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onChange(bytes)
|
||||
}
|
||||
|
||||
_updateNumber = event => {
|
||||
const input = event.target.value
|
||||
|
||||
if (!input) {
|
||||
return this._onChange(input, this.state.unit)
|
||||
}
|
||||
|
||||
const number = +input
|
||||
|
||||
// NaN: do not ack this change.
|
||||
if (number !== number) { // eslint-disable-line no-self-compare
|
||||
return
|
||||
}
|
||||
|
||||
// Same numeric value: simply update the input.
|
||||
const prevInput = this.state.input
|
||||
if (prevInput && +prevInput === number) {
|
||||
return this.setState({ input })
|
||||
}
|
||||
|
||||
this._onChange(input, this.state.unit)
|
||||
}
|
||||
|
||||
_updateUnit = unit => {
|
||||
this.setState({ unit })
|
||||
this._onChange()
|
||||
const { input } = this.state
|
||||
|
||||
// 0 is always 0, no matter the unit.
|
||||
if (+input) {
|
||||
this._onChange(input, unit)
|
||||
} else {
|
||||
this.setState({ unit })
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
autoFocus,
|
||||
className,
|
||||
placeholder,
|
||||
readOnly,
|
||||
placeholder,
|
||||
required,
|
||||
style
|
||||
} = this.props
|
||||
|
||||
return <span
|
||||
className={classNames(className, 'input-group')}
|
||||
style={style}
|
||||
>
|
||||
return <span className={classNames('input-group', className)} style={style}>
|
||||
<input
|
||||
autoFocus={autoFocus}
|
||||
className='form-control'
|
||||
defaultValue={this._defaultValue}
|
||||
min={0}
|
||||
onChange={this._onChange}
|
||||
disabled={readOnly}
|
||||
onChange={this._updateNumber}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
required={required}
|
||||
ref='value'
|
||||
type='number'
|
||||
type='text'
|
||||
value={this.state.input}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<DropdownButton
|
||||
bsStyle='secondary'
|
||||
disabled={readOnly}
|
||||
id='size'
|
||||
pullRight
|
||||
disabled={readOnly}
|
||||
title={this.state.unit}
|
||||
>
|
||||
{map(UNITS, unit =>
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { Component } from 'react'
|
||||
import ReactSelect from 'react-select'
|
||||
import {
|
||||
AutoSizer,
|
||||
VirtualScroll
|
||||
List
|
||||
} from 'react-virtualized'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
@@ -40,19 +40,21 @@ export default class Select extends Component {
|
||||
const focusedOptionIndex = options.indexOf(focusedOption)
|
||||
const height = Math.min(maxHeight, options.length * optionHeight)
|
||||
|
||||
const wrappedRowRenderer = ({ index }) =>
|
||||
const wrappedRowRenderer = ({ index, key, style }) =>
|
||||
this._optionRenderer({
|
||||
...otherOptions,
|
||||
focusedOption,
|
||||
focusedOptionIndex,
|
||||
key,
|
||||
option: options[index],
|
||||
options
|
||||
options,
|
||||
style
|
||||
})
|
||||
|
||||
return (
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
<VirtualScroll
|
||||
<List
|
||||
height={height}
|
||||
rowCount={options.length}
|
||||
rowHeight={optionHeight}
|
||||
@@ -68,8 +70,10 @@ export default class Select extends Component {
|
||||
_optionRenderer = ({
|
||||
focusedOption,
|
||||
focusOption,
|
||||
key,
|
||||
labelKey,
|
||||
option,
|
||||
style,
|
||||
selectValue
|
||||
}) => {
|
||||
let className = 'Select-option'
|
||||
@@ -91,7 +95,8 @@ export default class Select extends Component {
|
||||
className={className}
|
||||
onClick={!disabled && (() => selectValue(option))}
|
||||
onMouseOver={!disabled && (() => focusOption(option))}
|
||||
style={{ height: props.optionHeight }}
|
||||
style={{ ...style, height: props.optionHeight }}
|
||||
key={key}
|
||||
>
|
||||
{props.optionRenderer(option, labelKey)}
|
||||
</div>
|
||||
@@ -102,6 +107,7 @@ export default class Select extends Component {
|
||||
return (
|
||||
<ReactSelect
|
||||
{...this.props}
|
||||
backspaceToRemoveMessage=''
|
||||
menuRenderer={this._renderMenu}
|
||||
menuStyle={SELECT_MENU_STYLE}
|
||||
style={SELECT_STYLE}
|
||||
|
||||
@@ -49,6 +49,7 @@ export default class Toggle extends Component {
|
||||
}
|
||||
|
||||
this.refs.input.checked = Boolean(value)
|
||||
this.forceUpdate()
|
||||
}
|
||||
|
||||
_onChange = event => {
|
||||
|
||||
20
src/common/home-filters.js
Normal file
20
src/common/home-filters.js
Normal file
@@ -0,0 +1,20 @@
|
||||
export const VM = {
|
||||
homeFilterPendingVms: 'current_operations:"" ',
|
||||
homeFilterNonRunningVms: '!power_state:running ',
|
||||
homeFilterHvmGuests: 'virtualizationMode:hvm ',
|
||||
homeFilterRunningVms: 'power_state:running ',
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const host = {
|
||||
homeFilterRunningHosts: 'power_state:running ',
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const pool = {
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const vmTemplate = {
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
226
src/common/hosts-patches-table.js
Normal file
226
src/common/hosts-patches-table.js
Normal file
@@ -0,0 +1,226 @@
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import { Portal } from 'react-overlays'
|
||||
|
||||
import _ from './intl'
|
||||
import ActionButton from './action-button'
|
||||
import Component from './base-component'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Link from './link'
|
||||
import propTypes from './prop-types'
|
||||
import SortedTable from './sorted-table'
|
||||
import TabButton from './tab-button'
|
||||
import { connectStore } from './utils'
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
createFilter,
|
||||
createSelector
|
||||
} from './selectors'
|
||||
import {
|
||||
getHostMissingPatches,
|
||||
installAllHostPatches,
|
||||
installAllPatchesOnPool
|
||||
} from './xo'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const MISSING_PATCHES_COLUMNS = [
|
||||
{
|
||||
name: _('srHost'),
|
||||
itemRenderer: host => <Link to={`/hosts/${host.id}`}>{host.name_label}</Link>,
|
||||
sortCriteria: host => host.name_label
|
||||
},
|
||||
{
|
||||
name: _('hostDescription'),
|
||||
itemRenderer: host => host.name_description,
|
||||
sortCriteria: host => host.name_description
|
||||
},
|
||||
{
|
||||
name: _('hostMissingPatches'),
|
||||
itemRenderer: (host, { missingPatches }) => <Link to={`/hosts/${host.id}/patches`}>{missingPatches[host.id]}</Link>,
|
||||
sortCriteria: (host, { missingPatches }) => missingPatches[host.id]
|
||||
},
|
||||
{
|
||||
name: _('patchUpdateButton'),
|
||||
itemRenderer: (host, { installAllHostPatches }) => (
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
handler={installAllHostPatches}
|
||||
handlerParam={host}
|
||||
icon='host-patch-update'
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
const POOLS_MISSING_PATCHES_COLUMNS = [{
|
||||
name: _('srPool'),
|
||||
itemRenderer: (host, { pools }) => {
|
||||
const pool = pools[host.$pool]
|
||||
return <Link to={`/pools/${pool.id}`}>{pool.name_label}</Link>
|
||||
},
|
||||
sortCriteria: (host, { pools }) => pools[host.$pool].name_label
|
||||
}].concat(MISSING_PATCHES_COLUMNS)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class HostsPatchesTable extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state.missingPatches = {}
|
||||
}
|
||||
|
||||
_getHosts = createFilter(
|
||||
() => this.props.hosts,
|
||||
createSelector(
|
||||
() => this.state.missingPatches,
|
||||
missingPatches => host => missingPatches[host.id]
|
||||
)
|
||||
)
|
||||
|
||||
_refreshMissingPatches = () => (
|
||||
Promise.all(
|
||||
map(this.props.hosts, this._refreshHostMissingPatches)
|
||||
)
|
||||
)
|
||||
|
||||
_installAllMissingPatches = () => {
|
||||
const pools = {}
|
||||
forEach(this._getHosts(), host => {
|
||||
pools[host.$pool] = true
|
||||
})
|
||||
|
||||
return Promise.all(map(
|
||||
keys(pools),
|
||||
installAllPatchesOnPool
|
||||
)).then(this._refreshMissingPatches)
|
||||
}
|
||||
|
||||
_refreshHostMissingPatches = host => (
|
||||
getHostMissingPatches(host).then(patches => {
|
||||
this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[host.id]: patches.length
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
_installAllHostPatches = host => (
|
||||
installAllHostPatches(host).then(() =>
|
||||
this._refreshHostMissingPatches(host)
|
||||
)
|
||||
)
|
||||
|
||||
componentWillMount () {
|
||||
this._refreshMissingPatches()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
// Force one Portal refresh.
|
||||
// Because Portal cannot see the container reference at first rendering.
|
||||
this.forceUpdate()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
forEach(nextProps.hosts, host => {
|
||||
const { id } = host
|
||||
|
||||
if (this.state.missingPatches[id] !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[id]: 0
|
||||
}
|
||||
})
|
||||
|
||||
this._refreshHostMissingPatches(host)
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const hosts = this._getHosts()
|
||||
const noPatches = isEmpty(hosts)
|
||||
const { props } = this
|
||||
|
||||
const Container = props.container || 'div'
|
||||
const Button = props.useTabButton ? TabButton : ActionButton
|
||||
|
||||
const Buttons = (
|
||||
<Container>
|
||||
<Button
|
||||
btnStyle='secondary'
|
||||
handler={this._refreshMissingPatches}
|
||||
icon='refresh'
|
||||
labelId='refreshPatches'
|
||||
/>
|
||||
<Button
|
||||
btnStyle='primary'
|
||||
disabled={noPatches}
|
||||
handler={this._installAllMissingPatches}
|
||||
icon='host-patch-update'
|
||||
labelId='installPoolPatches'
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!noPatches
|
||||
? (
|
||||
<SortedTable
|
||||
collection={hosts}
|
||||
columns={props.displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
|
||||
userData={{
|
||||
installAllHostPatches: this._installAllHostPatches,
|
||||
missingPatches: this.state.missingPatches,
|
||||
pools: props.pools
|
||||
}}
|
||||
/>
|
||||
) : <p>{_('patchNothing')}</p>
|
||||
}
|
||||
<Portal container={() => props.buttonsGroupContainer()}>
|
||||
{Buttons}
|
||||
</Portal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@connectStore(() => {
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
|
||||
return {
|
||||
pools: getPools
|
||||
}
|
||||
})
|
||||
class HostsPatchesTableByPool extends Component {
|
||||
render () {
|
||||
const { props } = this
|
||||
return <HostsPatchesTable {...props} pools={props.pools} />
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default propTypes({
|
||||
buttonsGroupContainer: propTypes.func.isRequired,
|
||||
container: propTypes.any,
|
||||
displayPools: propTypes.bool,
|
||||
hosts: propTypes.oneOfType([
|
||||
propTypes.arrayOf(propTypes.object),
|
||||
propTypes.objectOf(propTypes.object)
|
||||
]).isRequired,
|
||||
useTabButton: propTypes.bool
|
||||
})(props => props.displayPools
|
||||
? <HostsPatchesTableByPool {...props} />
|
||||
: <HostsPatchesTable {...props} />
|
||||
)
|
||||
@@ -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
@@ -218,9 +218,6 @@ export default {
|
||||
// Original text: "SR"
|
||||
homeTypeSr: 'SR',
|
||||
|
||||
// Original text: "VDI"
|
||||
homeTypeVdi: 'VDI',
|
||||
|
||||
// Original text: "Sort"
|
||||
homeSort: 'סינון',
|
||||
|
||||
@@ -272,11 +269,11 @@ export default {
|
||||
// Original text: "vCPUs"
|
||||
homeSortByvCPUs: 'כמות המאבדים',
|
||||
|
||||
// Original text: "{displayed, number}x {vmIcon} (on {total, number})"
|
||||
homeDisplayedVms: undefined,
|
||||
// Original text: "{displayed, number}x {icon} (on {total, number})"
|
||||
homeDisplayedItems: undefined,
|
||||
|
||||
// Original text: "{selected, number}x {vmIcon} selected (on {total, number})"
|
||||
homeSelectedVms: undefined,
|
||||
// Original text: "{selected, number}x {icon} selected (on {total, number})"
|
||||
homeSelectedItems: undefined,
|
||||
|
||||
// Original text: "More"
|
||||
homeMore: 'עוד',
|
||||
@@ -939,7 +936,7 @@ export default {
|
||||
srType: undefined,
|
||||
|
||||
// Original text: "Status"
|
||||
pdbStatus: undefined,
|
||||
pbdStatus: undefined,
|
||||
|
||||
// Original text: "Connected"
|
||||
pbdStatusConnected: undefined,
|
||||
@@ -1676,6 +1673,30 @@ export default {
|
||||
// Original text: "No backups available"
|
||||
noBackup: undefined,
|
||||
|
||||
// Original text: "Shutdown host"
|
||||
stopHostModalTitle: undefined,
|
||||
|
||||
// Original text: "This will shutdown your host. Do you want to continue?"
|
||||
stopHostModalMessage: undefined,
|
||||
|
||||
// Original text: "Restart host"
|
||||
restartHostModalTitle: undefined,
|
||||
|
||||
// Original text: "This will restart your host. Do you want to continue?"
|
||||
restartHostModalMessage: undefined,
|
||||
|
||||
// Original text: "Restart VM"
|
||||
restartVmModalTitle: undefined,
|
||||
|
||||
// Original text: "Are you sure you want to restart {name}?"
|
||||
restartVmModalMessage: undefined,
|
||||
|
||||
// Original text: "Stop VM"
|
||||
stopVmModalTitle: undefined,
|
||||
|
||||
// Original text: "Are you sure you want to stop {name}?"
|
||||
stopVmModalMessage: undefined,
|
||||
|
||||
// Original text: "Start VM{vms, plural, one {} other {s}}"
|
||||
startVmsModalTitle: undefined,
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ export default {
|
||||
newMenu: 'Novo(a)',
|
||||
|
||||
// Original text: "Tasks"
|
||||
taskMenu: 'Atividades',
|
||||
taskMenu: 'Tarefas',
|
||||
|
||||
// Original text: "VM"
|
||||
newVmPage: 'VM',
|
||||
@@ -146,6 +146,12 @@ export default {
|
||||
// Original text: "Custom Job"
|
||||
customJob: 'Personalização do Trabalho',
|
||||
|
||||
// Original text: "EN"
|
||||
enLang: 'Inglês',
|
||||
|
||||
// Original text: "FR"
|
||||
frLang: 'Francês',
|
||||
|
||||
// Original text: "Username:"
|
||||
usernameLabel: 'Usuário',
|
||||
|
||||
@@ -215,9 +221,6 @@ export default {
|
||||
// Original text: "SR"
|
||||
homeTypeSr: 'SR',
|
||||
|
||||
// Original text: "VDI"
|
||||
homeTypeVdi: 'VDI',
|
||||
|
||||
// Original text: "Sort"
|
||||
homeSort: 'Classificar',
|
||||
|
||||
@@ -240,7 +243,7 @@ export default {
|
||||
homeFilterDisabledHosts: 'Hosts Desativados',
|
||||
|
||||
// Original text: "Running VMs"
|
||||
homeFilterRunningVms: 'Vms Ativas',
|
||||
homeFilterRunningVms: 'VMs Ativas',
|
||||
|
||||
// Original text: "Non running VMs"
|
||||
homeFilterNonRunningVms: 'VMs Paradas',
|
||||
@@ -270,10 +273,10 @@ export default {
|
||||
homeSortByvCPUs: 'vCPUs',
|
||||
|
||||
// Original text: "{displayed, number}x {vmIcon} (on {total, number})"
|
||||
homeDisplayedVms: '{displayed, number}x {vmIcon} (sobre {total, number})',
|
||||
homeDisplayedVms: '{displayed, number}x {vmIcon} (de {total, number})',
|
||||
|
||||
// Original text: "{selected, number}x {vmIcon} selected (on {total, number})"
|
||||
homeSelectedVms: '{selected, number}x {vmIcon} selected (sobre {total, number})',
|
||||
homeSelectedVms: '{selected, number}x {vmIcon} selected (de {total, number})',
|
||||
|
||||
// Original text: "More"
|
||||
homeMore: 'Mais',
|
||||
@@ -405,7 +408,7 @@ export default {
|
||||
unknownSchedule: 'Desconhecido',
|
||||
|
||||
// Original text: "Job"
|
||||
job: 'tarefa',
|
||||
job: 'Tarefa',
|
||||
|
||||
// Original text: "Job ID"
|
||||
jobId: 'ID tarefa',
|
||||
@@ -414,10 +417,10 @@ export default {
|
||||
jobName: 'Nome',
|
||||
|
||||
// Original text: "Start"
|
||||
jobStart: 'Iniciar',
|
||||
jobStart: 'Inicia',
|
||||
|
||||
// Original text: "End"
|
||||
jobEnd: 'Terminar',
|
||||
jobEnd: 'Termina',
|
||||
|
||||
// Original text: "Duration"
|
||||
jobDuration: 'Duração',
|
||||
@@ -507,7 +510,7 @@ export default {
|
||||
newSrTypeSelection: 'Selecionar o tipo de armazenamento (storage)',
|
||||
|
||||
// Original text: "Settings"
|
||||
newSrSettings: 'Configuraçõesé',
|
||||
newSrSettings: 'Configurações',
|
||||
|
||||
// Original text: "Storage Usage"
|
||||
newSrUsage: 'Uso de armazenamento (storage)',
|
||||
@@ -936,7 +939,7 @@ export default {
|
||||
srType: 'Tipo',
|
||||
|
||||
// Original text: "Status"
|
||||
pdbStatus: 'Status',
|
||||
pbdStatus: 'Status',
|
||||
|
||||
// Original text: "Connected"
|
||||
pbdStatusConnected: 'Conectado',
|
||||
@@ -1308,7 +1311,7 @@ export default {
|
||||
vmPanel: 'VM{vms, plural, one {} other {s}}',
|
||||
|
||||
// Original text: "RAM Usage"
|
||||
memoryStatePanel: 'Utilização da RAM',
|
||||
memoryStatePanel: 'Utilização RAM',
|
||||
|
||||
// Original text: "CPUs Usage"
|
||||
cpuStatePanel: 'Utilização de CPU',
|
||||
@@ -1392,10 +1395,10 @@ export default {
|
||||
orphanedVms: 'VMs órfãs',
|
||||
|
||||
// Original text: "No orphans"
|
||||
noOrphanedObject: 'Sem órfãos',
|
||||
noOrphanedObject: 'Sem órfãs',
|
||||
|
||||
// Original text: "Remove all orphaned VDIs"
|
||||
removeAllOrphanedObject: 'Remover todos os VDIs órfãos',
|
||||
removeAllOrphanedObject: 'Remover todos as VDIs órfãs',
|
||||
|
||||
// Original text: "Name"
|
||||
vmNameLabel: 'Nome',
|
||||
@@ -1536,16 +1539,16 @@ export default {
|
||||
newVmCloudConfig: 'Configuração do Cloud',
|
||||
|
||||
// Origingal text: "Create VMs"
|
||||
newVmCreateVms: undefined,
|
||||
newVmCreateVms: 'Criar VMs',
|
||||
|
||||
// Original text : "Are you sure you want to create {nbVms} VMs?"
|
||||
newVmCreateVmsConfirm: undefined,
|
||||
newVmCreateVmsConfirm: 'Você tem certeza que deseja criar {nbVms} VMs?',
|
||||
|
||||
// Original text : "Multiple VMs"
|
||||
newVmMultipleVms: undefined,
|
||||
newVmMultipleVms: 'Multiplas VMs',
|
||||
|
||||
// Original text: "Resource sets"
|
||||
resourceSets: 'Ajustes dos recursos',
|
||||
resourceSets: 'Ajustes de recursos',
|
||||
|
||||
// Original text: "Resource set name"
|
||||
resourceSetName: 'Ajuste de nome do recurso',
|
||||
@@ -1569,7 +1572,7 @@ export default {
|
||||
deleteResourceSetWarning: 'Deletar grupo de recurso',
|
||||
|
||||
// Original text: "Are you sure you want to delete this resource set?"
|
||||
deleteResourceSetQuestion: 'Você tem certeza que deseja deletar este grupo de recurso?',
|
||||
deleteResourceSetQuestion: 'Você tem certeza que deseja deletar este ajuste?',
|
||||
|
||||
// Original text: "Missing objects:"
|
||||
resourceSetMissingObjects: 'Objetos faltando',
|
||||
@@ -1596,7 +1599,7 @@ export default {
|
||||
noHostsAvailable: 'Sem hosts disponiveis',
|
||||
|
||||
// Original text: "VMs created from this resource set shall run on the following hosts."
|
||||
availableHostsDescription: 'VMs criadas a partir desse conjunto de recursos deve ser executado nos seguintes hosts.',
|
||||
availableHostsDescription: 'VMs criadas a partir desse conjunto de recursos deve ser executado nos hosts indicados.',
|
||||
|
||||
// Original text: "Maximum CPUs"
|
||||
maxCpus: 'Limite de CPUs',
|
||||
@@ -1605,7 +1608,7 @@ export default {
|
||||
maxRam: 'Limite de RAM (GiB)',
|
||||
|
||||
// Original text: "Maximum disk space"
|
||||
maxDiskSpace: 'Limite de espaço do disco',
|
||||
maxDiskSpace: 'Limite de espaço de disco',
|
||||
|
||||
// Original text: "No limits."
|
||||
noResourceSetLimits: 'Sem limites',
|
||||
@@ -1620,7 +1623,7 @@ export default {
|
||||
usedResource: 'Usado:',
|
||||
|
||||
// Original text: "Try dropping some backups here, or click to select backups to upload. Accept only .xva files."
|
||||
importVmsList: 'Tente soltar alguns backups aqui, ou clique para selecionar backups que seja feito o upload. Apenas arquivos .xva são aceitos.',
|
||||
importVmsList: 'Tente soltar alguns backups aqui, ou clique para selecionar os backups para que seja feito o upload. Apenas arquivos .xva são aceitos.',
|
||||
|
||||
// Original text: "No selected VMs."
|
||||
noSelectedVms: 'Nenhuma VM selecionada',
|
||||
@@ -1673,6 +1676,30 @@ export default {
|
||||
// Original text: "No backups available"
|
||||
noBackup: 'Nenhum backup disponível',
|
||||
|
||||
// Original text: "Shutdown host"
|
||||
stopHostModalTitle: 'Desligar host',
|
||||
|
||||
// Original text: "This will shutdown your host. Do you want to continue?"
|
||||
stopHostModalMessage: 'O host será desligado. Você tem certeza que deseja continuar?',
|
||||
|
||||
// Original text: "Restart host"
|
||||
restartHostModalTitle: 'Reiniciar host',
|
||||
|
||||
// Original text: "This will restart your host. Do you want to continue?"
|
||||
restartHostModalMessage: 'O host será reiniciado. Você tem certeza que deseja continuar?',
|
||||
|
||||
// Original text: "Restart VM"
|
||||
restartVmModalTitle: 'Reiniciar VM',
|
||||
|
||||
// Original text: "Are you sure you want to restart {name}?"
|
||||
restartVmModalMessage: 'Você tem certeza que deseja reiniciar {name}?',
|
||||
|
||||
// Original text: "Stop VM"
|
||||
stopVmModalTitle: 'Parar VM',
|
||||
|
||||
// Original text: "Are you sure you want to stop {name}?"
|
||||
stopVmModalMessage: 'Você tem certeza que deseja parar {name}?',
|
||||
|
||||
// Original text: "Start VM{vms, plural, one {} other {s}}"
|
||||
startVmsModalTitle: 'Iniciar VM{vms, plural, one {} other {s}}',
|
||||
|
||||
@@ -1712,6 +1739,9 @@ export default {
|
||||
// Original text: "Migrate VM"
|
||||
migrateVmModalTitle: 'Migrar VM',
|
||||
|
||||
// Original text: "Are you sure you want to migrate this VM to {hostName}?"
|
||||
migrateVmModalBody: 'Você tem certeza que deseja migrar esta VM para {hostName}?',
|
||||
|
||||
// Original text: "Select a destination host:"
|
||||
migrateVmAdvancedModalSelectHost: 'Selecionar um host de destino:',
|
||||
|
||||
@@ -1746,7 +1776,7 @@ export default {
|
||||
importBackupModalSelectBackup: 'Selecionar backup...',
|
||||
|
||||
// Original text: "Are you sure you want to remove all orphaned VDIs?"
|
||||
removeAllOrphanedModalWarning: 'Você tem certeza que deseja remover todos os VDIs orfãos?',
|
||||
removeAllOrphanedModalWarning: 'Você tem certeza que deseja remover todos as VDIs orfãs?',
|
||||
|
||||
// Original text: "Remove all logs"
|
||||
removeAllLogsModalTitle: 'Remover todos os logs',
|
||||
|
||||
2278
src/common/intl/locales/zh.js
Normal file
2278
src/common/intl/locales/zh.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,11 @@ var forEach = require('lodash/forEach')
|
||||
var isString = require('lodash/isString')
|
||||
|
||||
var messages = {
|
||||
statusConnecting: 'Connecting',
|
||||
statusDisconnected: 'Disconnected',
|
||||
statusLoading: 'Loading…',
|
||||
errorPageNotFound: 'Page not found',
|
||||
|
||||
editableLongClickPlaceholder: 'Long click to edit',
|
||||
editableClickPlaceholder: 'Click to edit',
|
||||
|
||||
@@ -13,16 +18,28 @@ var messages = {
|
||||
confirmOk: 'OK',
|
||||
confirmCancel: 'Cancel',
|
||||
|
||||
// ----- Filters -----
|
||||
onError: 'On error',
|
||||
successful: 'Successful',
|
||||
|
||||
// ----- Copiable component -----
|
||||
copyToClipboard: 'Copy to clipboard',
|
||||
|
||||
// ----- Pills -----
|
||||
pillMaster: 'Master',
|
||||
|
||||
// ----- Titles -----
|
||||
homePage: 'Home',
|
||||
homeVmPage: 'VMs',
|
||||
homeHostPage: 'Hosts',
|
||||
homePoolPage: 'Pools',
|
||||
homeTemplatePage: 'Templates',
|
||||
dashboardPage: 'Dashboard',
|
||||
overviewDashboardPage: 'Overview',
|
||||
overviewVisualizationDashboardPage: 'Visualizations',
|
||||
overviewStatsDashboardPage: 'Statistics',
|
||||
overviewHealthDashboardPage: 'Health',
|
||||
selfServicePage: 'Self service',
|
||||
selfServiceDashboardPage: 'Dashboard',
|
||||
selfServiceAdminPage: 'Administration',
|
||||
backupPage: 'Backup',
|
||||
jobsPage: 'Jobs',
|
||||
updatePage: 'Updates',
|
||||
@@ -32,9 +49,12 @@ var messages = {
|
||||
settingsGroupsPage: 'Groups',
|
||||
settingsAclsPage: 'ACLs',
|
||||
settingsPluginsPage: 'Plugins',
|
||||
settingsLogsPage: 'Logs',
|
||||
settingsIpsPage: 'IPs',
|
||||
aboutPage: 'About',
|
||||
newMenu: 'New',
|
||||
taskMenu: 'Tasks',
|
||||
taskPage: 'Tasks',
|
||||
newVmPage: 'VM',
|
||||
newSrPage: 'Storage',
|
||||
newServerPage: 'Server',
|
||||
@@ -57,9 +77,16 @@ var messages = {
|
||||
customJob: 'Custom Job',
|
||||
userPage: 'User',
|
||||
|
||||
// ----- Support -----
|
||||
noSupport: 'No support',
|
||||
freeUpgrade: 'Free upgrade!',
|
||||
|
||||
// ----- Sign out -----
|
||||
signOut: 'Sign out',
|
||||
|
||||
// ----- User Profile -----
|
||||
editUserProfile: 'Edit my settings {username}',
|
||||
|
||||
// ----- Home view ------
|
||||
homeFetchingData: 'Fetching data…',
|
||||
homeWelcome: 'Welcome on Xen Orchestra!',
|
||||
@@ -76,11 +103,12 @@ var messages = {
|
||||
homeRestoreBackupMessage: 'Restore a backup from a remote store',
|
||||
homeNewVmMessage: 'This will create a new VM',
|
||||
homeFilters: 'Filters',
|
||||
homeNoMatches: 'No results! Click here to reset your filters',
|
||||
homeTypePool: 'Pool',
|
||||
homeTypeHost: 'Host',
|
||||
homeTypeVm: 'VM',
|
||||
homeTypeSr: 'SR',
|
||||
homeTypeVdi: 'VDI',
|
||||
homeTypeVmTemplate: 'Template',
|
||||
homeSort: 'Sort',
|
||||
homeAllPools: 'Pools',
|
||||
homeAllHosts: 'Hosts',
|
||||
@@ -98,11 +126,13 @@ var messages = {
|
||||
homeSortByPowerstate: 'Power state',
|
||||
homeSortByRAM: 'RAM',
|
||||
homeSortByvCPUs: 'vCPUs',
|
||||
homeDisplayedVms: '{displayed, number}x {vmIcon} (on {total, number})',
|
||||
homeSelectedVms: '{selected, number}x {vmIcon} selected (on {total, number})',
|
||||
homeSortByCpus: 'CPUs',
|
||||
homeDisplayedItems: '{displayed, number}x {icon} (on {total, number})',
|
||||
homeSelectedItems: '{selected, number}x {icon} selected (on {total, number})',
|
||||
homeMore: 'More',
|
||||
homeMigrateTo: 'Migrate to…',
|
||||
homeMissingPaths: 'Missing patches',
|
||||
homePoolMaster: 'Master:',
|
||||
highAvailability: 'High Availability',
|
||||
|
||||
// ----- Forms -----
|
||||
@@ -120,11 +150,19 @@ var messages = {
|
||||
selectPifs: 'Select PIF(s)…',
|
||||
selectPools: 'Select Pool(s)…',
|
||||
selectRemotes: 'Select Remote(s)…',
|
||||
selectResourceSets: 'Select resource set(s)…',
|
||||
selectResourceSetsVmTemplate: 'Select template(s)…',
|
||||
selectResourceSetsSr: 'Select SR(s)…',
|
||||
selectResourceSetsNetwork: 'Select network(s)…',
|
||||
selectResourceSetsVdi: 'Select disk(s)…',
|
||||
selectSshKey: 'Select SSH key(s)…',
|
||||
selectSrs: 'Select SR(s)…',
|
||||
selectVms: 'Select VM(s)…',
|
||||
selectVmTemplates: 'Select VM template(s)…',
|
||||
selectTags: 'Select tag(s)…',
|
||||
selectVdis: 'Select disk(s)…',
|
||||
selectTimezone: 'Select timezone…',
|
||||
selectIp: 'Select IP(s)...',
|
||||
fillRequiredInformations: 'Fill required informations.',
|
||||
fillOptionalInformations: 'Fill informations (optional)',
|
||||
selectTableReset: 'Reset',
|
||||
@@ -132,29 +170,30 @@ var messages = {
|
||||
// --- Dates/Scheduler ---
|
||||
|
||||
schedulingMonth: 'Month',
|
||||
schedulingEveryMonth: 'Every month',
|
||||
schedulingEachSelectedMonth: 'Each selected month',
|
||||
schedulingMonthDay: 'Day of the month',
|
||||
schedulingEveryMonthDay: 'Every day',
|
||||
schedulingEachSelectedMonthDay: 'Each selected day',
|
||||
schedulingWeekDay: 'Day of the week',
|
||||
schedulingEveryWeekDay: 'Every day',
|
||||
schedulingEachSelectedWeekDay: 'Each selected day',
|
||||
schedulingHour: 'Hour',
|
||||
schedulingEveryHour: 'Every hour',
|
||||
schedulingEveryNHour: 'Every N hour',
|
||||
schedulingEachSelectedHour: 'Each selected hour',
|
||||
schedulingMinute: 'Minute',
|
||||
schedulingEveryMinute: 'Every minute',
|
||||
schedulingEveryNMinute: 'Every N minute',
|
||||
schedulingEachSelectedMinute: 'Each selected minute',
|
||||
schedulingReset: 'Reset',
|
||||
unknownSchedule: 'Unknown',
|
||||
timezonePickerServerValue: 'Xo-server timezone:',
|
||||
timezonePickerUseLocalTime: 'Web browser timezone',
|
||||
timezonePickerUseServerTime: 'Xo-server timezone',
|
||||
serverTimezoneOption: 'Server timezone ({value})',
|
||||
cronPattern: 'Cron Pattern:',
|
||||
backupEditNotFoundTitle: 'Cannot edit backup',
|
||||
backupEditNotFoundMessage: 'Missing required info for edition',
|
||||
job: 'Job',
|
||||
jobId: 'Job ID',
|
||||
jobName: 'Name',
|
||||
jobNamePlaceholder: 'Name of your job (forbidden: "_")',
|
||||
jobStart: 'Start',
|
||||
jobEnd: 'End',
|
||||
jobDuration: 'Duration',
|
||||
@@ -163,6 +202,8 @@ var messages = {
|
||||
jobTag: 'Tag',
|
||||
jobScheduling: 'Scheduling',
|
||||
jobState: 'State',
|
||||
jobTimezone: 'Timezone',
|
||||
jobServerTimezone: 'xo-server',
|
||||
runJob: 'Run job',
|
||||
runJobVerbose: 'One shot running started. See overview for logs.',
|
||||
jobStarted: 'Started',
|
||||
@@ -177,9 +218,17 @@ var messages = {
|
||||
noJobs: 'No jobs found.',
|
||||
noSchedules: 'No schedules found',
|
||||
jobActionPlaceHolder: 'Select a xo-server API command',
|
||||
jobSchedules: 'Schedules',
|
||||
jobScheduleNamePlaceHolder: 'Name of your schedule',
|
||||
jobScheduleJobPlaceHolder: 'Select a Job',
|
||||
|
||||
// ------ New backup -----
|
||||
newBackupSelection: 'Select your backup type:',
|
||||
smartBackupModeSelection: 'Select backup mode:',
|
||||
normalBackup: 'Normal backup',
|
||||
smartBackup: 'Smart backup',
|
||||
localRemoteWarningTitle: 'Local remote selected',
|
||||
localRemoteWarningMessage: 'Warning: local remotes will use limited XOA disk space. Only for advanced users.',
|
||||
|
||||
// ------ New Remote -----
|
||||
remoteList: 'Remote stores for backup',
|
||||
@@ -197,9 +246,34 @@ var messages = {
|
||||
remoteTestFile: 'Test file',
|
||||
remoteTestSuccessMessage: 'The remote appears to work correctly',
|
||||
|
||||
// ------ Remote -----
|
||||
remoteName: 'Name',
|
||||
remotePath: 'Path',
|
||||
remoteState: 'State',
|
||||
remoteDevice: 'Device',
|
||||
remoteShare: 'Share',
|
||||
remoteAuth: 'Auth',
|
||||
remoteMounted: 'Mounted',
|
||||
remoteUnmounted: 'Unmounted',
|
||||
remoteConnectTip: 'Connect',
|
||||
remoteDisconnectTip: 'Disconnect',
|
||||
remoteDeleteTip: 'Delete',
|
||||
remoteNamePlaceHolder: 'remote name *',
|
||||
remoteMyNamePlaceHolder: 'Name *',
|
||||
remoteLocalPlaceHolderPath: '/path/to/backup',
|
||||
remoteNfsPlaceHolderHost: 'host *',
|
||||
remoteNfsPlaceHolderPath: '/path/to/backup',
|
||||
remoteSmbPlaceHolderRemotePath: 'subfolder [path\\to\\backup]',
|
||||
remoteSmbPlaceHolderUsername: 'Username',
|
||||
remoteSmbPlaceHolderPassword: 'Password',
|
||||
remoteSmbPlaceHolderDomain: 'Domain',
|
||||
remoteSmbPlaceHolderAddressShare: '<address>\\<share> *',
|
||||
remotePlaceHolderPassword: 'password(fill to edit)',
|
||||
|
||||
// ------ New Storage -----
|
||||
newSrTitle: 'Create a new SR',
|
||||
newSrGeneral: 'General',
|
||||
newSrTypeSelection: 'Select Strorage Type:',
|
||||
newSrTypeSelection: 'Select Storage Type:',
|
||||
newSrSettings: 'Settings',
|
||||
newSrUsage: 'Storage Usage',
|
||||
newSrSummary: 'Summary',
|
||||
@@ -222,7 +296,9 @@ var messages = {
|
||||
// ----- Acls, Users, Groups ------
|
||||
subjectName: 'Users/Groups',
|
||||
objectName: 'Object',
|
||||
aclNoneFound: 'No acls found',
|
||||
roleName: 'Role',
|
||||
aclCreate: 'Create',
|
||||
newGroupName: 'New Group Name',
|
||||
createGroup: 'Create Group',
|
||||
createGroupButton: 'Create',
|
||||
@@ -231,6 +307,7 @@ var messages = {
|
||||
removeUserFromGroup: 'Remove user from Group',
|
||||
deleteUserConfirm: 'Are you sure you want to delete this user?',
|
||||
deleteUser: 'Delete User',
|
||||
noUser: 'no user',
|
||||
unknownUser: 'unknown user',
|
||||
noGroupFound: 'No group found',
|
||||
groupNameColumn: 'Name',
|
||||
@@ -261,6 +338,25 @@ var messages = {
|
||||
cancelPluginEdition: 'Cancel',
|
||||
pluginConfigurationSuccess: 'Plugin configuration',
|
||||
pluginConfigurationChanges: 'Plugin configuration successfully saved!',
|
||||
pluginConfigurationPresetTitle: 'Predefined configuration',
|
||||
pluginConfigurationChoosePreset: 'Choose a predefined configuration.',
|
||||
applyPluginPreset: 'Apply',
|
||||
|
||||
// ----- User preferences -----
|
||||
saveNewUserFilterErrorTitle: 'Save filter error',
|
||||
saveNewUserFilterErrorBody: 'Bad parameter: name must be given.',
|
||||
filterName: 'Name:',
|
||||
filterValue: 'Value:',
|
||||
saveNewFilterTitle: 'Save new filter',
|
||||
setUserFiltersTitle: 'Set custom filters',
|
||||
setUserFiltersBody: 'Are you sure you want to set custom filters?',
|
||||
removeUserFilterTitle: 'Remove custom filter',
|
||||
removeUserFilterBody: 'Are you sure you want to remove custom filter?',
|
||||
defaultFilter: 'Default filter',
|
||||
defaultFilters: 'Default filters',
|
||||
customFilters: 'Custom filters',
|
||||
customizeFilters: 'Customize filters',
|
||||
saveCustomFilters: 'Save custom filters',
|
||||
|
||||
// ----- VM actions ------
|
||||
startVmLabel: 'Start',
|
||||
@@ -289,6 +385,10 @@ var messages = {
|
||||
srForget: 'Forget this SR',
|
||||
srRemoveButton: 'Remove this SR',
|
||||
srNoVdis: 'No VDIs in this storage',
|
||||
// ----- Pool general -----
|
||||
poolTitleRamUsage: 'Pool RAM usage:',
|
||||
poolRamUsage: '{used} used on {total}',
|
||||
poolMaster: 'Master:',
|
||||
// ----- Pool tabs -----
|
||||
hostsTabName: 'Hosts',
|
||||
// ----- Pool advanced tab -----
|
||||
@@ -300,6 +400,7 @@ var messages = {
|
||||
hostDescription: 'Description',
|
||||
hostMemory: 'Memory',
|
||||
noHost: 'No hosts',
|
||||
memoryLeftTooltip: '{used}% used ({free} free)',
|
||||
// ----- Pool network tab -----
|
||||
poolNetworkNameLabel: 'Name',
|
||||
poolNetworkDescription: 'Description',
|
||||
@@ -308,6 +409,8 @@ var messages = {
|
||||
poolNetworkMTU: 'MTU',
|
||||
poolNetworkPifAttached: 'Connected',
|
||||
poolNetworkPifDetached: 'Disconnected',
|
||||
showPifs: 'Show PIFs',
|
||||
hidePifs: 'Hide PIFs',
|
||||
// ----- Pool actions ------
|
||||
addSrLabel: 'Add SR',
|
||||
addVmLabel: 'Add VM',
|
||||
@@ -322,6 +425,7 @@ var messages = {
|
||||
restartHostAgent: 'Restart toolstack',
|
||||
forceRebootHostLabel: 'Force reboot',
|
||||
rebootHostLabel: 'Reboot',
|
||||
rebootUpdateHostLabel: 'Reboot for applying updates',
|
||||
emergencyModeLabel: 'Emergency mode',
|
||||
// ----- Host tabs -----
|
||||
storageTabName: 'Storage',
|
||||
@@ -350,6 +454,7 @@ var messages = {
|
||||
hostLicenseExpiry: 'Expiry',
|
||||
// ----- Host net tabs -----
|
||||
networkCreateButton: 'Add a network',
|
||||
networkCreateBondedButton: 'Add a bonded network',
|
||||
pifDeviceLabel: 'Device',
|
||||
pifNetworkLabel: 'Network',
|
||||
pifVlanLabel: 'VLAN',
|
||||
@@ -360,13 +465,18 @@ var messages = {
|
||||
pifStatusConnected: 'Connected',
|
||||
pifStatusDisconnected: 'Disconnected',
|
||||
pifNoInterface: 'No physical interface detected',
|
||||
pifInUse: 'This interface is currently in use',
|
||||
defaultLockingMode: 'Default locking mode',
|
||||
// ----- Host storage tabs -----
|
||||
addSrDeviceButton: 'Add a storage',
|
||||
srNameLabel: 'Name',
|
||||
srType: 'Type',
|
||||
pdbStatus: 'Status',
|
||||
pbdStatus: 'Status',
|
||||
pbdStatusConnected: 'Connected',
|
||||
pbdStatusDisconnected: 'Disconnected',
|
||||
pbdConnect: 'Connect',
|
||||
pbdDisconnect: 'Disconnect',
|
||||
pbdForget: 'Forget',
|
||||
srShared: 'Shared',
|
||||
srNotShared: 'Not shared',
|
||||
pbdNoSr: 'No storage detected',
|
||||
@@ -374,7 +484,7 @@ var messages = {
|
||||
patchNameLabel: 'Name',
|
||||
patchUpdateButton: 'Install all patches',
|
||||
patchDescription: 'Description',
|
||||
patchApplied: 'Release date',
|
||||
patchApplied: 'Applied date',
|
||||
patchSize: 'Size',
|
||||
patchStatus: 'Status',
|
||||
patchStatusApplied: 'Applied',
|
||||
@@ -383,17 +493,21 @@ var messages = {
|
||||
patchReleaseDate: 'Release date',
|
||||
patchGuidance: 'Guidance',
|
||||
patchAction: 'Action',
|
||||
hostInstalledPatches: 'Downloaded patches',
|
||||
hostAppliedPatches: 'Applied patches',
|
||||
hostMissingPatches: 'Missing patches',
|
||||
hostUpToDate: 'Host up-to-date!',
|
||||
// ----- Pool patch tabs -----
|
||||
refreshPatches: 'Refresh patches',
|
||||
installPoolPatches: 'Install pool patches',
|
||||
// ----- Pool storage tabs -----
|
||||
defaultSr: 'Default SR',
|
||||
setAsDefaultSr: 'Set as default SR',
|
||||
|
||||
// ----- VM tabs -----
|
||||
generalTabName: 'General',
|
||||
statsTabName: 'Stats',
|
||||
consoleTabName: 'Console',
|
||||
containersTabName: 'Container',
|
||||
snapshotsTabName: 'Snapshots',
|
||||
logsTabName: 'Logs',
|
||||
advancedTabName: 'Advanced',
|
||||
@@ -424,7 +538,7 @@ var messages = {
|
||||
statsCpu: 'CPU usage',
|
||||
statsMemory: 'Memory usage',
|
||||
statsNetwork: 'Network throughput',
|
||||
useCombinedValuesOnStats: 'Use combined values:',
|
||||
useStackedValuesOnStats: 'Stacked values',
|
||||
statDisk: 'Disk throughput',
|
||||
statLastTenMinutes: 'Last 10 minutes',
|
||||
statLastTwoHours: 'Last 2 hours',
|
||||
@@ -436,6 +550,21 @@ var messages = {
|
||||
ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
|
||||
tipLabel: 'Tip:',
|
||||
tipConsoleLabel: 'non-US keyboard could have issues with console: switch your own layout to US.',
|
||||
hideHeaderTooltip: 'Hide infos',
|
||||
showHeaderTooltip: 'Show infos',
|
||||
|
||||
// ----- VM container tab -----
|
||||
containerName: 'Name',
|
||||
containerCommand: 'Command',
|
||||
containerCreated: 'Creation date',
|
||||
containerStatus: 'Status',
|
||||
containerAction: 'Action',
|
||||
noContainers: 'No existing containers',
|
||||
containerStop: 'Stop this container',
|
||||
containerStart: 'Start this container',
|
||||
containerPause: 'Pause this container',
|
||||
containerResume: 'Resume this container',
|
||||
containerRestart: 'Restart this container',
|
||||
|
||||
// ----- VM disk tab -----
|
||||
vdiAction: 'Action',
|
||||
@@ -447,11 +576,21 @@ var messages = {
|
||||
vdiTags: 'Tags',
|
||||
vdiSize: 'Size',
|
||||
vdiSr: 'SR',
|
||||
vdiVm: 'VM',
|
||||
vdiMigrate: 'Migrate VDI',
|
||||
vdiMigrateSelectSr: 'Destination SR:',
|
||||
vdiMigrateAll: 'Migrate all VDIs',
|
||||
vdiMigrateNoSr: 'No SR',
|
||||
vdiMigrateNoSrMessage: 'A target SR is required to migrate a VDI',
|
||||
vdiForget: 'Forget',
|
||||
vdiRemove: 'Remove VDI',
|
||||
vdbBootableStatus: 'Boot flag',
|
||||
vdbStatus: 'Status',
|
||||
vbdStatusConnected: 'Connected',
|
||||
vbdStatusDisconnected: 'Disconnected',
|
||||
vbdNoVbd: 'No disks',
|
||||
vbdConnect: 'Connect VBD',
|
||||
vbdDisconnect: 'Disconnect VBD',
|
||||
|
||||
// ----- VM network tab -----
|
||||
vifCreateDeviceButton: 'New device',
|
||||
@@ -463,13 +602,26 @@ var messages = {
|
||||
vifStatusLabel: 'Status',
|
||||
vifStatusConnected: 'Connected',
|
||||
vifStatusDisconnected: 'Disconnected',
|
||||
vifConnect: 'Connect',
|
||||
vifDisconnect: 'Disconnect',
|
||||
vifRemove: 'Remove',
|
||||
vifIpAddresses: 'IP addresses',
|
||||
vifMacAutoGenerate: 'Auto-generated if empty',
|
||||
vifAllowedIps: 'Allowed IPs',
|
||||
vifNoIps: 'No IPs',
|
||||
vifLockedNetwork: 'Network locked',
|
||||
vifLockedNetworkNoIps: 'Network locked and no IPs are allowed for this interface',
|
||||
vifUnLockedNetwork: 'Network not locked',
|
||||
vifUnknownNetwork: 'Unknown network',
|
||||
|
||||
// ----- VM snapshot tab -----
|
||||
noSnapshots: 'No snapshots',
|
||||
snapshotCreateButton: 'New snapshot',
|
||||
tipCreateSnapshotLabel: 'Just click on the snapshot button to create one!',
|
||||
revertSnapshot: 'Revert VM to this snapshot',
|
||||
deleteSnapshot: 'Remove this snapshot',
|
||||
copySnapshot: 'Create a VM from this snapshot',
|
||||
exportSnapshot: 'Export this snapshot',
|
||||
snapshotDate: 'Creation date',
|
||||
snapshotName: 'Name',
|
||||
snapshotAction: 'Action',
|
||||
@@ -491,7 +643,9 @@ var messages = {
|
||||
uuid: 'UUID',
|
||||
virtualizationMode: 'Virtualization mode',
|
||||
cpuWeightLabel: 'CPU weight',
|
||||
defaultCpuWeight: 'Default',
|
||||
defaultCpuWeight: 'Default ({value, number})',
|
||||
cpuCapLabel: 'CPU cap',
|
||||
defaultCpuCap: 'Default ({value, number})',
|
||||
pvArgsLabel: 'PV args',
|
||||
xenToolsStatus: 'Xen tools status',
|
||||
xenToolsStatusValue: {
|
||||
@@ -519,6 +673,14 @@ var messages = {
|
||||
vmViewNamePlaceholder: 'Click to add a name',
|
||||
vmViewDescriptionPlaceholder: 'Click to add a description',
|
||||
|
||||
// ----- Templates -----
|
||||
|
||||
templateHomeNamePlaceholder: 'Click to add a name',
|
||||
templateHomeDescriptionPlaceholder: 'Click to add a description',
|
||||
templateDelete: 'Delete template',
|
||||
templateDeleteModalTitle: 'Delete VM template{templates, plural, one {} other {s}}',
|
||||
templateDeleteModalBody: 'Are you sure you want to delete {templates, plural, one {this} other {these}} template{templates, plural, one {} other {s}}?',
|
||||
|
||||
// ----- Dashboard -----
|
||||
poolPanel: 'Pool{pools, plural, one {} other {s}}',
|
||||
hostPanel: 'Host{hosts, plural, one {} other {s}}',
|
||||
@@ -529,7 +691,7 @@ var messages = {
|
||||
taskStatePanel: 'Pending tasks',
|
||||
usersStatePanel: 'Users',
|
||||
srStatePanel: 'Storage state',
|
||||
ofUsage: 'of',
|
||||
ofUsage: '{usage} (of {total})',
|
||||
noSrs: 'No storage',
|
||||
srName: 'Name',
|
||||
srPool: 'Pool',
|
||||
@@ -541,6 +703,7 @@ var messages = {
|
||||
srFree: 'free',
|
||||
srUsageStatePanel: 'Storage Usage',
|
||||
srTopUsageStatePanel: 'Top 5 SR Usage (in %)',
|
||||
vmsStates: '{running} running ({halted} halted)',
|
||||
|
||||
// --- Stats board --
|
||||
weekHeatmapData: '{value} {date, date, medium}',
|
||||
@@ -572,9 +735,12 @@ var messages = {
|
||||
alarmObject: 'Issue on',
|
||||
alarmPool: 'Pool',
|
||||
alarmRemoveAll: 'Remove all alarms',
|
||||
spaceLeftTooltip: '{used}% used ({free} left)',
|
||||
|
||||
// ----- New VM -----
|
||||
newVmCreateNewVmOn: 'Create a new VM on {pool}',
|
||||
newVmCreateNewVmOn: 'Create a new VM on {select}',
|
||||
newVmCreateNewVmOn2: 'Create a new VM on {select1} or {select2}',
|
||||
newVmCreateNewVmNoPermission: 'You have no permission to create a VM',
|
||||
newVmInfoPanel: 'Infos',
|
||||
newVmNameLabel: 'Name',
|
||||
newVmTemplateLabel: 'Template',
|
||||
@@ -582,6 +748,9 @@ var messages = {
|
||||
newVmPerfPanel: 'Performances',
|
||||
newVmVcpusLabel: 'vCPUs',
|
||||
newVmRamLabel: 'RAM',
|
||||
newVmStaticMaxLabel: 'Static memory max',
|
||||
newVmDynamicMinLabel: 'Dynamic memory min',
|
||||
newVmDynamicMaxLabel: 'Dynamic memory max',
|
||||
newVmInstallSettingsPanel: 'Install settings',
|
||||
newVmIsoDvdLabel: 'ISO/DVD',
|
||||
newVmNetworkLabel: 'Network',
|
||||
@@ -605,20 +774,29 @@ var messages = {
|
||||
newVmBootAfterCreate: 'Boot VM after creation',
|
||||
newVmMacPlaceholder: 'Auto-generated if empty',
|
||||
newVmCpuWeightLabel: 'CPU weight',
|
||||
newVmCpuWeightQuarter: 'Quarter (1/4)',
|
||||
newVmCpuWeightHalf: 'Half (1/2)',
|
||||
newVmCpuWeightNormal: 'Normal',
|
||||
newVmCpuWeightDouble: 'Double (x2)',
|
||||
newVmDefaultCpuWeight: 'Default: {value, number}',
|
||||
newVmCpuCapLabel: 'CPU cap',
|
||||
newVmDefaultCpuCap: 'Default: {value, number}',
|
||||
newVmCloudConfig: 'Cloud config',
|
||||
newVmCreateVms: 'Create VMs',
|
||||
newVmCreateVmsConfirm: 'Are you sure you want to create {nbVms} VMs?',
|
||||
newVmMultipleVms: 'Multiple VMs:',
|
||||
newVmSelectResourceSet: 'Select a resource set:',
|
||||
newVmMultipleVmsPattern: 'Name pattern:',
|
||||
newVmMultipleVmsPatternPlaceholder: 'e.g.: \\{name\\}_%',
|
||||
newVmFirstIndex: 'First index:',
|
||||
newVmNumberRecalculate: 'Recalculate VMs number',
|
||||
newVmNameRefresh: 'Refresh VMs name',
|
||||
newVmAdvancedPanel: 'Advanced',
|
||||
newVmShowAdvanced: 'Show advanced settings',
|
||||
newVmHideAdvanced: 'Hide advanced settings',
|
||||
|
||||
// ----- Self -----
|
||||
resourceSets: 'Resource sets',
|
||||
noResourceSets: 'No resource sets.',
|
||||
loadingResourceSets: 'Loading resource sets...',
|
||||
resourceSetName: 'Resource set name',
|
||||
resourceSetCreation: 'Creation and edition',
|
||||
recomputeResourceSets: 'Recompute all limits',
|
||||
saveResourceSet: 'Save',
|
||||
resetResourceSet: 'Reset',
|
||||
editResourceSet: 'Edit',
|
||||
@@ -641,9 +819,10 @@ var messages = {
|
||||
totalResource: 'Total:',
|
||||
remainingResource: 'Remaining:',
|
||||
usedResource: 'Used:',
|
||||
resourceSetNew: 'New',
|
||||
|
||||
// ---- VM import ---
|
||||
importVmsList: 'Try dropping some backups here, or click to select backups to upload. Accept only .xva files.',
|
||||
importVmsList: 'Try dropping some VMs files here, or click to select VMs to upload. Accept only .xva/.ova files.',
|
||||
noSelectedVms: 'No selected VMs.',
|
||||
vmImportToPool: 'To Pool:',
|
||||
vmImportToSr: 'To SR:',
|
||||
@@ -653,48 +832,82 @@ var messages = {
|
||||
vmImportFailed: 'VM import failed',
|
||||
startVmImport: 'Import starting…',
|
||||
startVmExport: 'Export starting…',
|
||||
nCpus: 'N CPUs',
|
||||
vmMemory: 'Memory',
|
||||
diskInfo: 'Disk {position} ({capacity})',
|
||||
diskDescription: 'Disk description',
|
||||
noDisks: 'No disks.',
|
||||
noNetworks: 'No networks.',
|
||||
networkInfo: 'Network {name}',
|
||||
noVmImportErrorDescription: 'No description available',
|
||||
vmImportError: 'Error:',
|
||||
vmImportFileType: '{type} file:',
|
||||
vmImportConfigAlert: 'Please to check and/or modify the VM configuration.',
|
||||
|
||||
// ---- Tasks ---
|
||||
noTasks: 'No pending tasks',
|
||||
xsTasks: 'Currently, there are not any pending XenServer tasks',
|
||||
|
||||
// ---- Backup views ---
|
||||
backupSchedules: 'Schedules',
|
||||
getRemote: 'Get remote',
|
||||
listRemote: 'List Remote',
|
||||
simpleBackup: 'simple',
|
||||
delta: 'delta',
|
||||
restoreBackups: 'Restore Backups',
|
||||
noRemotes: 'No remotes',
|
||||
remoteEnabled: 'enabled',
|
||||
remoteError: 'error',
|
||||
remoteEnabled: 'Enabled',
|
||||
remoteError: 'Error',
|
||||
noBackup: 'No backup available',
|
||||
backupVmNameColumn: 'VM Name',
|
||||
backupTagColumn: 'Backup Tag',
|
||||
lastBackupColumn: 'Last Backup',
|
||||
availableBackupsColumn: 'Available Backups',
|
||||
restoreColumn: 'Restore',
|
||||
restoreTip: 'Restore VM',
|
||||
restoreTip: 'View restore options',
|
||||
displayBackup: 'Display backups',
|
||||
importBackupTitle: 'Import VM',
|
||||
importBackupMessage: 'Starting your backup import',
|
||||
vmsToBackup: 'VMs to backup',
|
||||
|
||||
// ----- Modals -----
|
||||
emergencyShutdownHostsModalTitle: 'Emergency shutdown Host{nHosts, plural, one {} other {s}}',
|
||||
emergencyShutdownHostsModalMessage: 'Are you sure you want to shutdown {nHosts} Host{nHosts, plural, one {} other {s}}?',
|
||||
stopHostModalTitle: 'Shutdown host',
|
||||
stopHostModalMessage: 'This will shutdown your host. Do you want to continue? If it\'s the pool master, your connection to the pool will be lost',
|
||||
addHostModalTitle: 'Add host',
|
||||
addHostModalMessage: 'Are you sure you want to add {host} to {pool}?',
|
||||
restartHostModalTitle: 'Restart host',
|
||||
restartHostModalMessage: 'This will restart your host. Do you want to continue?',
|
||||
restartHostsAgentsModalTitle: 'Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}',
|
||||
restartHostsAgentsModalMessage: 'Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?',
|
||||
restartHostsModalTitle: 'Restart Host{nHosts, plural, one {} other {s}}',
|
||||
restartHostsModalMessage: 'Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}}?',
|
||||
startVmsModalTitle: 'Start VM{vms, plural, one {} other {s}}',
|
||||
startVmsModalMessage: 'Are you sure you want to start {vms} VM{vms, plural, one {} other {s}}?',
|
||||
stopHostsModalTitle: 'Stop Host{nHosts, plural, one {} other {s}}',
|
||||
stopHostsModalMessage: 'Are you sure you want to stop {nHosts} Host{nHosts, plural, one {} other {s}}?',
|
||||
stopVmsModalTitle: 'Stop VM{vms, plural, one {} other {s}}',
|
||||
stopVmsModalMessage: 'Are you sure you want to stop {vms} VM{vms, plural, one {} other {s}}?',
|
||||
restartVmModalTitle: 'Restart VM',
|
||||
restartVmModalMessage: 'Are you sure you want to restart {name}?',
|
||||
stopVmModalTitle: 'Stop VM',
|
||||
stopVmModalMessage: 'Are you sure you want to stop {name}?',
|
||||
restartVmsModalTitle: 'Restart VM{vms, plural, one {} other {s}}',
|
||||
restartVmsModalMessage: 'Are you sure you want to restart {vms} VM{vms, plural, one {} other {s}}?',
|
||||
snapshotVmsModalTitle: 'Snapshot VM{vms, plural, one {} other {s}}',
|
||||
snapshotVmsModalMessage: 'Are you sure you want to snapshot {vms} VM{vms, plural, one {} other {s}}?',
|
||||
deleteVmModalTitle: 'Delete VM',
|
||||
deleteVmsModalTitle: 'Delete VM{vms, plural, one {} other {s}}',
|
||||
deleteVmModalMessage: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
|
||||
deleteVmsModalMessage: 'Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
|
||||
deleteVmModalTitle: 'Delete VM',
|
||||
deleteVmModalMessage: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
|
||||
migrateVmModalTitle: 'Migrate VM',
|
||||
migrateVmSelectHost: 'Select a destination host:',
|
||||
migrateVmSelectMigrationNetwork: 'Select a migration network:',
|
||||
migrateVmSelectSrs: 'For each VDI, select an SR:',
|
||||
migrateVmSelectNetworks: 'For each VIF, select a network:',
|
||||
migrateVmsSelectSr: 'Select a destination SR:',
|
||||
migrateVmsSelectSrIntraPool: 'Select a destination SR for local disks:',
|
||||
migrateVmsSelectNetwork: 'Select a network on which to connect each VIF:',
|
||||
migrateVmsSmartMapping: 'Smart mapping',
|
||||
migrateVmName: 'Name',
|
||||
@@ -703,8 +916,13 @@ var messages = {
|
||||
migrateVmNetwork: 'Network',
|
||||
migrateVmNoTargetHost: 'No target host',
|
||||
migrateVmNoTargetHostMessage: 'A target host is required to migrate a VM',
|
||||
migrateVmBadPowerState: 'Bad power state: only running VMs can be migrated.',
|
||||
migrateVmSomeBadPowerState: 'Some of the selected VMs are not running (e.g.: {vm}) and therefore will not be migrated.',
|
||||
deleteVdiModalTitle: 'Delete VDI',
|
||||
deleteVdiModalMessage: 'Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST',
|
||||
revertVmModalTitle: 'Revert your VM',
|
||||
deleteSnapshotModalTitle: 'Delete snapshot',
|
||||
deleteSnapshotModalMessage: 'Are you sure you want to delete this snapshot?',
|
||||
revertVmModalMessage: 'Are you sure you want to revert this VM to the snapshot state? This operation is irreversible.',
|
||||
revertVmModalSnapshotBefore: 'Snapshot before',
|
||||
importBackupModalTitle: 'Import a {name} Backup',
|
||||
importBackupModalStart: 'Start VM after restore',
|
||||
importBackupModalSelectBackup: 'Select your backup…',
|
||||
@@ -727,17 +945,32 @@ var messages = {
|
||||
serverPassword: 'Password',
|
||||
serverAction: 'Action',
|
||||
serverReadOnly: 'Read Only',
|
||||
serverDisconnect: 'Disconnect server',
|
||||
serverPlaceHolderUser: 'username',
|
||||
serverPlaceHolderPassword: 'password',
|
||||
serverPlaceHolderAddress: 'address[:port]',
|
||||
serverConnect: 'Connect',
|
||||
|
||||
// ----- Copy VM -----
|
||||
copyVm: 'Copy VM',
|
||||
copyVmConfirm: 'Are you sure you want to copy this VM to {SR}?',
|
||||
copyVmName: 'Name',
|
||||
copyVmNamePattern: 'Name pattern',
|
||||
copyVmNamePlaceholder: 'If empty: name of the copied VM',
|
||||
copyVmNamePatternPlaceholder: 'e.g.: "\\{name\\}_COPY"',
|
||||
copyVmSelectSr: 'Select SR',
|
||||
copyVmCompress: 'Use compression',
|
||||
copyVmsNoTargetSr: 'No target SR',
|
||||
copyVmsNoTargetSrMessage: 'A target SR is required to copy a VM',
|
||||
|
||||
// ----- Detach host -----
|
||||
detachHostModalTitle: 'Detach host',
|
||||
detachHostModalMessage: 'Are you sure you want to detach {host} from its pool? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND REBOOT THE HOST.',
|
||||
detachHost: 'Detach',
|
||||
|
||||
// ----- Network -----
|
||||
newNetworkCreate: 'Create network',
|
||||
newBondedNetworkCreate: 'Create bonded network',
|
||||
newNetworkInterface: 'Interface',
|
||||
newNetworkName: 'Name',
|
||||
newNetworkDescription: 'Description',
|
||||
@@ -745,8 +978,18 @@ var messages = {
|
||||
newNetworkDefaultVlan: 'No VLAN if empty',
|
||||
newNetworkMtu: 'MTU',
|
||||
newNetworkDefaultMtu: 'Default: 1500',
|
||||
newNetworkNoNameErrorTitle: 'Name required',
|
||||
newNetworkNoNameErrorMessage: 'A name is required to create a network',
|
||||
newNetworkBondMode: 'Bond mode',
|
||||
deleteNetwork: 'Delete network',
|
||||
deleteNetworkConfirm: 'Are you sure you want to delete this network?',
|
||||
networkInUse: 'This network is currently in use',
|
||||
pillBonded: 'Bonded',
|
||||
|
||||
// ----- Add host -----
|
||||
addHostSelectHost: 'Host',
|
||||
addHostNoHost: 'No host',
|
||||
addHostNoHostMessage: 'No host selected to be added',
|
||||
|
||||
// ----- About View -----
|
||||
xenOrchestra: 'Xen Orchestra',
|
||||
@@ -776,18 +1019,23 @@ var messages = {
|
||||
or: 'Or',
|
||||
tryIt: 'Try it for free!',
|
||||
availableIn: 'This feature is available starting from {plan} Edition',
|
||||
notAvailable: 'This feature is not available in your version, contact your administrator to know more.',
|
||||
|
||||
// ----- Updates View -----
|
||||
updateTitle: 'Updates',
|
||||
registration: 'Registration',
|
||||
trial: 'Trial',
|
||||
settings: 'Settings',
|
||||
proxySettings: 'Proxy settings',
|
||||
update: 'Update',
|
||||
refresh: 'Refresh',
|
||||
upgrade: 'Upgrade',
|
||||
noUpdaterCommunity: 'No updater available for Community Edition',
|
||||
noUpdaterSubscribe: 'Please consider subscribe and try it with all features for free during 15 days on',
|
||||
noUpdaterWarning: 'Manual update could break your current installation due to dependencies issues, do it with caution',
|
||||
currentVersion: 'Current version:',
|
||||
register: 'Register',
|
||||
editRegistration: 'Edit registration',
|
||||
trialRegistration: 'Please, take time to register in order to enjoy your trial.',
|
||||
trialStartButton: 'Start trial',
|
||||
trialAvailableUntil: 'You can use a trial version until {date, date, medium}. Upgrade your appliance to get it.',
|
||||
@@ -829,7 +1077,66 @@ var messages = {
|
||||
pwdChangeSuccessBody: 'Your password has been successfully changed.',
|
||||
pwdChangeError: 'Incorrect password',
|
||||
pwdChangeErrorBody: 'The old password provided is incorrect. Your password has not been changed.',
|
||||
changePasswordOk: 'OK'
|
||||
changePasswordOk: 'OK',
|
||||
sshKeys: 'SSH keys',
|
||||
newSshKey: 'New SSH key',
|
||||
deleteSshKey: 'Delete',
|
||||
noSshKeys: 'No SSH keys',
|
||||
newSshKeyModalTitle: 'New SSH key',
|
||||
sshKeyErrorTitle: 'Invalid key',
|
||||
sshKeyErrorMessage: 'An SSH key requires both a title and a key.',
|
||||
title: 'Title',
|
||||
key: 'Key',
|
||||
deleteSshKeyConfirm: 'Delete SSH key',
|
||||
deleteSshKeyConfirmMessage: 'Are you sure you want to delete the SSH key {title}?',
|
||||
|
||||
// ----- Usage -----
|
||||
others: 'Others',
|
||||
|
||||
// ----- Logs -----
|
||||
loadingLogs: 'Loading logs...',
|
||||
logUser: 'User',
|
||||
logMethod: 'Method',
|
||||
logParams: 'Params',
|
||||
logMessage: 'Message',
|
||||
logError: 'Error',
|
||||
logDisplayDetails: 'Display details',
|
||||
logTime: 'Date',
|
||||
logNoStackTrace: 'No stack trace',
|
||||
logNoParams: 'No params',
|
||||
logDelete: 'Delete log',
|
||||
logDeleteAll: 'Delete all logs',
|
||||
logDeleteAllTitle: 'Delete all logs',
|
||||
logDeleteAllMessage: 'Are you sure you want to delete all the logs?',
|
||||
|
||||
// ----- IPs ------
|
||||
ipPoolName: 'Name',
|
||||
ipPoolIps: 'IPs',
|
||||
ipPoolIpsPlaceholder: 'IPs (e.g.: 1.0.0.12-1.0.0.17;1.0.0.23)',
|
||||
ipPoolNetworks: 'Networks',
|
||||
ipsNoIpPool: 'No IP pools',
|
||||
ipsCreate: 'Create',
|
||||
ipsDeleteAllTitle: 'Delete all IP pools',
|
||||
ipsDeleteAllMessage: 'Are you sure you want to delete all the IP pools?',
|
||||
ipsVifs: 'VIFs',
|
||||
ipsNotUsed: 'Not used',
|
||||
|
||||
// ----- Shortcuts -----
|
||||
shortcutModalTitle: 'Keyboard shortcuts',
|
||||
shortcut_XoApp: 'Global',
|
||||
shortcut_GO_TO_HOSTS: 'Go to hosts list',
|
||||
shortcut_GO_TO_POOLS: 'Go to pools list',
|
||||
shortcut_GO_TO_VMS: 'Go to VMs list',
|
||||
shortcut_CREATE_VM: 'Create a new VM',
|
||||
shortcut_UNFOCUS: 'Unfocus field',
|
||||
shortcut_HELP: 'Show shortcuts key bindings',
|
||||
shortcut_Home: 'Home',
|
||||
shortcut_SEARCH: 'Focus search bar',
|
||||
shortcut_NAV_DOWN: 'Next item',
|
||||
shortcut_NAV_UP: 'Previous item',
|
||||
shortcut_SELECT: 'Select item',
|
||||
shortcut_JUMP_INTO: 'Open'
|
||||
|
||||
}
|
||||
forEach(messages, function (message, id) {
|
||||
if (isString(message)) {
|
||||
|
||||
126
src/common/ip.js
Normal file
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
|
||||
}
|
||||
@@ -23,7 +23,7 @@ export default class BooleanInput extends AbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<div className='checkbox form-control'>
|
||||
<Toggle
|
||||
defaultValue={props.defaultValue || props.schema.default}
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
ref='input'
|
||||
|
||||
@@ -48,12 +48,14 @@ export default class GenericInput extends Component {
|
||||
render () {
|
||||
const {
|
||||
schema,
|
||||
defaultValue = schema.default,
|
||||
uiSchema = EMPTY_OBJECT,
|
||||
...opts
|
||||
} = this.props
|
||||
|
||||
const props = {
|
||||
...opts,
|
||||
defaultValue,
|
||||
schema,
|
||||
uiSchema,
|
||||
ref: 'input'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import Combobox from '../combobox'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
@@ -20,16 +21,16 @@ export default class IntegerInput extends AbstractInput {
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const { onChange } = props
|
||||
const { schema } = props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<input
|
||||
className='form-control'
|
||||
defaultValue={props.defaultValue || ''}
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={onChange && (event => onChange(event.target.value))}
|
||||
placeholder={props.placeholder}
|
||||
onChange={props.onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
step={1}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import Combobox from '../combobox'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
@@ -20,16 +21,16 @@ export default class NumberInput extends AbstractInput {
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const { onChange } = props
|
||||
const { schema } = props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<input
|
||||
className='form-control'
|
||||
defaultValue={props.defaultValue || ''}
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={onChange && (event => onChange(event.target.value))}
|
||||
placeholder={props.placeholder}
|
||||
onChange={props.onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
step='any'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import Combobox from '../combobox'
|
||||
import propTypes from '../prop-types'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
@@ -12,19 +13,19 @@ import { PrimitiveInputWrapper } from './helpers'
|
||||
export default class StringInput extends AbstractInput {
|
||||
render () {
|
||||
const { props } = this
|
||||
const { onChange } = props
|
||||
const { schema } = props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<input
|
||||
className='form-control'
|
||||
defaultValue={props.defaultValue || ''}
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={onChange && (event => onChange(event.target.value))}
|
||||
placeholder={props.placeholder}
|
||||
onChange={props.onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
type={props.password ? 'password' : 'text'}
|
||||
type={props.password && 'password'}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -60,7 +60,10 @@ const _addRef = (component, ref) => {
|
||||
class Confirm extends Component {
|
||||
_resolve = () => {
|
||||
const { body } = this.refs
|
||||
this.props.resolve(body && body.value || body.getWrappedInstance && body.getWrappedInstance().value)
|
||||
this.props.resolve(body && (body.getWrappedInstance
|
||||
? body.getWrappedInstance().value
|
||||
: body.value
|
||||
))
|
||||
instance.close()
|
||||
}
|
||||
_reject = () => {
|
||||
@@ -79,9 +82,10 @@ class Confirm extends Component {
|
||||
return <div>
|
||||
<Header closeButton>
|
||||
<Title>
|
||||
{icon
|
||||
? <span><Icon icon={icon} /> {title}</span>
|
||||
: title}
|
||||
{icon
|
||||
? <span><Icon icon={icon} /> {title}</span>
|
||||
: title
|
||||
}
|
||||
</Title>
|
||||
</Header>
|
||||
<Body>
|
||||
|
||||
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,4 @@
|
||||
import React, { Component } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import Icon from './icon'
|
||||
import propTypes from './prop-types'
|
||||
@@ -55,13 +55,12 @@ export const SrItem = propTypes({
|
||||
let label = `${sr.name_label || sr.id}`
|
||||
|
||||
if (isSrWritable(sr)) {
|
||||
label += ` (${formatSize(sr.size)})`
|
||||
label += ` (${formatSize(sr.size - sr.physical_usage)} free)`
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Icon icon='sr' /> {label}
|
||||
{container && ` (${container.name_label || container.id})`}
|
||||
</span>
|
||||
)
|
||||
}))
|
||||
@@ -108,6 +107,27 @@ const xoItemToRender = {
|
||||
<Icon icon='user' /> {user.email}
|
||||
</span>
|
||||
),
|
||||
resourceSet: resourceSet => (
|
||||
<span>
|
||||
<Icon icon='resource-set' /> {resourceSet.name}
|
||||
</span>
|
||||
),
|
||||
sshKey: key => (
|
||||
<span>
|
||||
<Icon icon='ssh-key' /> {key.label}
|
||||
</span>
|
||||
),
|
||||
ipPool: ipPool => (
|
||||
<span>
|
||||
<Icon icon='ip' /> {ipPool.name}
|
||||
</span>
|
||||
),
|
||||
ipAddress: ({label, used}) => {
|
||||
if (used) {
|
||||
return <strong className='text-warning'>{label}</strong>
|
||||
}
|
||||
return <span>{label}</span>
|
||||
},
|
||||
|
||||
// XO objects.
|
||||
pool: pool => (
|
||||
@@ -133,6 +153,13 @@ const xoItemToRender = {
|
||||
// VM.
|
||||
VM: vm => <VmItem vm={vm} />,
|
||||
'VM-snapshot': vm => <VmItem vm={vm} />,
|
||||
'VM-controller': vm => (
|
||||
<span>
|
||||
<Icon icon='host' />
|
||||
{' '}
|
||||
<VmItem vm={vm} />
|
||||
</span>
|
||||
),
|
||||
|
||||
// PIF.
|
||||
PIF: pif => (
|
||||
@@ -154,7 +181,10 @@ const renderXoItem = (item, {
|
||||
} = {}) => {
|
||||
const { id, type, label } = item
|
||||
|
||||
if (!type && label) {
|
||||
if (!type) {
|
||||
if (process.env.NODE_ENV !== 'production' && !label) {
|
||||
throw new Error(`an item must have at least either a type or a label`)
|
||||
}
|
||||
return (
|
||||
<span key={id} className={className}>
|
||||
{label}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import forEach from 'lodash/forEach'
|
||||
import includes from 'lodash/includes'
|
||||
import join from 'lodash/join'
|
||||
import later from 'later'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import sortedIndex from 'lodash/sortedIndex'
|
||||
import { FormattedTime } from 'react-intl'
|
||||
import { FormattedDate, FormattedTime } from 'react-intl'
|
||||
import {
|
||||
Panel,
|
||||
Tab,
|
||||
Tabs
|
||||
} from 'react-bootstrap-4/lib'
|
||||
@@ -15,21 +13,29 @@ import {
|
||||
import _ from './intl'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import TimezonePicker from './timezone-picker'
|
||||
import { Card, CardHeader, CardBlock } from './card'
|
||||
import { Col, Row } from './grid'
|
||||
import { Range } from './form'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const NAV_EVERY = 1
|
||||
const NAV_EACH_SELECTED = 2
|
||||
const NAV_EVERY_N = 3
|
||||
// By default later use UTC but we use this line for futures versions.
|
||||
later.date.UTC()
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const NAV_EACH_SELECTED = 1
|
||||
const NAV_EVERY_N = 2
|
||||
|
||||
const MIN_PREVIEWS = 5
|
||||
const MAX_PREVIEWS = 20
|
||||
|
||||
const MONTHS = [
|
||||
[ 0, 1, 2, 3, 4, 5 ],
|
||||
[ 6, 7, 8, 9, 10, 11 ]
|
||||
[ 0, 1, 2 ],
|
||||
[ 3, 4, 5 ],
|
||||
[ 6, 7, 8 ],
|
||||
[ 9, 10, 11 ]
|
||||
]
|
||||
|
||||
const DAYS = (() => {
|
||||
@@ -48,7 +54,11 @@ const DAYS = (() => {
|
||||
return days
|
||||
})()
|
||||
|
||||
const WEEK_DAYS = [[ 0, 1, 2, 3, 4, 5, 6 ]]
|
||||
const WEEK_DAYS = [
|
||||
[ 0, 1, 2 ],
|
||||
[ 3, 4, 5 ],
|
||||
[ 6 ]
|
||||
]
|
||||
|
||||
const HOURS = (() => {
|
||||
const hours = []
|
||||
@@ -78,30 +88,46 @@ const MINS = (() => {
|
||||
return minutes
|
||||
})()
|
||||
|
||||
const PICKTIME_TO_ID = {
|
||||
minute: 0,
|
||||
hour: 1,
|
||||
monthDay: 2,
|
||||
month: 3,
|
||||
weekDay: 4
|
||||
}
|
||||
|
||||
const TIME_FORMAT = {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
minute: 'numeric',
|
||||
|
||||
// The timezone is not significant for displaying the date previews
|
||||
// as long as it is the same used to generate the next occurrences
|
||||
// from the cron patterns.
|
||||
|
||||
// Therefore we can use UTC everywhere and say to the user that the
|
||||
// previews are in the configured timezone.
|
||||
timeZone: 'UTC'
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// monthNum: [ 0 : 11 ]
|
||||
const getMonthName = (monthNum) =>
|
||||
<FormattedTime value={new Date(1970, monthNum)} month='long' />
|
||||
<FormattedDate value={Date.UTC(1970, monthNum)} month='long' timeZone='UTC' />
|
||||
|
||||
// dayNum: [ 0 : 6 ]
|
||||
const getDayName = (dayNum) =>
|
||||
// January, 1970, 5th => Monday
|
||||
<FormattedTime value={new Date(1970, 0, 4 + dayNum)} weekday='long' />
|
||||
<FormattedDate value={Date.UTC(1970, 0, 4 + dayNum)} weekday='long' timeZone='UTC' />
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
cron: propTypes.string.isRequired
|
||||
cronPattern: propTypes.string.isRequired
|
||||
})
|
||||
export class SchedulePreview extends Component {
|
||||
_handleChange = value => {
|
||||
@@ -111,12 +137,15 @@ export class SchedulePreview extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const cronSched = later.parse.cron(props.cron)
|
||||
const { cronPattern } = this.props
|
||||
const cronSched = later.parse.cron(cronPattern)
|
||||
const dates = later.schedule(cronSched).next(this.state.value || MIN_PREVIEWS)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='alert alert-info' role='alert'>
|
||||
{_('cronPattern')} <strong>{cronPattern}</strong>
|
||||
</div>
|
||||
<div className='form-inline p-b-1'>
|
||||
<Range min={MIN_PREVIEWS} max={MAX_PREVIEWS} onChange={this._handleChange} />
|
||||
</div>
|
||||
@@ -137,34 +166,21 @@ export class SchedulePreview extends Component {
|
||||
|
||||
@propTypes({
|
||||
children: propTypes.any.isRequired,
|
||||
onChange: propTypes.func
|
||||
onChange: propTypes.func.isRequired,
|
||||
tdId: propTypes.number.isRequired,
|
||||
value: propTypes.bool.isRequired
|
||||
})
|
||||
class ToggleTd extends Component {
|
||||
get value () {
|
||||
return this.state.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
const { onChange } = this.props
|
||||
|
||||
this.setState({
|
||||
value
|
||||
}, onChange && (() => onChange(value)))
|
||||
}
|
||||
|
||||
_onClick = () => {
|
||||
const { onChange } = this.props
|
||||
const value = !this.state.value
|
||||
|
||||
this.setState({
|
||||
value
|
||||
}, onChange && (() => onChange(value)))
|
||||
const { props } = this
|
||||
props.onChange(props.tdId, !props.value)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
return (
|
||||
<td style={{ cursor: 'pointer' }} className={this.state.value ? 'table-success' : ''} onClick={this._onClick}>
|
||||
{this.props.children}
|
||||
<td style={{ cursor: 'pointer' }} className={props.value ? 'table-success' : ''} onClick={this._onClick}>
|
||||
{props.children}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
@@ -173,79 +189,64 @@ class ToggleTd extends Component {
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
data: propTypes.array.isRequired,
|
||||
dataRender: propTypes.func,
|
||||
onChange: propTypes.func
|
||||
options: propTypes.array.isRequired,
|
||||
optionsRenderer: propTypes.func,
|
||||
onChange: propTypes.func.isRequired,
|
||||
value: propTypes.array.isRequired
|
||||
})
|
||||
class TableSelect extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
this.state = {
|
||||
value: []
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.state.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
const { onChange } = this.props
|
||||
|
||||
forEach(this.refs, (ref, id) => {
|
||||
// Don't call ref.input directly because onChange of each ToggleTd is called else!
|
||||
ref.setState({
|
||||
value: includes(value, +id)
|
||||
})
|
||||
})
|
||||
|
||||
this.setState({
|
||||
value
|
||||
}, onChange && (() => onChange(value)))
|
||||
static defaultProps = {
|
||||
optionsRenderer: value => value
|
||||
}
|
||||
|
||||
_reset = () => {
|
||||
this.value = []
|
||||
this.props.onChange([])
|
||||
}
|
||||
|
||||
_handleChange = (id, value) => {
|
||||
const { onChange } = this.props
|
||||
const newValue = this.state.value.slice()
|
||||
_handleChange = (tdId, tdValue) => {
|
||||
const { props } = this
|
||||
|
||||
if (value) {
|
||||
newValue.splice(sortedIndex(newValue, id), 0, id)
|
||||
const newValue = props.value.slice()
|
||||
const index = sortedIndex(newValue, tdId)
|
||||
|
||||
if (tdValue) {
|
||||
// Add
|
||||
if (newValue[index] !== tdId) {
|
||||
newValue.splice(index, 0, tdId)
|
||||
}
|
||||
} else {
|
||||
newValue.splice(sortedIndex(newValue, id), 1)
|
||||
// Remove
|
||||
if (newValue[index] === tdId) {
|
||||
newValue.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
value: newValue
|
||||
}, onChange && (() => onChange(newValue)))
|
||||
props.onChange(newValue)
|
||||
}
|
||||
|
||||
render () {
|
||||
const dataRender = this.props.dataRender || ((value) => value)
|
||||
const {
|
||||
props: {
|
||||
data
|
||||
}
|
||||
} = this
|
||||
const { length } = data[0]
|
||||
options,
|
||||
optionsRenderer,
|
||||
value
|
||||
} = this.props
|
||||
const { length } = options[0]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className='table table-bordered table-sm'>
|
||||
<tbody>
|
||||
{map(data, (line, i) => (
|
||||
{map(options, (line, i) => (
|
||||
<tr key={i}>
|
||||
{map(line, (value, j) => {
|
||||
const id = length * i + j
|
||||
{map(line, (tdOption, j) => {
|
||||
const tdId = length * i + j
|
||||
return (
|
||||
<ToggleTd
|
||||
key={id}
|
||||
ref={id}
|
||||
children={dataRender(value)}
|
||||
onChange={(value) => { this._handleChange(id, value) }}
|
||||
children={optionsRenderer(tdOption)}
|
||||
tdId={tdId}
|
||||
key={tdId}
|
||||
onChange={this._handleChange}
|
||||
value={includes(value, tdId)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
@@ -264,177 +265,153 @@ class TableSelect extends Component {
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
dataRender: propTypes.func,
|
||||
onChange: propTypes.func,
|
||||
optionsRenderer: propTypes.func,
|
||||
onChange: propTypes.func.isRequired,
|
||||
range: propTypes.array,
|
||||
type: propTypes.string.isRequired
|
||||
labelId: propTypes.string.isRequired,
|
||||
value: propTypes.any.isRequired,
|
||||
valueRenderer: propTypes.func
|
||||
})
|
||||
class TimePicker extends Component {
|
||||
static defaultProps = {
|
||||
valueRenderer: e => +e
|
||||
}
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
this.state = {
|
||||
activeKey: NAV_EVERY
|
||||
activeKey: NAV_EACH_SELECTED,
|
||||
tableValue: []
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { activeKey } = this.state
|
||||
const { refs } = this
|
||||
_update (props) {
|
||||
const { value, valueRenderer } = props
|
||||
|
||||
if (activeKey === NAV_EVERY) {
|
||||
return 'all'
|
||||
}
|
||||
|
||||
if (activeKey === NAV_EACH_SELECTED) {
|
||||
return refs.select.value
|
||||
}
|
||||
|
||||
return refs.range.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
const { refs } = this
|
||||
const { onChange } = this.props
|
||||
|
||||
if (value === 'all') {
|
||||
this.setState({
|
||||
activeKey: NAV_EVERY
|
||||
}, onChange && (() => onChange(value)))
|
||||
} else if (Array.isArray(value)) {
|
||||
this.setState({
|
||||
activeKey: NAV_EACH_SELECTED
|
||||
})
|
||||
refs.select.value = value
|
||||
} else {
|
||||
if (value.indexOf('/') === 1) {
|
||||
this.setState({
|
||||
activeKey: NAV_EVERY_N
|
||||
}, () => { this.refs.range.value = value.split('/')[1] })
|
||||
} else {
|
||||
this.setState({
|
||||
activeKey: NAV_EACH_SELECTED,
|
||||
tableValue: value === '*'
|
||||
? []
|
||||
: map(value.split(','), valueRenderer)
|
||||
})
|
||||
refs.range.value = value
|
||||
}
|
||||
}
|
||||
|
||||
_updateOpen = () => {
|
||||
this.setState({
|
||||
open: !this.state.open
|
||||
})
|
||||
componentWillMount () {
|
||||
this._update(this.props)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
this._update(props)
|
||||
}
|
||||
|
||||
_selectTab = activeKey => {
|
||||
const { onChange } = this.props
|
||||
|
||||
this.setState({
|
||||
activeKey
|
||||
}, onChange && (() => onChange(this.value)))
|
||||
}, () => {
|
||||
const { activeKey, tableValue } = this.state
|
||||
const { onChange } = this.props
|
||||
const { refs } = this
|
||||
|
||||
if (activeKey === NAV_EACH_SELECTED) {
|
||||
onChange(tableValue)
|
||||
} else {
|
||||
onChange(refs.range.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_handleTableValue = tableValue => {
|
||||
this.setState({
|
||||
tableValue
|
||||
}, () => this.props.onChange(tableValue))
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
props,
|
||||
state
|
||||
} = this
|
||||
|
||||
const {
|
||||
onChange,
|
||||
options,
|
||||
optionsRenderer,
|
||||
range,
|
||||
type
|
||||
} = props
|
||||
labelId
|
||||
} = this.props
|
||||
const { tableValue } = this.state
|
||||
|
||||
const tableSelect = (
|
||||
<TableSelect
|
||||
onChange={this._handleTableValue}
|
||||
options={options}
|
||||
optionsRenderer={optionsRenderer}
|
||||
value={tableValue}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className='card'>
|
||||
<button className='card-header btn btn-lg btn-block' onClick={this._updateOpen}>
|
||||
{_(`scheduling${type}`)}
|
||||
</button>
|
||||
<Panel collapsible expanded={state.open}>
|
||||
<div className='card-block'>
|
||||
<Tabs bsStyle='tabs' activeKey={state.activeKey} onSelect={this._selectTab}>
|
||||
<Tab tabClassName='nav-item' eventKey={NAV_EVERY} title={_(`schedulingEvery${type}`)} />
|
||||
<Tab tabClassName='nav-item' eventKey={NAV_EACH_SELECTED} title={_(`schedulingEachSelected${type}`)}>
|
||||
<TableSelect ref='select' data={props.data} dataRender={props.dataRender} onChange={onChange} />
|
||||
<Card>
|
||||
<CardHeader>
|
||||
{_(`scheduling${labelId}`)}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{range
|
||||
? (
|
||||
<Tabs bsStyle='tabs' activeKey={this.state.activeKey} onSelect={this._selectTab}>
|
||||
<Tab tabClassName='nav-item' eventKey={NAV_EACH_SELECTED} title={_(`schedulingEachSelected${labelId}`)}>
|
||||
{tableSelect}
|
||||
</Tab>
|
||||
<Tab tabClassName='nav-item' eventKey={NAV_EVERY_N} title={_(`schedulingEveryN${labelId}`)}>
|
||||
<Range ref='range' min={range[0]} max={range[1]} onChange={onChange} />
|
||||
</Tab>
|
||||
{range &&
|
||||
<Tab tabClassName='nav-item' eventKey={NAV_EVERY_N} title={_(`schedulingEveryN${type}`)}>
|
||||
<Range ref='range' min={range[0]} max={range[1]} onChange={onChange} />
|
||||
</Tab>}
|
||||
</Tabs>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
) : tableSelect
|
||||
}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const ID_TO_PICKTIME = [
|
||||
'minute',
|
||||
'hour',
|
||||
'monthDay',
|
||||
'month',
|
||||
'weekDay'
|
||||
]
|
||||
const HOURS_RANGE = [2, 12]
|
||||
const MINUTES_RANGE = [2, 30]
|
||||
|
||||
const decrement = e => e - 1
|
||||
|
||||
@propTypes({
|
||||
onChange: propTypes.func
|
||||
cronPattern: propTypes.string.isRequired,
|
||||
onChange: propTypes.func,
|
||||
timezone: propTypes.string
|
||||
})
|
||||
export default class Scheduler extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
this.cron = {
|
||||
minute: '*',
|
||||
hour: '*',
|
||||
monthDay: '*',
|
||||
month: '*',
|
||||
weekDay: '*'
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { cron } = this
|
||||
return `${cron.minute} ${cron.hour} ${cron.monthDay} ${cron.month} ${cron.weekDay}`
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
if (!value) {
|
||||
value = '* * * * *'
|
||||
}
|
||||
|
||||
forEach(value.split(' '), (t, id) => {
|
||||
const ref = this.refs[ID_TO_PICKTIME[id]]
|
||||
|
||||
if (t === '*') {
|
||||
ref.value = 'all'
|
||||
} else if (t.indexOf('/') === 1) {
|
||||
ref.value = t.split('/')[1]
|
||||
} else {
|
||||
ref.value = map(t.split(','), e => +e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
_update (type, value) {
|
||||
const { cron } = this
|
||||
const { onChange } = this.props
|
||||
|
||||
if (value === 'all') {
|
||||
cron[type] = '*'
|
||||
} else if (Array.isArray(value)) {
|
||||
if (Array.isArray(value)) {
|
||||
if (!value.length) {
|
||||
cron[type] = '*'
|
||||
value = '*'
|
||||
} else {
|
||||
cron[type] = join(
|
||||
value = join(
|
||||
(type === 'monthDay' || type === 'month')
|
||||
? map(value, (n) => n + 1)
|
||||
? map(value, n => n + 1)
|
||||
: value,
|
||||
','
|
||||
)
|
||||
}
|
||||
} else {
|
||||
cron[type] = `*/${value}`
|
||||
value = `*/${value}`
|
||||
}
|
||||
|
||||
if (onChange) {
|
||||
onChange(this.value)
|
||||
}
|
||||
const { props } = this
|
||||
const cronPattern = props.cronPattern.split(' ')
|
||||
cronPattern[PICKTIME_TO_ID[type]] = value
|
||||
|
||||
this.props.onChange({
|
||||
cronPattern: cronPattern.join(' '),
|
||||
timezone: props.timezone
|
||||
})
|
||||
}
|
||||
|
||||
_onHourChange = value => this._update('hour', value)
|
||||
@@ -443,49 +420,71 @@ export default class Scheduler extends Component {
|
||||
_onMonthDayChange = value => this._update('monthDay', value)
|
||||
_onWeekDayChange = value => this._update('weekDay', value)
|
||||
|
||||
_onTimezoneChange = timezone => {
|
||||
const { props } = this
|
||||
props.onChange({
|
||||
cronPattern: props.cronPattern,
|
||||
timezone
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
cronPattern,
|
||||
timezone
|
||||
} = this.props
|
||||
const cronPatternArr = cronPattern.split(' ')
|
||||
|
||||
return (
|
||||
<div className='card-block'>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<TimePicker
|
||||
ref='month'
|
||||
type='Month'
|
||||
dataRender={getMonthName}
|
||||
data={MONTHS}
|
||||
labelId='Month'
|
||||
optionsRenderer={getMonthName}
|
||||
options={MONTHS}
|
||||
onChange={this._onMonthChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['month']]}
|
||||
valueRenderer={decrement}
|
||||
/>
|
||||
<TimePicker
|
||||
ref='monthDay'
|
||||
type='MonthDay'
|
||||
data={DAYS}
|
||||
labelId='MonthDay'
|
||||
options={DAYS}
|
||||
onChange={this._onMonthDayChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
|
||||
valueRenderer={decrement}
|
||||
/>
|
||||
<TimePicker
|
||||
ref='weekDay'
|
||||
type='WeekDay'
|
||||
dataRender={getDayName}
|
||||
data={WEEK_DAYS}
|
||||
labelId='WeekDay'
|
||||
optionsRenderer={getDayName}
|
||||
options={WEEK_DAYS}
|
||||
onChange={this._onWeekDayChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['weekDay']]}
|
||||
/>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<TimePicker
|
||||
ref='hour'
|
||||
type='Hour'
|
||||
data={HOURS}
|
||||
range={[2, 12]}
|
||||
labelId='Hour'
|
||||
options={HOURS}
|
||||
range={HOURS_RANGE}
|
||||
onChange={this._onHourChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['hour']]}
|
||||
/>
|
||||
<TimePicker
|
||||
ref='minute'
|
||||
type='Minute'
|
||||
data={MINS}
|
||||
range={[2, 30]}
|
||||
labelId='Minute'
|
||||
options={MINS}
|
||||
range={MINUTES_RANGE}
|
||||
onChange={this._onMinuteChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['minute']]}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<hr />
|
||||
<TimezonePicker value={timezone} onChange={this._onTimezoneChange} />
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,12 +2,17 @@ import React from 'react'
|
||||
import assign from 'lodash/assign'
|
||||
import classNames from 'classnames'
|
||||
import filter from 'lodash/filter'
|
||||
import flatten from 'lodash/flatten'
|
||||
import forEach from 'lodash/forEach'
|
||||
import groupBy from 'lodash/groupBy'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import keyBy from 'lodash/keyBy'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import pick from 'lodash/pick'
|
||||
import sortBy from 'lodash/sortBy'
|
||||
import store from 'store'
|
||||
import { parse as parseRemote } from 'xo-remote-parser'
|
||||
|
||||
import _ from './intl'
|
||||
@@ -19,16 +24,21 @@ import {
|
||||
createFilter,
|
||||
createGetObjectsOfType,
|
||||
createGetTags,
|
||||
createSelector
|
||||
createSelector,
|
||||
getObject
|
||||
} from './selectors'
|
||||
import {
|
||||
connectStore,
|
||||
mapPlus
|
||||
mapPlus,
|
||||
resolveResourceSets
|
||||
} from './utils'
|
||||
import {
|
||||
isSrWritable,
|
||||
subscribeCurrentUser,
|
||||
subscribeGroups,
|
||||
subscribeIpPools,
|
||||
subscribeRemotes,
|
||||
subscribeResourceSets,
|
||||
subscribeRoles,
|
||||
subscribeUsers
|
||||
} from './xo'
|
||||
@@ -45,6 +55,32 @@ const getLabel = object =>
|
||||
|
||||
// ===================================================================
|
||||
|
||||
/*
|
||||
* WITHOUT xoContainers :
|
||||
*
|
||||
* xoObjects: [
|
||||
* { type: 'myType', id: 'abc', label: 'First object' },
|
||||
* { type: 'myType', id: 'def', label: 'Second object' }
|
||||
* ]
|
||||
*
|
||||
*
|
||||
* WITH xoContainers :
|
||||
*
|
||||
* xoContainers: [
|
||||
* { type: 'containerType', id: 'ghi', label: 'First container' },
|
||||
* { type: 'containerType', id: 'jkl', label: 'Second container' }
|
||||
* ]
|
||||
*
|
||||
* xoObjects: {
|
||||
* ghi: [
|
||||
* { type: 'objectType', id: 'mno', label: 'First object' }
|
||||
* { type: 'objectType', id: 'pqr', label: 'Second object' }
|
||||
* ],
|
||||
* jkl: [
|
||||
* { type: 'objectType', id: 'stu', label: 'Third object' }
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
@propTypes({
|
||||
autoFocus: propTypes.bool,
|
||||
clearable: propTypes.bool,
|
||||
@@ -77,7 +113,7 @@ export class GenericSelect extends Component {
|
||||
// Returns the values of the selected objects
|
||||
// if they are contained in xoObjectsById.
|
||||
return mapPlus(value, (value, push) => {
|
||||
const o = xoObjectsById[value.value || value]
|
||||
const o = xoObjectsById[value.value !== undefined ? value.value : value]
|
||||
|
||||
if (o) {
|
||||
push(o)
|
||||
@@ -91,11 +127,11 @@ export class GenericSelect extends Component {
|
||||
// Supports id strings and objects.
|
||||
_setValue (value, props = this.props) {
|
||||
if (props.multi) {
|
||||
return map(value, object => object.id || object)
|
||||
return map(value, object => object.id !== undefined ? object.id : object)
|
||||
}
|
||||
|
||||
return (value != null)
|
||||
? value.id || value
|
||||
? value.id !== undefined ? value.id : value
|
||||
: ''
|
||||
}
|
||||
|
||||
@@ -197,7 +233,7 @@ export class GenericSelect extends Component {
|
||||
|
||||
this.setState({
|
||||
value: this._setValue(value)
|
||||
}, onChange && (() => { onChange(this.value) }))
|
||||
}, onChange && (() => onChange(this.value)))
|
||||
}
|
||||
|
||||
// GroupBy: Display option with margin if not disabled and containers exists.
|
||||
@@ -262,13 +298,28 @@ const makeSubscriptionSelect = (subscribe, props) => (
|
||||
class extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
xoObjects: []
|
||||
}
|
||||
|
||||
this._getFilteredXoObjects = createFilter(
|
||||
this._getFilteredXoContainers = createFilter(
|
||||
() => this.state.xoContainers,
|
||||
() => this.props.containerPredicate
|
||||
)
|
||||
|
||||
this._getFilteredXoObjects = createSelector(
|
||||
() => this.state.xoObjects,
|
||||
() => this.props.predicate
|
||||
() => this.state.xoContainers && this._getFilteredXoContainers(),
|
||||
() => this.props.predicate,
|
||||
(xoObjects, xoContainers, predicate) => {
|
||||
if (xoContainers == null) {
|
||||
return filter(xoObjects, predicate)
|
||||
} else {
|
||||
// Filter xoObjects with `predicate`...
|
||||
const filteredObjects = mapValues(xoObjects, xoObjectsGroup =>
|
||||
filter(xoObjectsGroup, predicate)
|
||||
)
|
||||
// ...and keep only those whose xoContainer hasn't been filtered out
|
||||
return pick(filteredObjects, map(xoContainers, container => container.id))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -291,7 +342,7 @@ const makeSubscriptionSelect = (subscribe, props) => (
|
||||
{...props}
|
||||
{...this.props}
|
||||
xoObjects={this._getFilteredXoObjects()}
|
||||
xoContainers={this.state.xoContainers}
|
||||
xoContainers={this.state.xoContainers && this._getFilteredXoContainers()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -414,7 +465,7 @@ export const SelectVmTemplate = makeStoreSelect(() => {
|
||||
xoObjects: getVmTemplatesByPool,
|
||||
xoContainers: getPools
|
||||
}
|
||||
}, { placeholder: _('selectVms') })
|
||||
}, { placeholder: _('selectVmTemplates') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -580,3 +631,244 @@ export const SelectRemote = makeSubscriptionSelect(subscriber => {
|
||||
|
||||
return unsubscribeRemotes
|
||||
}, { placeholder: _('selectRemotes') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectResourceSet = makeSubscriptionSelect(subscriber => {
|
||||
const unsubscribeResourceSets = subscribeResourceSets(resourceSets => {
|
||||
const xoObjects = map(sortBy(resolveResourceSets(resourceSets), 'name'), resourceSet => ({...resourceSet, type: 'resourceSet'}))
|
||||
|
||||
subscriber({xoObjects})
|
||||
})
|
||||
|
||||
return unsubscribeResourceSets
|
||||
}, { placeholder: _('selectResourceSets') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class SelectResourceSetsVmTemplate extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||
this.setState({
|
||||
resourceSets: resolveResourceSets(resourceSets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getTemplates = createSelector(
|
||||
() => this.props.resourceSet,
|
||||
({ objectsByType }) => {
|
||||
const { predicate } = this.props
|
||||
const templates = objectsByType['VM-template']
|
||||
return sortBy(predicate ? filter(templates, predicate) : templates, 'name_label')
|
||||
}
|
||||
)
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
placeholder={_('selectResourceSetsVmTemplate')}
|
||||
{...this.props}
|
||||
xoObjects={this._getTemplates()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class SelectResourceSetsSr extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||
this.setState({
|
||||
resourceSets: resolveResourceSets(resourceSets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getSrs = createSelector(
|
||||
() => this.props.resourceSet,
|
||||
({ objectsByType }) => {
|
||||
const { predicate } = this.props
|
||||
const srs = objectsByType['SR']
|
||||
return sortBy(predicate ? filter(srs, predicate) : srs, 'name_label')
|
||||
}
|
||||
)
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
placeholder={_('selectResourceSetsSr')}
|
||||
{...this.props}
|
||||
xoObjects={this._getSrs()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class SelectResourceSetsVdi extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||
this.setState({
|
||||
resourceSets: resolveResourceSets(resourceSets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getObject (id) {
|
||||
return getObject(store.getState(), id, true)
|
||||
}
|
||||
|
||||
_getSrs = createSelector(
|
||||
() => this.props.resourceSet,
|
||||
({ objectsByType }) => {
|
||||
const { srPredicate } = this.props
|
||||
const srs = objectsByType['SR']
|
||||
return srPredicate ? filter(srs, srPredicate) : srs
|
||||
}
|
||||
)
|
||||
|
||||
_getVdis = createSelector(
|
||||
this._getSrs,
|
||||
srs => sortBy(map(flatten(map(srs, sr => sr.VDIs)), this._getObject), 'name_label')
|
||||
)
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
placeholder={_('selectResourceSetsVdi')}
|
||||
{...this.props}
|
||||
xoObjects={this._getVdis()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class SelectResourceSetsNetwork extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||
this.setState({
|
||||
resourceSets: resolveResourceSets(resourceSets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getNetworks = createSelector(
|
||||
() => this.props.resourceSet,
|
||||
({ objectsByType }) => {
|
||||
const { predicate } = this.props
|
||||
const networks = objectsByType['network']
|
||||
return sortBy(predicate ? filter(networks, predicate) : networks, 'name_label')
|
||||
}
|
||||
)
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
placeholder={_('selectResourceSetsNetwork')}
|
||||
{...this.props}
|
||||
xoObjects={this._getNetworks()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class SelectSshKey extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeCurrentUser(user => {
|
||||
this.setState({
|
||||
sshKeys: user && user.preferences && map(user.preferences.sshKeys, (key, id) => ({
|
||||
id,
|
||||
label: key.title,
|
||||
type: 'sshKey'
|
||||
}))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
placeholder={_('selectSshKey')}
|
||||
{...this.props}
|
||||
xoObjects={this.state.sshKeys || []}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectIp = makeSubscriptionSelect(subscriber => {
|
||||
const unsubscribeIpPools = subscribeIpPools(ipPools => {
|
||||
const sortedIpPools = sortBy(ipPools, 'name')
|
||||
const xoObjects = mapValues(
|
||||
groupBy(sortedIpPools, 'id'),
|
||||
ipPools => map(ipPools[0].addresses, (address, ip) => ({
|
||||
...address,
|
||||
id: ip,
|
||||
label: ip,
|
||||
type: 'ipAddress',
|
||||
used: !isEmpty(address.vifs)
|
||||
}))
|
||||
)
|
||||
const xoContainers = map(sortedIpPools, ipPool => ({
|
||||
...ipPool,
|
||||
type: 'ipPool'
|
||||
}))
|
||||
subscriber({ xoObjects, xoContainers })
|
||||
})
|
||||
|
||||
return unsubscribeIpPools
|
||||
}, { placeholder: _('selectIp') })
|
||||
|
||||
@@ -7,6 +7,7 @@ import isArray from 'lodash/isArray'
|
||||
import isArrayLike from 'lodash/isArrayLike'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import orderBy from 'lodash/orderBy'
|
||||
import pickBy from 'lodash/pickBy'
|
||||
import size from 'lodash/size'
|
||||
@@ -212,6 +213,8 @@ const _getId = (state, { routeParams, id }) => routeParams
|
||||
|
||||
export const getLang = state => state.lang
|
||||
|
||||
export const getStatus = state => state.status
|
||||
|
||||
export const getUser = state => state.user
|
||||
|
||||
const _getPermissionsPredicate = invoke(() => {
|
||||
@@ -240,18 +243,29 @@ const _getPermissionsPredicate = invoke(() => {
|
||||
}
|
||||
})
|
||||
|
||||
export const isAdmin = (...args) => {
|
||||
const user = getUser(...args)
|
||||
|
||||
return user && user.permission === 'admin'
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// Common selector creators.
|
||||
|
||||
// Creates an object selector from an id selector.
|
||||
export const createGetObject = (idSelector = _getId) =>
|
||||
(state, props) => {
|
||||
(state, props, useResourceSet) => {
|
||||
const object = state.objects.all[idSelector(state, props)]
|
||||
if (!object) {
|
||||
return
|
||||
}
|
||||
|
||||
if (useResourceSet) {
|
||||
return object
|
||||
}
|
||||
|
||||
const predicate = _getPermissionsPredicate(state)
|
||||
|
||||
if (!predicate) {
|
||||
if (predicate == null) {
|
||||
return object // no filtering
|
||||
@@ -315,7 +329,11 @@ const _extendCollectionSelector = (selector, objectsType) => {
|
||||
return selector
|
||||
}
|
||||
_addGroupBy(selector)
|
||||
selector.find = predicate => createFinder(selector, predicate)
|
||||
const _addFind = selector => {
|
||||
selector.find = predicate => createFinder(selector, predicate)
|
||||
return selector
|
||||
}
|
||||
_addFind(selector)
|
||||
|
||||
// groupBy can be chained.
|
||||
const _addSort = selector => {
|
||||
@@ -335,9 +353,9 @@ const _extendCollectionSelector = (selector, objectsType) => {
|
||||
_addFilter(selector)
|
||||
|
||||
// filter, groupBy and sort can be chained.
|
||||
selector.pick = idsSelector => _addFilter(_addGroupBy(_addSort(
|
||||
selector.pick = idsSelector => _addFind(_addFilter(_addGroupBy(_addSort(
|
||||
createPicker(selector, idsSelector)
|
||||
)))
|
||||
))))
|
||||
|
||||
return selector
|
||||
}
|
||||
@@ -355,7 +373,7 @@ const _extendCollectionSelector = (selector, objectsType) => {
|
||||
// - groupBy: returns a selector which returns the objects grouped by
|
||||
// a value determined by a getter selector
|
||||
// - pick: returns a selector which returns only the objects with given
|
||||
// ids (filter, groupBy and sort can be chained)
|
||||
// ids (filter, find, groupBy and sort can be chained)
|
||||
// - sort: returns a selector which returns the objects appropriately
|
||||
// sorted (groupBy can be chained)
|
||||
export const createGetObjectsOfType = type => {
|
||||
@@ -409,6 +427,32 @@ export const createGetObjectMessages = objectSelector =>
|
||||
// ...
|
||||
export const getObject = createGetObject((_, id) => id)
|
||||
|
||||
export const createDoesHostNeedRestart = hostSelector => {
|
||||
// Returns the first patch of the host which requires it to be
|
||||
// restarted.
|
||||
const restartPoolPatch = createGetObjectsOfType('pool_patch').pick(
|
||||
create(
|
||||
createGetObjectsOfType('host_patch').pick(
|
||||
(state, props) => {
|
||||
const host = hostSelector(state, props)
|
||||
return host && host.patches
|
||||
}
|
||||
).filter(create(
|
||||
(state, props) => {
|
||||
const host = hostSelector(state, props)
|
||||
return host && host.startTime
|
||||
},
|
||||
startTime => patch => patch.time > startTime
|
||||
)),
|
||||
hostPatches => map(hostPatches, hostPatch => hostPatch.pool_patch)
|
||||
)
|
||||
).find([ ({ guidance }) => find(guidance, action =>
|
||||
action === 'restartHost' || action === 'restartXapi'
|
||||
) ])
|
||||
|
||||
return (state, props) => restartPoolPatch(state, props) !== undefined
|
||||
}
|
||||
|
||||
export const createGetHostMetrics = hostSelector => _createCollectionWrapper(
|
||||
create(
|
||||
hostSelector,
|
||||
|
||||
35
src/common/shortcuts.js
Normal file
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,14 +1,21 @@
|
||||
import React from 'react'
|
||||
import _ from 'intl'
|
||||
import ceil from 'lodash/ceil'
|
||||
import debounce from 'lodash/debounce'
|
||||
import findIndex from 'lodash/findIndex'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import map from 'lodash/map'
|
||||
import { Pagination } from 'react-bootstrap-4/lib'
|
||||
import React from 'react'
|
||||
import { Dropdown, MenuItem, Pagination } from 'react-bootstrap-4/lib'
|
||||
import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabricator.babeljs.io/T6662 so Dropdown.Menu won't work like https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
|
||||
import DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
|
||||
import { Portal } from 'react-overlays'
|
||||
|
||||
import Component from '../base-component'
|
||||
import Icon from '../icon'
|
||||
import propTypes from '../prop-types'
|
||||
import SingleLineRow from '../single-line-row'
|
||||
import { BlockLink } from '../link'
|
||||
import { Container, Col } from '../grid'
|
||||
import { create as createMatcher } from '../complex-matcher'
|
||||
import {
|
||||
@@ -24,16 +31,19 @@ import styles from './index.css'
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
filters: propTypes.object,
|
||||
nFilteredItems: propTypes.number.isRequired,
|
||||
nItems: propTypes.number.isRequired,
|
||||
onChange: propTypes.func.isRequired
|
||||
})
|
||||
class TableFilter extends Component {
|
||||
_cleanFilter = () => {
|
||||
_cleanFilter = () => this._setFilter('')
|
||||
|
||||
_setFilter = filterValue => {
|
||||
const { filter } = this.refs
|
||||
filter.value = ''
|
||||
filter.value = filterValue
|
||||
filter.focus()
|
||||
this.props.onChange('')
|
||||
this.props.onChange(filterValue)
|
||||
}
|
||||
|
||||
_onChange = event => {
|
||||
@@ -46,7 +56,22 @@ class TableFilter extends Component {
|
||||
return (
|
||||
<div className='input-group'>
|
||||
<span className='input-group-addon'>{props.nFilteredItems} / {props.nItems}</span>
|
||||
<span className='input-group-addon'><Icon icon='search' /></span>
|
||||
{isEmpty(props.filters)
|
||||
? <span className='input-group-addon'><Icon icon='search' /></span>
|
||||
: <div className='input-group-btn'>
|
||||
<Dropdown id='filter'>
|
||||
<DropdownToggle bsStyle='info'>
|
||||
<Icon icon='search' />
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
{map(props.filters, (filter, label) =>
|
||||
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
|
||||
{_(label)}
|
||||
</MenuItem>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>}
|
||||
<input
|
||||
type='text'
|
||||
ref='filter'
|
||||
@@ -115,6 +140,7 @@ const DEFAULT_ITEMS_PER_PAGE = 10
|
||||
propTypes.object
|
||||
]).isRequired,
|
||||
columns: propTypes.arrayOf(propTypes.shape({
|
||||
default: propTypes.bool,
|
||||
name: propTypes.node.isRequired,
|
||||
itemRenderer: propTypes.func.isRequired,
|
||||
sortCriteria: propTypes.oneOfType([
|
||||
@@ -124,16 +150,30 @@ const DEFAULT_ITEMS_PER_PAGE = 10
|
||||
sortOrder: propTypes.string
|
||||
})).isRequired,
|
||||
filterContainer: propTypes.func,
|
||||
filters: propTypes.object,
|
||||
itemsPerPage: propTypes.number,
|
||||
paginationContainer: propTypes.func,
|
||||
rowLink: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
propTypes.string
|
||||
]),
|
||||
userData: propTypes.any
|
||||
})
|
||||
export default class SortedTable extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
let selectedColumn = props.defaultColumn
|
||||
if (selectedColumn == null) {
|
||||
selectedColumn = findIndex(props.columns, 'default')
|
||||
|
||||
if (selectedColumn === -1) {
|
||||
selectedColumn = 0
|
||||
}
|
||||
}
|
||||
|
||||
this.state = {
|
||||
selectedColumn: props.defaultColumn || 0,
|
||||
selectedColumn,
|
||||
itemsPerPage: props.itemsPerPage || DEFAULT_ITEMS_PER_PAGE
|
||||
}
|
||||
|
||||
@@ -173,7 +213,9 @@ export default class SortedTable extends Component {
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this._sort(this.state.selectedColumn)
|
||||
this.setState({
|
||||
sortOrder: this.props.columns[this.state.selectedColumn].sortOrder === 'desc' ? 'desc' : 'asc'
|
||||
})
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
@@ -220,6 +262,8 @@ export default class SortedTable extends Component {
|
||||
const {
|
||||
paginationContainer,
|
||||
filterContainer,
|
||||
filters,
|
||||
rowLink,
|
||||
userData
|
||||
} = props
|
||||
|
||||
@@ -242,6 +286,7 @@ export default class SortedTable extends Component {
|
||||
|
||||
const filterInstance = (
|
||||
<TableFilter
|
||||
filters={filters}
|
||||
nFilteredItems={nFilteredItems}
|
||||
nItems={this._getTotalNumberOfItems()}
|
||||
onChange={this._onFilterChange}
|
||||
@@ -265,15 +310,23 @@ export default class SortedTable extends Component {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(this._getVisibleItems(), (item, key) => (
|
||||
<tr key={key}>
|
||||
{map(props.columns, (column, key) => (
|
||||
<td key={key}>
|
||||
{column.itemRenderer(item, userData)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
{map(this._getVisibleItems(), (item, i) => {
|
||||
const columns = map(props.columns, (column, key) => (
|
||||
<td key={key}>
|
||||
{column.itemRenderer(item, userData)}
|
||||
</td>
|
||||
))
|
||||
|
||||
const { id = i } = item
|
||||
|
||||
return rowLink
|
||||
? <BlockLink
|
||||
key={id}
|
||||
tagName='tr'
|
||||
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
|
||||
>{columns}</BlockLink>
|
||||
: <tr key={id}>{columns}</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{(!paginationContainer || !filterContainer) && (
|
||||
|
||||
@@ -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>
|
||||
: []
|
||||
|
||||
106
src/common/timezone-picker.js
Normal file
106
src/common/timezone-picker.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import ActionButton from 'action-button'
|
||||
import map from 'lodash/map'
|
||||
import moment from 'moment-timezone'
|
||||
import React from 'react'
|
||||
|
||||
import _ from './intl'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import { getXoServerTimezone } from './xo'
|
||||
import { Select } from './form'
|
||||
|
||||
const XO_SERVER_TIMEZONE = 'xo-server'
|
||||
|
||||
@propTypes({
|
||||
defaultValue: propTypes.string,
|
||||
onChange: propTypes.func.isRequired,
|
||||
value: propTypes.string
|
||||
})
|
||||
export default class TimezonePicker extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state.options = map(moment.tz.names(), value => ({ label: value, value }))
|
||||
}
|
||||
|
||||
get value () {
|
||||
const value = this.refs.select.value
|
||||
return (value === XO_SERVER_TIMEZONE) ? null : value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value || XO_SERVER_TIMEZONE
|
||||
}
|
||||
|
||||
_updateTimezone (value) {
|
||||
this.props.onChange(value)
|
||||
}
|
||||
_handleChange = option => {
|
||||
return this._updateTimezone(
|
||||
!option || option.value === XO_SERVER_TIMEZONE
|
||||
? null
|
||||
: option.value
|
||||
)
|
||||
}
|
||||
_useServerTime = () => {
|
||||
this._updateTimezone(null)
|
||||
}
|
||||
_useLocalTime = () => {
|
||||
this._updateTimezone(moment.tz.guess())
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
// Use local timezone (Web browser) if no default value.
|
||||
if (this.props.value === undefined) {
|
||||
this._useLocalTime()
|
||||
}
|
||||
|
||||
getXoServerTimezone.then(serverTimezone => {
|
||||
this.setState({
|
||||
options: [{
|
||||
label: _('serverTimezoneOption', {
|
||||
value: serverTimezone
|
||||
}),
|
||||
value: XO_SERVER_TIMEZONE
|
||||
}].concat(this.state.options),
|
||||
serverTimezone
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
return (
|
||||
<div>
|
||||
<div className='alert alert-info' role='alert'>
|
||||
{_('timezonePickerServerValue')} <strong>{state.serverTimezone}</strong>
|
||||
</div>
|
||||
<Select
|
||||
className='m-b-1'
|
||||
defaultValue={props.defaultValue}
|
||||
onChange={this._handleChange}
|
||||
options={state.options}
|
||||
placeholder={_('selectTimezone')}
|
||||
ref='select'
|
||||
value={props.value || XO_SERVER_TIMEZONE}
|
||||
/>
|
||||
<div className='pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='m-r-1'
|
||||
handler={this._useServerTime}
|
||||
icon='time'
|
||||
>
|
||||
{_('timezonePickerUseServerTime')}
|
||||
</ActionButton>
|
||||
<ActionButton
|
||||
btnStyle='secondary'
|
||||
handler={this._useLocalTime}
|
||||
icon='time'
|
||||
>
|
||||
{_('timezonePickerUseLocalTime')}
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
287
src/common/tooltip/get-position.js
Normal file
287
src/common/tooltip/get-position.js
Normal file
@@ -0,0 +1,287 @@
|
||||
// Source: https://github.com/wwayne/react-tooltip/blob/master/src/utils/getPosition.js
|
||||
|
||||
/**
|
||||
* Calculate the position of tooltip
|
||||
*
|
||||
* @params
|
||||
* - `e` {Event} the event of current mouse
|
||||
* - `target` {Element} the currentTarget of the event
|
||||
* - `node` {DOM} the react-tooltip object
|
||||
* - `place` {String} top / right / bottom / left
|
||||
* - `effect` {String} float / solid
|
||||
* - `offset` {Object} the offset to default position
|
||||
*
|
||||
* @return {Object
|
||||
* - `isNewState` {Bool} required
|
||||
* - `newState` {Object}
|
||||
* - `position` {OBject} {left: {Number}, top: {Number}}
|
||||
*/
|
||||
export default function (e, target, node, place, effect, offset) {
|
||||
const tipWidth = node.clientWidth
|
||||
const tipHeight = node.clientHeight
|
||||
const {mouseX, mouseY} = getCurrentOffset(e, target, effect)
|
||||
const defaultOffset = getDefaultPosition(effect, target.clientWidth, target.clientHeight, tipWidth, tipHeight)
|
||||
const {extraOffsetX, extraOffsetY} = calculateOffset(offset)
|
||||
|
||||
const windowWidth = window.innerWidth
|
||||
const windowHeight = window.innerHeight
|
||||
|
||||
const {parentTop, parentLeft} = getParent(target)
|
||||
|
||||
// Get the edge offset of the tooltip
|
||||
const getTipOffsetLeft = (place) => {
|
||||
const offsetX = defaultOffset[place].l
|
||||
return mouseX + offsetX + extraOffsetX
|
||||
}
|
||||
const getTipOffsetRight = (place) => {
|
||||
const offsetX = defaultOffset[place].r
|
||||
return mouseX + offsetX + extraOffsetX
|
||||
}
|
||||
const getTipOffsetTop = (place) => {
|
||||
const offsetY = defaultOffset[place].t
|
||||
return mouseY + offsetY + extraOffsetY
|
||||
}
|
||||
const getTipOffsetBottom = (place) => {
|
||||
const offsetY = defaultOffset[place].b
|
||||
return mouseY + offsetY + extraOffsetY
|
||||
}
|
||||
|
||||
// Judge if the tooltip has over the window(screen)
|
||||
const outsideVertical = () => {
|
||||
let result = false
|
||||
let newPlace
|
||||
if (getTipOffsetTop('left') < 0 &&
|
||||
getTipOffsetBottom('left') <= windowHeight &&
|
||||
getTipOffsetBottom('bottom') <= windowHeight) {
|
||||
result = true
|
||||
newPlace = 'bottom'
|
||||
} else if (getTipOffsetBottom('left') > windowHeight &&
|
||||
getTipOffsetTop('left') >= 0 &&
|
||||
getTipOffsetTop('top') >= 0) {
|
||||
result = true
|
||||
newPlace = 'top'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
const outsideLeft = () => {
|
||||
let {result, newPlace} = outsideVertical() // Deal with vertical as first priority
|
||||
if (result && outsideHorizontal().result) {
|
||||
return {result: false} // No need to change, if change to vertical will out of space
|
||||
}
|
||||
if (!result && getTipOffsetLeft('left') < 0 && getTipOffsetRight('right') <= windowWidth) {
|
||||
result = true // If vertical ok, but let out of side and right won't out of side
|
||||
newPlace = 'right'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
const outsideRight = () => {
|
||||
let {result, newPlace} = outsideVertical()
|
||||
if (result && outsideHorizontal().result) {
|
||||
return {result: false} // No need to change, if change to vertical will out of space
|
||||
}
|
||||
if (!result && getTipOffsetRight('right') > windowWidth && getTipOffsetLeft('left') >= 0) {
|
||||
result = true
|
||||
newPlace = 'left'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
|
||||
const outsideHorizontal = () => {
|
||||
let result = false
|
||||
let newPlace
|
||||
if (getTipOffsetLeft('top') < 0 &&
|
||||
getTipOffsetRight('top') <= windowWidth &&
|
||||
getTipOffsetRight('right') <= windowWidth) {
|
||||
result = true
|
||||
newPlace = 'right'
|
||||
} else if (getTipOffsetRight('top') > windowWidth &&
|
||||
getTipOffsetLeft('top') >= 0 &&
|
||||
getTipOffsetLeft('left') >= 0) {
|
||||
result = true
|
||||
newPlace = 'left'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
const outsideTop = () => {
|
||||
let {result, newPlace} = outsideHorizontal()
|
||||
if (result && outsideVertical().result) {
|
||||
return {result: false}
|
||||
}
|
||||
if (!result && getTipOffsetTop('top') < 0 && getTipOffsetBottom('bottom') <= windowHeight) {
|
||||
result = true
|
||||
newPlace = 'bottom'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
const outsideBottom = () => {
|
||||
let {result, newPlace} = outsideHorizontal()
|
||||
if (result && outsideVertical().result) {
|
||||
return {result: false}
|
||||
}
|
||||
if (!result && getTipOffsetBottom('bottom') > windowHeight && getTipOffsetTop('top') >= 0) {
|
||||
result = true
|
||||
newPlace = 'top'
|
||||
}
|
||||
return {result, newPlace}
|
||||
}
|
||||
|
||||
// Return new state to change the placement to the reverse if possible
|
||||
const outsideLeftResult = outsideLeft()
|
||||
const outsideRightResult = outsideRight()
|
||||
const outsideTopResult = outsideTop()
|
||||
const outsideBottomResult = outsideBottom()
|
||||
|
||||
if (place === 'left' && outsideLeftResult.result) {
|
||||
return {
|
||||
isNewState: true,
|
||||
newState: {place: outsideLeftResult.newPlace}
|
||||
}
|
||||
} else if (place === 'right' && outsideRightResult.result) {
|
||||
return {
|
||||
isNewState: true,
|
||||
newState: {place: outsideRightResult.newPlace}
|
||||
}
|
||||
} else if (place === 'top' && outsideTopResult.result) {
|
||||
return {
|
||||
isNewState: true,
|
||||
newState: {place: outsideTopResult.newPlace}
|
||||
}
|
||||
} else if (place === 'bottom' && outsideBottomResult.result) {
|
||||
return {
|
||||
isNewState: true,
|
||||
newState: {place: outsideBottomResult.newPlace}
|
||||
}
|
||||
}
|
||||
|
||||
// Return tooltip offset position
|
||||
return {
|
||||
isNewState: false,
|
||||
position: {
|
||||
left: getTipOffsetLeft(place) - parentLeft,
|
||||
top: getTipOffsetTop(place) - parentTop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get current mouse offset
|
||||
const getCurrentOffset = (e, currentTarget, effect) => {
|
||||
const boundingClientRect = currentTarget.getBoundingClientRect()
|
||||
const targetTop = boundingClientRect.top
|
||||
const targetLeft = boundingClientRect.left
|
||||
const targetWidth = currentTarget.clientWidth
|
||||
const targetHeight = currentTarget.clientHeight
|
||||
|
||||
if (effect === 'float') {
|
||||
return {
|
||||
mouseX: e.clientX,
|
||||
mouseY: e.clientY
|
||||
}
|
||||
}
|
||||
return {
|
||||
mouseX: targetLeft + (targetWidth / 2),
|
||||
mouseY: targetTop + (targetHeight / 2)
|
||||
}
|
||||
}
|
||||
|
||||
// List all possibility of tooltip final offset
|
||||
// This is useful in judging if it is necessary for tooltip to switch position when out of window
|
||||
const getDefaultPosition = (effect, targetWidth, targetHeight, tipWidth, tipHeight) => {
|
||||
let top
|
||||
let right
|
||||
let bottom
|
||||
let left
|
||||
const disToMouse = 3
|
||||
const triangleHeight = 2
|
||||
const cursorHeight = 12 // Optimize for float bottom only, cause the cursor will hide the tooltip
|
||||
|
||||
if (effect === 'float') {
|
||||
top = {
|
||||
l: -(tipWidth / 2),
|
||||
r: tipWidth / 2,
|
||||
t: -(tipHeight + disToMouse + triangleHeight),
|
||||
b: -disToMouse
|
||||
}
|
||||
bottom = {
|
||||
l: -(tipWidth / 2),
|
||||
r: tipWidth / 2,
|
||||
t: disToMouse + cursorHeight,
|
||||
b: tipHeight + disToMouse + triangleHeight + cursorHeight
|
||||
}
|
||||
left = {
|
||||
l: -(tipWidth + disToMouse + triangleHeight),
|
||||
r: -disToMouse,
|
||||
t: -(tipHeight / 2),
|
||||
b: tipHeight / 2
|
||||
}
|
||||
right = {
|
||||
l: disToMouse,
|
||||
r: tipWidth + disToMouse + triangleHeight,
|
||||
t: -(tipHeight / 2),
|
||||
b: tipHeight / 2
|
||||
}
|
||||
} else if (effect === 'solid') {
|
||||
top = {
|
||||
l: -(tipWidth / 2),
|
||||
r: tipWidth / 2,
|
||||
t: -(targetHeight / 2 + tipHeight + triangleHeight),
|
||||
b: -(targetHeight / 2)
|
||||
}
|
||||
bottom = {
|
||||
l: -(tipWidth / 2),
|
||||
r: tipWidth / 2,
|
||||
t: targetHeight / 2,
|
||||
b: targetHeight / 2 + tipHeight + triangleHeight
|
||||
}
|
||||
left = {
|
||||
l: -(tipWidth + targetWidth / 2 + triangleHeight),
|
||||
r: -(targetWidth / 2),
|
||||
t: -(tipHeight / 2),
|
||||
b: tipHeight / 2
|
||||
}
|
||||
right = {
|
||||
l: targetWidth / 2,
|
||||
r: tipWidth + targetWidth / 2 + triangleHeight,
|
||||
t: -(tipHeight / 2),
|
||||
b: tipHeight / 2
|
||||
}
|
||||
}
|
||||
|
||||
return {top, bottom, left, right}
|
||||
}
|
||||
|
||||
// Consider additional offset into position calculation
|
||||
const calculateOffset = (offset) => {
|
||||
let extraOffsetX = 0
|
||||
let extraOffsetY = 0
|
||||
|
||||
if (Object.prototype.toString.apply(offset) === '[object String]') {
|
||||
offset = JSON.parse(offset.toString().replace(/'/g, '"'))
|
||||
}
|
||||
for (let key in offset) {
|
||||
if (key === 'top') {
|
||||
extraOffsetY -= parseInt(offset[key], 10)
|
||||
} else if (key === 'bottom') {
|
||||
extraOffsetY += parseInt(offset[key], 10)
|
||||
} else if (key === 'left') {
|
||||
extraOffsetX -= parseInt(offset[key], 10)
|
||||
} else if (key === 'right') {
|
||||
extraOffsetX += parseInt(offset[key], 10)
|
||||
}
|
||||
}
|
||||
|
||||
return {extraOffsetX, extraOffsetY}
|
||||
}
|
||||
|
||||
// Get the offset of the parent elements
|
||||
const getParent = (currentTarget) => {
|
||||
let currentParent = currentTarget
|
||||
while (currentParent) {
|
||||
if (currentParent.style.transform.length > 0) break
|
||||
currentParent = currentParent.parentElement
|
||||
}
|
||||
|
||||
const parentTop = currentParent && currentParent.getBoundingClientRect().top || 0
|
||||
const parentLeft = currentParent && currentParent.getBoundingClientRect().left || 0
|
||||
|
||||
return {parentTop, parentLeft}
|
||||
}
|
||||
@@ -1,45 +1,20 @@
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.common {
|
||||
opacity: 0;
|
||||
transition: opacity .3s;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.container:hover .common {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
composes: common;
|
||||
|
||||
border-bottom: .5em solid rgba(0, 0, 0, .8);
|
||||
border-left: .5em solid transparent;
|
||||
border-right: .5em solid transparent;
|
||||
font-size: 1rem;
|
||||
left: 25%;
|
||||
margin-left: 1em;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
composes: common;
|
||||
|
||||
background: #333;
|
||||
background: rgba(0, 0, 0, .8);
|
||||
border-radius: .25em;
|
||||
.tooltipEnabled {
|
||||
background-color: #222;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $fff;
|
||||
color: #fff;
|
||||
font-size: 1rem;
|
||||
left: 25%;
|
||||
margin-top: .5em;
|
||||
padding: .5em;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
min-width: fit-content;
|
||||
max-width: 20em;
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
margin-left: 0px;
|
||||
margin-top: 0px;
|
||||
opacity: 0.9;
|
||||
padding: 8px 21px;
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
transition: opacity 0.3s ease-out, margin-top 0.3s ease-out, margin-left 0.3s ease-out;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.tooltipDisabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1,30 +1,148 @@
|
||||
import classNames from 'classnames'
|
||||
import React, { PropTypes } from 'react'
|
||||
import isString from 'lodash/isString'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
import Component from '../base-component'
|
||||
import getPosition from './get-position'
|
||||
import propTypes from '../prop-types'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
const Tooltip = ({
|
||||
children,
|
||||
className,
|
||||
content,
|
||||
style,
|
||||
tagName: Component = 'span'
|
||||
}) => (
|
||||
<Component className={classNames(className, styles.container)} style={style}>
|
||||
<div className={styles.arrow} />
|
||||
<div className={styles.tooltip}>
|
||||
{content}
|
||||
</div>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
// ===================================================================
|
||||
|
||||
Tooltip.propTypes = {
|
||||
children: PropTypes.node,
|
||||
className: PropTypes.string,
|
||||
content: PropTypes.any.isRequired,
|
||||
style: PropTypes.object,
|
||||
tagName: PropTypes.string
|
||||
let instance
|
||||
|
||||
export class TooltipViewer extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
if (instance) {
|
||||
throw new Error('Tooltip viewer is a singleton!')
|
||||
}
|
||||
instance = this
|
||||
this.state.place = 'top'
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
className,
|
||||
content,
|
||||
place,
|
||||
show,
|
||||
style
|
||||
} = this.state
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(show ? styles.tooltipEnabled : styles.tooltipDisabled, className)}
|
||||
style={{
|
||||
marginTop: (place === 'top' && '-10px') || (place === 'bottom' && '10px'),
|
||||
marginLeft: (place === 'left' && '-10px') || (place === 'right' && '10px'),
|
||||
...style
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export { Tooltip as default }
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
children: propTypes.oneOfType([
|
||||
propTypes.element,
|
||||
propTypes.string
|
||||
]),
|
||||
className: propTypes.string,
|
||||
content: propTypes.node,
|
||||
style: propTypes.object,
|
||||
tagName: propTypes.string
|
||||
})
|
||||
export default class Tooltip extends Component {
|
||||
componentDidMount () {
|
||||
this._addListeners()
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this._removeListeners()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
if (props.children !== this.props.children) {
|
||||
this._removeListeners()
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.children !== this.props.children) {
|
||||
this._addListeners()
|
||||
}
|
||||
}
|
||||
|
||||
_addListeners () {
|
||||
const node = this._node = ReactDOM.findDOMNode(this)
|
||||
|
||||
node.addEventListener('mouseenter', this._showTooltip)
|
||||
node.addEventListener('mouseleave', this._hideTooltip)
|
||||
node.addEventListener('mousemove', this._updateTooltip)
|
||||
}
|
||||
|
||||
_removeListeners () {
|
||||
const node = this._node
|
||||
this._hideTooltip()
|
||||
|
||||
if (!node) {
|
||||
return
|
||||
}
|
||||
|
||||
node.removeEventListener('mouseenter', this._showTooltip)
|
||||
node.removeEventListener('mouseleave', this._hideTooltip)
|
||||
node.removeEventListener('mousemove', this._updateTooltip)
|
||||
|
||||
this._node = null
|
||||
}
|
||||
|
||||
_showTooltip = () => {
|
||||
const { props } = this
|
||||
|
||||
instance.setState({
|
||||
className: props.className,
|
||||
content: props.content,
|
||||
show: true,
|
||||
style: props.style
|
||||
})
|
||||
}
|
||||
|
||||
_hideTooltip = () => {
|
||||
instance.setState({ show: false })
|
||||
}
|
||||
|
||||
_updateTooltip = event => {
|
||||
const node = ReactDOM.findDOMNode(instance)
|
||||
const result = getPosition(event, event.currentTarget, node, instance.state.place, 'solid', {})
|
||||
|
||||
if (result.isNewState) {
|
||||
return instance.setState(result.newState, () => this._updateTooltip(event))
|
||||
}
|
||||
|
||||
const { position } = result
|
||||
node.style.left = `${position.left}px`
|
||||
node.style.top = `${position.top}px`
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children } = this.props
|
||||
|
||||
if (!children) {
|
||||
return <span />
|
||||
}
|
||||
|
||||
if (isString(children)) {
|
||||
return <span>{children}</span>
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
}
|
||||
|
||||
72
src/common/usage/index.js
Normal file
72
src/common/usage/index.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import _ from 'intl'
|
||||
import classNames from 'classnames'
|
||||
import React, { PropTypes, cloneElement } from 'react'
|
||||
import sum from 'lodash/sum'
|
||||
|
||||
import Tooltip from '../tooltip'
|
||||
|
||||
const Usage = ({ total, children }) => {
|
||||
const limit = total / 400
|
||||
const othersValues = React.Children.map(children, child => {
|
||||
const { value } = child.props
|
||||
return value < limit && value
|
||||
})
|
||||
const othersTotal = sum(othersValues)
|
||||
return <span className='usage'>
|
||||
{React.Children.map(children, (child, index) =>
|
||||
child.props.value > limit && cloneElement(child, { total })
|
||||
)}
|
||||
<Element
|
||||
others
|
||||
tooltip={_('others')}
|
||||
total={total}
|
||||
value={othersTotal}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
Usage.propTypes = {
|
||||
total: PropTypes.number.isRequired
|
||||
}
|
||||
export { Usage as default }
|
||||
|
||||
const Element = ({ highlight, href, others, tooltip, total, value }) => (
|
||||
<Tooltip content={tooltip}>
|
||||
<a
|
||||
href={href}
|
||||
className={classNames(
|
||||
'usage-element',
|
||||
highlight && 'usage-element-highlight',
|
||||
others && 'usage-element-others'
|
||||
)}
|
||||
style={{ width: (value / total) * 100 + '%' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
Element.propTypes = {
|
||||
highlight: PropTypes.bool,
|
||||
href: PropTypes.string,
|
||||
others: PropTypes.bool,
|
||||
tooltip: PropTypes.node,
|
||||
value: PropTypes.number.isRequired
|
||||
}
|
||||
export { Element as UsageElement }
|
||||
|
||||
export const Limits = ({ used, toBeUsed, limit }) => {
|
||||
const available = limit - used
|
||||
|
||||
return <span className='limits'>
|
||||
<span
|
||||
className='limits-used'
|
||||
style={{ width: ((used || 0) / limit) * 100 + '%' }}
|
||||
/>
|
||||
<span
|
||||
className={toBeUsed > available ? 'limits-over-used' : 'limits-to-be-used'}
|
||||
style={{ width: (Math.min((toBeUsed || 0), available) / limit) * 100 + '%' }}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
Limits.propTypes = {
|
||||
used: PropTypes.number,
|
||||
toBeUsed: PropTypes.number,
|
||||
limit: PropTypes.number.isRequired
|
||||
}
|
||||
@@ -1,17 +1,26 @@
|
||||
import * as actions from 'store/actions'
|
||||
import escapeRegExp from 'lodash/escapeRegExp'
|
||||
import every from 'lodash/every'
|
||||
import forEach from 'lodash/forEach'
|
||||
import getStream from 'get-stream'
|
||||
import humanFormat from 'human-format'
|
||||
import isArray from 'lodash/isArray'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isFunction from 'lodash/isFunction'
|
||||
import isPlainObject from 'lodash/isPlainObject'
|
||||
import isString from 'lodash/isString'
|
||||
import join from 'lodash/join'
|
||||
import keys from 'lodash/keys'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import React from 'react'
|
||||
import ReadableStream from 'readable-stream'
|
||||
import replace from 'lodash/replace'
|
||||
import store from 'store'
|
||||
import { connect } from 'react-redux'
|
||||
import { getObject } from 'selectors'
|
||||
|
||||
import _ from './intl'
|
||||
import BaseComponent from './base-component'
|
||||
import invoke from './invoke'
|
||||
|
||||
@@ -174,11 +183,12 @@ export const firstDefined = function () {
|
||||
const n = arguments.length
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const arg = arguments[i]
|
||||
if (arg != null) {
|
||||
if (arg !== undefined) {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Returns the current XOA Plan or the Plan name if number given
|
||||
@@ -203,7 +213,7 @@ export const getXoaPlan = plan => {
|
||||
export const mapPlus = (collection, cb) => {
|
||||
const result = []
|
||||
const push = ::result.push
|
||||
forEach(collection, value => cb(value, push))
|
||||
forEach(collection, (value, index) => cb(value, push, index))
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -216,10 +226,10 @@ export const noop = () => {}
|
||||
export const osFamily = invoke({
|
||||
centos: [ 'centos' ],
|
||||
debian: [ 'debian' ],
|
||||
docker: [ 'coreos' ],
|
||||
fedora: [ 'fedora' ],
|
||||
freebsd: [ 'freebsd' ],
|
||||
gentoo: [ 'gentoo' ],
|
||||
linux: [ 'coreos' ],
|
||||
'linux-mint': [ 'linux-mint' ],
|
||||
netbsd: [ 'netbsd' ],
|
||||
oracle: [ 'oracle' ],
|
||||
@@ -275,7 +285,7 @@ export const normalizeXenToolsStatus = status => {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const _NotFound = () => <h1>Page not found</h1>
|
||||
const _NotFound = () => <h1>{_('errorPageNotFound')}</h1>
|
||||
|
||||
// Decorator to declare routes on a component.
|
||||
//
|
||||
@@ -350,3 +360,102 @@ export function rethrow (cb) {
|
||||
Promise.resolve(cb(error)).then(() => { throw error })
|
||||
)
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const resolveResourceSet = resourceSet => {
|
||||
if (!resourceSet) {
|
||||
return
|
||||
}
|
||||
|
||||
const { objects, ...attrs } = resourceSet
|
||||
const resolvedObjects = {}
|
||||
const resolvedSet = {
|
||||
...attrs,
|
||||
missingObjects: [],
|
||||
objectsByType: resolvedObjects
|
||||
}
|
||||
const state = store.getState()
|
||||
|
||||
forEach(objects, id => {
|
||||
const object = getObject(state, id, true) // true: useResourceSet to bypass permissions
|
||||
|
||||
// Error, missing resource.
|
||||
if (!object) {
|
||||
resolvedSet.missingObjects.push(id)
|
||||
return
|
||||
}
|
||||
|
||||
const { type } = object
|
||||
|
||||
if (!resolvedObjects[type]) {
|
||||
resolvedObjects[type] = [ object ]
|
||||
} else {
|
||||
resolvedObjects[type].push(object)
|
||||
}
|
||||
})
|
||||
|
||||
return resolvedSet
|
||||
}
|
||||
|
||||
export const resolveResourceSets = resourceSets =>
|
||||
map(resourceSets, resolveResourceSet)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Creates a string replacer based on a pattern and a list of rules
|
||||
//
|
||||
// ```js
|
||||
// const myReplacer = buildTemplate('{name}_COPY_{name}_{id}_%', {
|
||||
// '{name}': vm => vm.name_label,
|
||||
// '{id}': vm => vm.id,
|
||||
// '%': (_, i) => i
|
||||
// })
|
||||
//
|
||||
// const newString = myReplacer({
|
||||
// name_label: 'foo',
|
||||
// id: 42,
|
||||
// }, 32)
|
||||
//
|
||||
// newString === 'foo_COPY_foo_42_32'
|
||||
// ```
|
||||
export function buildTemplate (pattern, rules) {
|
||||
const regExp = new RegExp(join(map(keys(rules), escapeRegExp), '|'), 'g')
|
||||
return (...params) => replace(pattern, regExp, match => {
|
||||
const rule = rules[match]
|
||||
return isFunction(rule) ? rule(...params) : rule
|
||||
})
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const streamToString = getStream
|
||||
|
||||
// ===================================================================
|
||||
|
||||
/* global FileReader */
|
||||
|
||||
// Creates a readable stream from a HTML file.
|
||||
export const htmlFileToStream = file => {
|
||||
const reader = new FileReader()
|
||||
const stream = new ReadableStream()
|
||||
let offset = 0
|
||||
|
||||
reader.onloadend = evt => {
|
||||
stream.push(evt.target.result)
|
||||
}
|
||||
reader.onerror = error => {
|
||||
stream.emit('error', error)
|
||||
}
|
||||
|
||||
stream._read = function (size) {
|
||||
if (offset >= file.size) {
|
||||
stream.push(null)
|
||||
} else {
|
||||
reader.readAsBinaryString(file.slice(offset, offset + size))
|
||||
offset += size
|
||||
}
|
||||
}
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import XoRemoteInput from './xo-remote-input'
|
||||
import XoRoleInput from './xo-role-input'
|
||||
import XoSrInput from './xo-sr-input'
|
||||
import XoSubjectInput from './xo-subject-input'
|
||||
import XoTagInput from './xo-tag-input'
|
||||
import XoVmInput from './xo-vm-input'
|
||||
import { getType, getXoType } from '../json-schema-input/helpers'
|
||||
|
||||
@@ -14,13 +15,14 @@ import { getType, getXoType } from '../json-schema-input/helpers'
|
||||
|
||||
const XO_TYPE_TO_COMPONENT = {
|
||||
host: XoHostInput,
|
||||
xoobject: XoHighLevelObjectInput,
|
||||
pool: XoPoolInput,
|
||||
remote: XoRemoteInput,
|
||||
role: XoRoleInput,
|
||||
sr: XoSrInput,
|
||||
subject: XoSubjectInput,
|
||||
vm: XoVmInput
|
||||
tag: XoTagInput,
|
||||
vm: XoVmInput,
|
||||
xoobject: XoHighLevelObjectInput
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
26
src/common/xo-json-schema-input/xo-tag-input.js
Normal file
26
src/common/xo-json-schema-input/xo-tag-input.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from 'react'
|
||||
import { SelectTag } from 'select-objects'
|
||||
|
||||
import XoAbstractInput from './xo-abstract-input'
|
||||
import { PrimitiveInputWrapper } from '../json-schema-input/helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class TagInput extends XoAbstractInput {
|
||||
render () {
|
||||
const { props } = this
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectTag
|
||||
disabled={props.disabled}
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,10 @@ import ChartistLegend from 'chartist-plugin-legend'
|
||||
import ChartistTooltip from 'chartist-plugin-tooltip'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import size from 'lodash/size'
|
||||
import values from 'lodash/values'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import find from 'lodash/find'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
import { computeArraysSum } from '../xo-stats'
|
||||
@@ -16,7 +18,15 @@ import styles from './index.css'
|
||||
const N_LABELS_X = 5
|
||||
|
||||
const LABEL_OFFSET_X = 40
|
||||
const LABEL_OFFSET_Y = 75
|
||||
const LABEL_OFFSET_Y = 85
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// See xo-stats.js, data can be null.
|
||||
// Return the size of the first non-null object.
|
||||
const getStatsLength = stats => size(find(stats, stats => stats != null))
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const makeOptions = ({ intl, nValues, endTimestamp, interval, valueTransform }) => ({
|
||||
showPoint: true,
|
||||
@@ -73,10 +83,15 @@ const buildSeries = ({ stats, label, addSumSeries }) => {
|
||||
for (const io in stats) {
|
||||
const ioData = stats[io]
|
||||
for (const letter in ioData) {
|
||||
series.push({
|
||||
name: `${label}${letter} (${io})`,
|
||||
data: ioData[letter]
|
||||
})
|
||||
const data = ioData[letter]
|
||||
|
||||
// See xo-stats.js, data can be null.
|
||||
if (data) {
|
||||
series.push({
|
||||
name: `${label}${letter} (${io})`,
|
||||
data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (addSumSeries) {
|
||||
@@ -104,7 +119,7 @@ export const CpuLineChart = injectIntl(propTypes({
|
||||
options: propTypes.object
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const stats = data.stats.cpus
|
||||
const { length } = (stats && stats[0]) || {}
|
||||
const length = getStatsLength(stats)
|
||||
|
||||
if (!length) {
|
||||
return templateError
|
||||
@@ -187,7 +202,7 @@ export const XvdLineChart = injectIntl(propTypes({
|
||||
options: propTypes.object
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const stats = data.stats.xvds
|
||||
const { length } = (stats && stats.r.a) || {}
|
||||
const length = stats && getStatsLength(stats.r)
|
||||
|
||||
if (!length) {
|
||||
return templateError
|
||||
@@ -219,7 +234,7 @@ export const VifLineChart = injectIntl(propTypes({
|
||||
options: propTypes.object
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const stats = data.stats.vifs
|
||||
const { length } = (stats && stats.rx[0]) || {}
|
||||
const length = stats && getStatsLength(stats.rx)
|
||||
|
||||
if (!length) {
|
||||
return templateError
|
||||
@@ -251,7 +266,7 @@ export const PifLineChart = injectIntl(propTypes({
|
||||
options: propTypes.object
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const stats = data.stats.pifs
|
||||
const { length } = (stats && stats.rx[0]) || {}
|
||||
const length = stats && getStatsLength(stats.rx)
|
||||
|
||||
if (!length) {
|
||||
return templateError
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React from 'react'
|
||||
import {
|
||||
Sparklines,
|
||||
SparklinesLine,
|
||||
SparklinesSpots
|
||||
SparklinesLine
|
||||
} from 'react-sparklines'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
@@ -14,7 +13,8 @@ import {
|
||||
const STYLE = {}
|
||||
|
||||
const WIDTH = 120
|
||||
const HEIGHT = 40
|
||||
const HEIGHT = 20
|
||||
const STROKE_WIDTH = 0.5
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -27,7 +27,7 @@ const templateError =
|
||||
|
||||
export const CpuSparkLines = propTypes({
|
||||
data: propTypes.object.isRequired
|
||||
})(({ data }) => {
|
||||
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
|
||||
const { cpus } = data.stats
|
||||
|
||||
if (!cpus) {
|
||||
@@ -35,16 +35,15 @@ export const CpuSparkLines = propTypes({
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={computeArraysAvg(cpus)} max={100} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 1, stroke: '#366e98', fill: '#366e98', fillOpacity: 0.5 }} color='#2598d9' />
|
||||
<SparklinesSpots />
|
||||
<Sparklines style={STYLE} data={computeArraysAvg(cpus)} max={100} min={0} width={width} height={height}>
|
||||
<SparklinesLine style={{ strokeWidth, stroke: '#366e98', fill: '#366e98', fillOpacity: 0.5 }} color='#2598d9' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
|
||||
export const MemorySparkLines = propTypes({
|
||||
data: propTypes.object.isRequired
|
||||
})(({ data }) => {
|
||||
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
|
||||
const { memory, memoryUsed } = data.stats
|
||||
|
||||
if (!memory || !memoryUsed) {
|
||||
@@ -52,16 +51,15 @@ export const MemorySparkLines = propTypes({
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={memoryUsed} max={memory[memory.length - 1]} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 1, stroke: '#990822', fill: '#990822', fillOpacity: 0.5 }} color='#cc0066' />
|
||||
<SparklinesSpots />
|
||||
<Sparklines style={STYLE} data={memoryUsed} max={memory[memory.length - 1]} min={0} width={width} height={height}>
|
||||
<SparklinesLine style={{ strokeWidth, stroke: '#990822', fill: '#990822', fillOpacity: 0.5 }} color='#cc0066' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
|
||||
export const XvdSparkLines = propTypes({
|
||||
data: propTypes.object.isRequired
|
||||
})(({ data }) => {
|
||||
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
|
||||
const { xvds } = data.stats
|
||||
|
||||
if (!xvds) {
|
||||
@@ -69,16 +67,15 @@ export const XvdSparkLines = propTypes({
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={computeObjectsAvg(xvds)} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 1, stroke: '#089944', fill: '#089944', fillOpacity: 0.5 }} color='#33cc33' />
|
||||
<SparklinesSpots />
|
||||
<Sparklines style={STYLE} data={computeObjectsAvg(xvds)} min={0} width={width} height={height}>
|
||||
<SparklinesLine style={{ strokeWidth, stroke: '#089944', fill: '#089944', fillOpacity: 0.5 }} color='#33cc33' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
|
||||
export const VifSparkLines = propTypes({
|
||||
data: propTypes.object.isRequired
|
||||
})(({ data }) => {
|
||||
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
|
||||
const { vifs } = data.stats
|
||||
|
||||
if (!vifs) {
|
||||
@@ -86,16 +83,15 @@ export const VifSparkLines = propTypes({
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={computeObjectsAvg(vifs)} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 1, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
|
||||
<SparklinesSpots />
|
||||
<Sparklines style={STYLE} data={computeObjectsAvg(vifs)} min={0} width={width} height={height}>
|
||||
<SparklinesLine style={{ strokeWidth, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
|
||||
export const PifSparkLines = propTypes({
|
||||
data: propTypes.object.isRequired
|
||||
})(({ data }) => {
|
||||
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
|
||||
const { pifs } = data.stats
|
||||
|
||||
if (!pifs) {
|
||||
@@ -103,16 +99,15 @@ export const PifSparkLines = propTypes({
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={computeObjectsAvg(pifs)} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 1, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
|
||||
<SparklinesSpots />
|
||||
<Sparklines style={STYLE} data={computeObjectsAvg(pifs)} min={0} width={width} height={height}>
|
||||
<SparklinesLine style={{ strokeWidth, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
|
||||
export const LoadSparkLines = propTypes({
|
||||
data: propTypes.object.isRequired
|
||||
})(({ data }) => {
|
||||
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
|
||||
const { load } = data.stats
|
||||
|
||||
if (!load) {
|
||||
@@ -120,9 +115,8 @@ export const LoadSparkLines = propTypes({
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={load} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 1, stroke: '#33cc33', fill: '#33cc33', fillOpacity: 0.5 }} color='#33cc33' />
|
||||
<SparklinesSpots />
|
||||
<Sparklines style={STYLE} data={load} min={0} width={width} height={height}>
|
||||
<SparklinesLine style={{ strokeWidth, stroke: '#33cc33', fill: '#33cc33', fillOpacity: 0.5 }} color='#33cc33' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
import map from 'lodash/map'
|
||||
import values from 'lodash/values'
|
||||
import { mapPlus } from 'utils'
|
||||
|
||||
// Returns a new array with arrays sums.
|
||||
// Example: computeArraysSum([[1, 2], [3, 4], [5, 0]) = [9, 6]
|
||||
export const computeArraysSum = arrays => {
|
||||
const _computeArraysSum = arrays => {
|
||||
if (!arrays || !arrays.length || !arrays[0].length) {
|
||||
return []
|
||||
}
|
||||
@@ -30,9 +31,12 @@ export const computeArraysSum = arrays => {
|
||||
|
||||
// Returns a new array with arrays avgs.
|
||||
// Example: computeArraysAvg([[1, 2], [3, 4], [5, 0]) = [4.5, 2]
|
||||
export const computeArraysAvg = arrays => {
|
||||
const sums = computeArraysSum(arrays)
|
||||
const _computeArraysAvg = arrays => {
|
||||
const sums = _computeArraysSum(arrays)
|
||||
|
||||
if (!arrays[0]) {
|
||||
return []
|
||||
}
|
||||
const n = arrays && arrays[0].length
|
||||
const m = arrays.length
|
||||
|
||||
@@ -43,6 +47,20 @@ export const computeArraysAvg = arrays => {
|
||||
return sums
|
||||
}
|
||||
|
||||
// Arrays can be null.
|
||||
// See: https://github.com/vatesfr/xo-web/issues/969
|
||||
//
|
||||
// It's a fix to avoid error like `Uncaught TypeError: Cannot read property 'length' of null`.
|
||||
// FIXME: Repare this bug in xo-server. (Warning: Can break the stats of xo-web v4.)
|
||||
const removeUndefinedArrays = arrays => mapPlus(arrays, (array, push) => {
|
||||
if (array != null) {
|
||||
push(array)
|
||||
}
|
||||
})
|
||||
|
||||
export const computeArraysSum = arrays => _computeArraysSum(removeUndefinedArrays(arrays))
|
||||
export const computeArraysAvg = arrays => _computeArraysAvg(removeUndefinedArrays(arrays))
|
||||
|
||||
// More complex than computeArraysAvg.
|
||||
//
|
||||
// Take in parameter one object like:
|
||||
@@ -53,7 +71,7 @@ export const computeArraysAvg = arrays => {
|
||||
//
|
||||
// Note: The parameter can be also an 3D array.
|
||||
export const computeObjectsAvg = objects => {
|
||||
return computeArraysAvg(
|
||||
return _computeArraysAvg(
|
||||
map(objects, object =>
|
||||
computeArraysAvg(values(object))
|
||||
)
|
||||
|
||||
@@ -165,15 +165,17 @@ export default class XoWeekHeatmap extends Component {
|
||||
<th><FormattedTime value={day.timestamp} {...DAY_TIME_FORMAT} /></th>
|
||||
{map(day.hours, (hour, key) => (
|
||||
<Tooltip
|
||||
className={styles.cell}
|
||||
key={key}
|
||||
style={{ background: hour ? hour.color : '#ffffff' }}
|
||||
tagName='td'
|
||||
content={hour
|
||||
? _('weekHeatmapData', { date: hour.date, value: this.props.cellRenderer(hour.value) })
|
||||
: _('weekHeatmapNoData')
|
||||
}
|
||||
/>
|
||||
key={key}
|
||||
>
|
||||
<td
|
||||
className={styles.cell}
|
||||
style={{ background: hour ? hour.color : '#ffffff' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
|
||||
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>
|
||||
}
|
||||
}
|
||||
61
src/common/xo/add-user-filter-modal/index.js
Normal file
61
src/common/xo/add-user-filter-modal/index.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import keys from 'lodash/keys'
|
||||
import React from 'react'
|
||||
|
||||
import * as FormGrid from '../../form-grid'
|
||||
import _ from '../../intl'
|
||||
import Combobox from '../../combobox'
|
||||
import Component from '../../base-component'
|
||||
import propTypes from '../../prop-types'
|
||||
import { createSelector } from '../../selectors'
|
||||
|
||||
@propTypes({
|
||||
type: propTypes.string.isRequired,
|
||||
user: propTypes.object.isRequired,
|
||||
value: propTypes.string.isRequired
|
||||
})
|
||||
export default class SaveNewUserFilterModalBody extends Component {
|
||||
get value () {
|
||||
return this.state.name || ''
|
||||
}
|
||||
|
||||
_getFilterOptions = createSelector(
|
||||
tmp => (
|
||||
(tmp = this.props.user) &&
|
||||
(tmp = tmp.preferences) &&
|
||||
(tmp = tmp.filters) &&
|
||||
tmp[this.props.type]
|
||||
),
|
||||
keys
|
||||
)
|
||||
|
||||
render () {
|
||||
const { value } = this.props
|
||||
const options = this._getFilterOptions()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('filterName')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<Combobox
|
||||
onChange={this.linkState('name')}
|
||||
options={options}
|
||||
value={this.state.name || ''}
|
||||
/>
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
<FormGrid.Row>
|
||||
<FormGrid.LabelCol>{_('filterValue')}</FormGrid.LabelCol>
|
||||
<FormGrid.InputCol>
|
||||
<input
|
||||
className='form-control'
|
||||
disabled
|
||||
type='text'
|
||||
value={value}
|
||||
/>
|
||||
</FormGrid.InputCol>
|
||||
</FormGrid.Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
103
src/common/xo/copy-vms-modal/index.js
Normal file
103
src/common/xo/copy-vms-modal/index.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import _, { messages } from 'intl'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import { injectIntl } from 'react-intl'
|
||||
|
||||
import BaseComponent from 'base-component'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Col } from 'grid'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import { SelectSr } from 'select-objects'
|
||||
import { Toggle } from 'form'
|
||||
import {
|
||||
buildTemplate,
|
||||
connectStore
|
||||
} from 'utils'
|
||||
|
||||
@connectStore(() => {
|
||||
const getVms = createGetObjectsOfType('VM').pick(
|
||||
(_, props) => props.vms
|
||||
)
|
||||
return {
|
||||
vms: getVms
|
||||
}
|
||||
}, { withRef: true })
|
||||
class CopyVmsModalBody extends BaseComponent {
|
||||
get value () {
|
||||
const { state } = this
|
||||
if (!state || !state.sr) {
|
||||
return {}
|
||||
}
|
||||
const { vms } = this.props
|
||||
const { namePattern } = state
|
||||
|
||||
const names = namePattern
|
||||
? map(vms, buildTemplate(namePattern, {
|
||||
'{name}': vm => vm.name_label,
|
||||
'{id}': vm => vm.id
|
||||
}))
|
||||
: map(vms, vm => vm.name_label)
|
||||
return {
|
||||
compress: state.compress,
|
||||
names,
|
||||
sr: state.sr.id
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.setState({
|
||||
compress: false,
|
||||
namePattern: '{name}_COPY'
|
||||
})
|
||||
}
|
||||
|
||||
_onChangeSr = sr =>
|
||||
this.setState({ sr })
|
||||
_onChangeNamePattern = event =>
|
||||
this.setState({ namePattern: event.target.value })
|
||||
_onChangeCompress = compress =>
|
||||
this.setState({ compress })
|
||||
|
||||
render () {
|
||||
const { formatMessage } = this.props.intl
|
||||
const { compress, namePattern, sr } = this.state
|
||||
return process.env.XOA_PLAN > 2
|
||||
? <div>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('copyVmSelectSr')}</Col>
|
||||
<Col size={6}>
|
||||
<SelectSr
|
||||
onChange={this.linkState('sr')}
|
||||
value={sr}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('copyVmName')}</Col>
|
||||
<Col size={6}>
|
||||
<input
|
||||
className='form-control'
|
||||
onChange={this.linkState('namePattern')}
|
||||
placeholder={formatMessage(messages.copyVmNamePatternPlaceholder)}
|
||||
type='text'
|
||||
value={namePattern}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('copyVmCompress')}</Col>
|
||||
<Col size={6}>
|
||||
<Toggle
|
||||
onChange={this.linkState('compress')}
|
||||
value={compress}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
: <div><Upgrade place='vmCopy' available={3} /></div>
|
||||
}
|
||||
}
|
||||
export default injectIntl(CopyVmsModalBody, { withRef: true })
|
||||
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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
||||
import BaseComponent from 'base-component'
|
||||
import every from 'lodash/every'
|
||||
import forEach from 'lodash/forEach'
|
||||
import find from 'lodash/find'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import React from 'react'
|
||||
import store from 'store'
|
||||
|
||||
import _ from '../../intl'
|
||||
import invoke from '../../invoke'
|
||||
@@ -22,8 +24,12 @@ import {
|
||||
import {
|
||||
createGetObjectsOfType,
|
||||
createPicker,
|
||||
createSelector
|
||||
createSelector,
|
||||
getObject
|
||||
} from '../../selectors'
|
||||
import {
|
||||
isSrShared
|
||||
} from 'xo'
|
||||
|
||||
import { isSrWritable } from '../'
|
||||
|
||||
@@ -59,6 +65,7 @@ import styles from './index.css'
|
||||
networks: getNetworks,
|
||||
pifs: getPifs,
|
||||
pools: getPools,
|
||||
vbds: getVbds,
|
||||
vdis: getVdis,
|
||||
vifs: getVifs
|
||||
}
|
||||
@@ -85,7 +92,26 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
)
|
||||
)
|
||||
|
||||
this._getNetworkPredicate = createSelector(
|
||||
this._getTargetNetworkPredicate = createSelector(
|
||||
createPicker(
|
||||
() => this.props.pifs,
|
||||
() => this.state.host.$PIFs
|
||||
),
|
||||
pifs => {
|
||||
if (!pifs) {
|
||||
return false
|
||||
}
|
||||
|
||||
const networks = {}
|
||||
forEach(pifs, pif => {
|
||||
networks[pif.$network] = true
|
||||
})
|
||||
|
||||
return network => networks[network.id]
|
||||
}
|
||||
)
|
||||
|
||||
this._getMigrationNetworkPredicate = createSelector(
|
||||
createPicker(
|
||||
() => this.props.pifs,
|
||||
() => this.state.host.$PIFs
|
||||
@@ -118,14 +144,53 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
}
|
||||
}
|
||||
|
||||
_getObject (id) {
|
||||
return getObject(store.getState(), id)
|
||||
}
|
||||
|
||||
_selectHost = host => {
|
||||
// No host selected
|
||||
if (!host) {
|
||||
this.setState({ intraPool: undefined, host: undefined })
|
||||
this.setState({
|
||||
host: undefined,
|
||||
intraPool: undefined
|
||||
})
|
||||
return
|
||||
}
|
||||
const { networks, pools, pifs, vdis, vifs } = this.props
|
||||
const defaultMigrationNetworkId = find(pifs, pif => pif.$host === host.id && pif.management).$network
|
||||
|
||||
const { pools, vbds, vdis, vm } = this.props
|
||||
const intraPool = vm.$pool === host.$pool
|
||||
|
||||
// Intra-pool
|
||||
const defaultSr = pools[host.$pool].default_SR
|
||||
if (intraPool) {
|
||||
let doNotMigrateVdis
|
||||
if (vm.$container === host.id) {
|
||||
doNotMigrateVdis = true
|
||||
} else {
|
||||
const _doNotMigrateVdi = {}
|
||||
forEach(vbds, vbd => {
|
||||
if (vbd.VDI != null) {
|
||||
_doNotMigrateVdi[vbd.VDI] = isSrShared(this._getObject(this._getObject(vbd.VDI).$SR))
|
||||
}
|
||||
})
|
||||
doNotMigrateVdis = every(_doNotMigrateVdi)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
doNotMigrateVdis,
|
||||
host,
|
||||
intraPool,
|
||||
mapVdisSrs: doNotMigrateVdis ? undefined : mapValues(vdis, vdi => defaultSr),
|
||||
mapVifsNetworks: undefined,
|
||||
migrationNetwork: undefined
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Inter-pool
|
||||
const { networks, pifs, vifs } = this.props
|
||||
const defaultMigrationNetworkId = find(pifs, pif => pif.$host === host.id && pif.management).$network
|
||||
|
||||
const defaultNetwork = invoke(() => {
|
||||
// First PIF with an IP.
|
||||
@@ -144,8 +209,9 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
})
|
||||
|
||||
this.setState({
|
||||
doNotMigrateVdis: false,
|
||||
host,
|
||||
intraPool: this.props.vm.$pool === host.$pool,
|
||||
intraPool,
|
||||
mapVdisSrs: mapValues(vdis, vdi => defaultSr),
|
||||
mapVifsNetworks: defaultNetworksForVif,
|
||||
migrationNetworkId: defaultMigrationNetworkId
|
||||
@@ -157,6 +223,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
render () {
|
||||
const { vdis, vifs, networks } = this.props
|
||||
const {
|
||||
doNotMigrateVdis,
|
||||
host,
|
||||
intraPool,
|
||||
mapVdisSrs,
|
||||
@@ -176,6 +243,28 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
{host && !doNotMigrateVdis && <div className={styles.groupBlock}>
|
||||
<SingleLineRow>
|
||||
<Col>{_('migrateVmSelectSrs')}</Col>
|
||||
</SingleLineRow>
|
||||
<br />
|
||||
<SingleLineRow>
|
||||
<Col size={6}><span className={styles.listTitle}>{_('migrateVmName')}</span></Col>
|
||||
<Col size={6}><span className={styles.listTitle}>{_('migrateVmSr')}</span></Col>
|
||||
</SingleLineRow>
|
||||
{map(vdis, vdi => <div className={styles.listItem} key={vdi.id}>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{vdi.name_label}</Col>
|
||||
<Col size={6}>
|
||||
<SelectSr
|
||||
onChange={sr => this.setState({ mapVdisSrs: { ...mapVdisSrs, [vdi.id]: sr.id } })}
|
||||
predicate={this._getSrPredicate()}
|
||||
value={mapVdisSrs[vdi.id]}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>)}
|
||||
</div>}
|
||||
{intraPool !== undefined &&
|
||||
(!intraPool &&
|
||||
<div>
|
||||
@@ -185,34 +274,12 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
<Col size={6}>
|
||||
<SelectNetwork
|
||||
onChange={this._selectMigrationNetwork}
|
||||
predicate={this._getNetworkPredicate()}
|
||||
predicate={this._getMigrationNetworkPredicate()}
|
||||
value={migrationNetworkId}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
<div className={styles.groupBlock}>
|
||||
<SingleLineRow>
|
||||
<Col>{_('migrateVmSelectSrs')}</Col>
|
||||
</SingleLineRow>
|
||||
<br />
|
||||
<SingleLineRow>
|
||||
<Col size={6}><span className={styles.listTitle}>{_('migrateVmName')}</span></Col>
|
||||
<Col size={6}><span className={styles.listTitle}>{_('migrateVmSr')}</span></Col>
|
||||
</SingleLineRow>
|
||||
{map(vdis, vdi => <div className={styles.listItem} key={vdi.id}>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{vdi.name_label}</Col>
|
||||
<Col size={6}>
|
||||
<SelectSr
|
||||
onChange={sr => this.setState({ mapVdisSrs: { ...mapVdisSrs, [vdi.id]: sr.id } })}
|
||||
predicate={this._getSrPredicate()}
|
||||
value={mapVdisSrs[vdi.id]}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>)}
|
||||
</div>
|
||||
<div className={styles.groupBlock}>
|
||||
<SingleLineRow>
|
||||
<Col>{_('migrateVmSelectNetworks')}</Col>
|
||||
@@ -228,7 +295,7 @@ export default class MigrateVmModalBody extends BaseComponent {
|
||||
<Col size={6}>
|
||||
<SelectNetwork
|
||||
onChange={network => this.setState({ mapVifsNetworks: { ...mapVifsNetworks, [vif.id]: network.id } })}
|
||||
predicate={this._getNetworkPredicate()}
|
||||
predicate={this._getTargetNetworkPredicate()}
|
||||
value={mapVifsNetworks[vif.id]}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@@ -2,14 +2,16 @@ import BaseComponent from 'base-component'
|
||||
import every from 'lodash/every'
|
||||
import flatten from 'lodash/flatten'
|
||||
import forEach from 'lodash/forEach'
|
||||
import filter from 'lodash/filter'
|
||||
import find from 'lodash/find'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import mapValues from 'lodash/mapValues'
|
||||
import React from 'react'
|
||||
import some from 'lodash/some'
|
||||
import store from 'store'
|
||||
|
||||
import _ from '../../intl'
|
||||
import Icon from 'icon'
|
||||
import invoke from '../../invoke'
|
||||
import SingleLineRow from '../../single-line-row'
|
||||
import { Col } from '../../grid'
|
||||
@@ -23,11 +25,14 @@ import {
|
||||
connectStore
|
||||
} from '../../utils'
|
||||
import {
|
||||
createFinder,
|
||||
createGetObjectsOfType,
|
||||
createPicker,
|
||||
createSelector
|
||||
createSelector,
|
||||
getObject
|
||||
} from '../../selectors'
|
||||
import {
|
||||
isSrShared
|
||||
} from 'xo'
|
||||
|
||||
import { isSrWritable } from '../'
|
||||
|
||||
@@ -39,16 +44,7 @@ const LINE_STYLE = { paddingBottom: '1em' }
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
|
||||
const getVms = createGetObjectsOfType('VM').pick(
|
||||
(_, props) => props.vms
|
||||
).filter(
|
||||
[ vm => vm.power_state === 'Running' ]
|
||||
)
|
||||
|
||||
const getNonRunningVm = createFinder(
|
||||
createGetObjectsOfType('VM').pick(
|
||||
(_, props) => props.vms
|
||||
),
|
||||
[ vm => vm.power_state !== 'Running' ]
|
||||
(_, props) => props.vms
|
||||
)
|
||||
|
||||
const getVbdsByVm = createGetObjectsOfType('VBD').pick(
|
||||
@@ -67,7 +63,6 @@ const LINE_STYLE = { paddingBottom: '1em' }
|
||||
|
||||
return {
|
||||
networks: getNetworks,
|
||||
nonRunningVm: getNonRunningVm,
|
||||
pifs: getPifs,
|
||||
pools: getPools,
|
||||
vbdsByVm: getVbdsByVm,
|
||||
@@ -92,7 +87,26 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
)
|
||||
)
|
||||
|
||||
this._getNetworkPredicate = createSelector(
|
||||
this._getTargetNetworkPredicate = createSelector(
|
||||
createPicker(
|
||||
() => this.props.pifs,
|
||||
() => this.state.host.$PIFs
|
||||
),
|
||||
pifs => {
|
||||
if (!pifs) {
|
||||
return false
|
||||
}
|
||||
|
||||
const networks = {}
|
||||
forEach(pifs, pif => {
|
||||
networks[pif.$network] = true
|
||||
})
|
||||
|
||||
return network => networks[network.id]
|
||||
}
|
||||
)
|
||||
|
||||
this._getMigrationNetworkPredicate = createSelector(
|
||||
createPicker(
|
||||
() => this.props.pifs,
|
||||
() => this.state.host.$PIFs
|
||||
@@ -117,8 +131,8 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { vms } = this.props
|
||||
const { host } = this.state
|
||||
const vms = filter(this.props.vms, vm => vm.$container !== host.id)
|
||||
if (!host || isEmpty(vms)) {
|
||||
return { vms }
|
||||
}
|
||||
@@ -129,6 +143,9 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
vifsByVm
|
||||
} = this.props
|
||||
const {
|
||||
intraPool,
|
||||
doNotMigrateVdi,
|
||||
doNotMigrateVmVdis,
|
||||
migrationNetworkId,
|
||||
networkId,
|
||||
smartVifMapping,
|
||||
@@ -138,10 +155,14 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
// Map VM --> ( Map VDI --> SR )
|
||||
const mapVmsMapVdisSrs = {}
|
||||
forEach(vbdsByVm, (vbds, vm) => {
|
||||
if (doNotMigrateVmVdis[vm]) {
|
||||
return
|
||||
}
|
||||
const mapVdisSrs = {}
|
||||
forEach(vbds, vbd => {
|
||||
if (!vbd.is_cd_drive && vbd.VDI) {
|
||||
mapVdisSrs[vbd.VDI] = srId
|
||||
const vdi = vbd.VDI
|
||||
if (!vbd.is_cd_drive && vdi) {
|
||||
mapVdisSrs[vdi] = intraPool && doNotMigrateVdi[vdi] ? this._getObject(vdi).SR : srId
|
||||
}
|
||||
})
|
||||
mapVmsMapVdisSrs[vm] = mapVdisSrs
|
||||
@@ -158,6 +179,9 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
// Map VM --> ( Map VIF --> network )
|
||||
const mapVmsMapVifsNetworks = {}
|
||||
forEach(vms, vm => {
|
||||
if (vm.$pool === host.$pool) {
|
||||
return
|
||||
}
|
||||
const mapVifsNetworks = {}
|
||||
forEach(vifsByVm[vm.id], vif => {
|
||||
mapVifsNetworks[vif.id] = smartVifMapping
|
||||
@@ -166,15 +190,25 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
})
|
||||
mapVmsMapVifsNetworks[vm.id] = mapVifsNetworks
|
||||
})
|
||||
|
||||
// Map VM --> migration network
|
||||
const mapVmsMigrationNetwork = mapValues(doNotMigrateVmVdis, doNotMigrateVdis =>
|
||||
doNotMigrateVdis ? undefined : migrationNetworkId
|
||||
)
|
||||
|
||||
return {
|
||||
mapVmsMapVdisSrs,
|
||||
mapVmsMapVifsNetworks,
|
||||
migrationNetwork: migrationNetworkId,
|
||||
mapVmsMigrationNetwork,
|
||||
targetHost: host.id,
|
||||
vms
|
||||
}
|
||||
}
|
||||
|
||||
_getObject (id) {
|
||||
return getObject(store.getState(), id)
|
||||
}
|
||||
|
||||
_selectHost = host => {
|
||||
if (!host) {
|
||||
this.setState({ targetHost: undefined })
|
||||
@@ -183,11 +217,30 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
const { pools, pifs } = this.props
|
||||
const defaultMigrationNetworkId = find(pifs, pif => pif.$host === host.id && pif.management).$network
|
||||
const defaultSrId = pools[host.$pool].default_SR
|
||||
const doNotMigrateVmVdis = {}
|
||||
const doNotMigrateVdi = {}
|
||||
forEach(this.props.vbdsByVm, (vbds, vm) => {
|
||||
if (this._getObject(vm).$container === host.id) {
|
||||
doNotMigrateVmVdis[vm] = true
|
||||
return
|
||||
}
|
||||
const _doNotMigrateVdi = {}
|
||||
forEach(vbds, vbd => {
|
||||
if (vbd.VDI != null) {
|
||||
doNotMigrateVdi[vbd.VDI] = _doNotMigrateVdi[vbd.VDI] = isSrShared(this._getObject(this._getObject(vbd.VDI).$SR))
|
||||
}
|
||||
})
|
||||
doNotMigrateVmVdis[vm] = every(_doNotMigrateVdi)
|
||||
})
|
||||
const noVdisMigration = every(doNotMigrateVmVdis)
|
||||
this.setState({
|
||||
host,
|
||||
intraPool: every(this.props.vms, vm => vm.$pool === host.$pool),
|
||||
doNotMigrateVdi,
|
||||
doNotMigrateVmVdis,
|
||||
migrationNetworkId: defaultMigrationNetworkId,
|
||||
networkId: defaultMigrationNetworkId,
|
||||
noVdisMigration,
|
||||
smartVifMapping: true,
|
||||
srId: defaultSrId
|
||||
})
|
||||
@@ -198,19 +251,12 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
_toggleSmartVifMapping = () => this.setState({ smartVifMapping: !this.state.smartVifMapping })
|
||||
|
||||
render () {
|
||||
if (isEmpty(this.props.vms)) {
|
||||
return <div>
|
||||
<Icon icon='error' />
|
||||
{' '}
|
||||
{_('migrateVmBadPowerState')}
|
||||
</div>
|
||||
}
|
||||
const { nonRunningVm } = this.props
|
||||
const {
|
||||
host,
|
||||
intraPool,
|
||||
migrationNetworkId,
|
||||
networkId,
|
||||
noVdisMigration,
|
||||
smartVifMapping,
|
||||
srId
|
||||
} = this.state
|
||||
@@ -234,17 +280,17 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
<Col size={6}>
|
||||
<SelectNetwork
|
||||
onChange={this._selectMigrationNetwork}
|
||||
predicate={this._getNetworkPredicate()}
|
||||
predicate={this._getMigrationNetworkPredicate()}
|
||||
value={migrationNetworkId}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
}
|
||||
{host && [
|
||||
{host && (!intraPool || !noVdisMigration) &&
|
||||
<div key='sr' style={LINE_STYLE}>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('migrateVmsSelectSr')}</Col>
|
||||
<Col size={6}>{!intraPool ? _('migrateVmsSelectSr') : _('migrateVmsSelectSrIntraPool')}</Col>
|
||||
<Col size={6}>
|
||||
<SelectSr
|
||||
onChange={this._selectSr}
|
||||
@@ -253,7 +299,9 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>,
|
||||
</div>
|
||||
}
|
||||
{host && !intraPool &&
|
||||
<div key='network' style={LINE_STYLE}>
|
||||
<SingleLineRow>
|
||||
<Col size={6}>{_('migrateVmsSelectNetwork')}</Col>
|
||||
@@ -261,7 +309,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
<SelectNetwork
|
||||
disabled={smartVifMapping}
|
||||
onChange={this._selectNetwork}
|
||||
predicate={this._getNetworkPredicate()}
|
||||
predicate={this._getTargetNetworkPredicate()}
|
||||
value={networkId}
|
||||
/>
|
||||
</Col>
|
||||
@@ -274,12 +322,7 @@ export default class MigrateVmsModalBody extends BaseComponent {
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
]}
|
||||
{nonRunningVm && <div>
|
||||
<Icon icon='error' />
|
||||
{' '}
|
||||
{_('migrateVmSomeBadPowerState', { vm: nonRunningVm.name_label })}
|
||||
</div>}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
58
src/common/xo/new-ssh-key-modal/index.js
Normal file
58
src/common/xo/new-ssh-key-modal/index.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import BaseComponent from 'base-component'
|
||||
import React from 'react'
|
||||
|
||||
import _ from '../../intl'
|
||||
import SingleLineRow from '../../single-line-row'
|
||||
import { Col } from '../../grid'
|
||||
import getEventValue from '../../get-event-value'
|
||||
|
||||
export default class NewSshKeyModalBody extends BaseComponent {
|
||||
get value () {
|
||||
return this.state
|
||||
}
|
||||
|
||||
_onKeyChange = event => {
|
||||
const key = getEventValue(event)
|
||||
const splitKey = key.split(' ')
|
||||
if (!this.state.title && splitKey.length === 3) {
|
||||
this.setState({ title: splitKey[2].split('\n')[0] })
|
||||
}
|
||||
this.setState({ key })
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
key,
|
||||
title
|
||||
} = this.state
|
||||
|
||||
return <div>
|
||||
<div className='p-b-1'>
|
||||
<SingleLineRow>
|
||||
<Col size={4}>{_('title')}</Col>
|
||||
<Col size={8}>
|
||||
<input
|
||||
className='form-control'
|
||||
onChange={this.linkState('title')}
|
||||
type='text'
|
||||
value={title || ''}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
<div className='p-b-1'>
|
||||
<SingleLineRow>
|
||||
<Col size={4}>{_('key')}</Col>
|
||||
<Col size={8}>
|
||||
<textarea
|
||||
className='form-control'
|
||||
onChange={this._onKeyChange}
|
||||
rows={10}
|
||||
value={key || ''}
|
||||
/>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
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>
|
||||
))
|
||||
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-tasks;
|
||||
}
|
||||
&-template {
|
||||
@extend .fa;
|
||||
@extend .fa-thumb-tack;
|
||||
}
|
||||
&-message {
|
||||
@extend .fa;
|
||||
@extend .fa-envelope-o;
|
||||
@@ -92,6 +96,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-clipboard;
|
||||
}
|
||||
&-shortcuts {
|
||||
@extend .fa;
|
||||
@extend .fa-keyboard-o;
|
||||
}
|
||||
&-info {
|
||||
@extend .fa;
|
||||
@extend .fa-info-circle;
|
||||
@@ -112,6 +120,14 @@
|
||||
@extend .fa;
|
||||
@extend .fa-play;
|
||||
}
|
||||
&-ssh-key {
|
||||
@extend .fa;
|
||||
@extend .fa-key;
|
||||
}
|
||||
&-ip {
|
||||
@extend .fa;
|
||||
@extend .fa-map-marker;
|
||||
}
|
||||
|
||||
&-shown {
|
||||
@extend .fa;
|
||||
@@ -144,11 +160,11 @@
|
||||
|
||||
&-asc {
|
||||
@extend .fa;
|
||||
@extend .fa-arrow-up;
|
||||
@extend .fa-arrow-down;
|
||||
}
|
||||
&-desc {
|
||||
@extend .fa;
|
||||
@extend .fa-arrow-down;
|
||||
@extend .fa-arrow-up;
|
||||
}
|
||||
&-sort {
|
||||
@extend .fa;
|
||||
@@ -159,12 +175,24 @@
|
||||
@extend .fa;
|
||||
@extend .fa-link;
|
||||
}
|
||||
|
||||
&-disconnect {
|
||||
@extend .fa;
|
||||
@extend .fa-chain-broken;
|
||||
}
|
||||
|
||||
&-lock {
|
||||
@extend .fa;
|
||||
@extend .fa-lock;
|
||||
}
|
||||
&-unlock {
|
||||
@extend .fa;
|
||||
@extend .fa-unlock;
|
||||
}
|
||||
&-unknown-status {
|
||||
@extend .fa;
|
||||
@extend .fa-question-circle;
|
||||
}
|
||||
|
||||
&-cpu {
|
||||
@extend .fa;
|
||||
@extend .fa-dashboard;
|
||||
@@ -377,6 +405,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-trash;
|
||||
}
|
||||
&-migrate {
|
||||
@extend .fa;
|
||||
@extend .fa-share;
|
||||
}
|
||||
}
|
||||
// Host
|
||||
&-host {
|
||||
@@ -496,6 +528,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-sort;
|
||||
}
|
||||
&-reset {
|
||||
@extend .fa;
|
||||
@extend .fa-undo;
|
||||
}
|
||||
&-save {
|
||||
@extend .fa;
|
||||
@extend .fa-floppy-o;
|
||||
@@ -537,6 +573,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-clock-o;
|
||||
}
|
||||
&-time {
|
||||
@extend .fa;
|
||||
@extend .fa-clock-o;
|
||||
}
|
||||
&-database {
|
||||
@extend .fa;
|
||||
@extend .fa-database;
|
||||
@@ -596,14 +636,6 @@
|
||||
&-menu-self-service {
|
||||
@extend .fa;
|
||||
@extend .fa-cloud;
|
||||
&-dashboard {
|
||||
@extend .fa;
|
||||
@extend .fa-dashboard;
|
||||
}
|
||||
&-admin {
|
||||
@extend .fa;
|
||||
@extend .fa-wrench;
|
||||
}
|
||||
}
|
||||
&-menu-backup {
|
||||
@extend .fa;
|
||||
@@ -668,6 +700,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-puzzle-piece;
|
||||
}
|
||||
&-logs {
|
||||
@extend .fa;
|
||||
@extend .fa-list;
|
||||
}
|
||||
}
|
||||
&-menu-about {
|
||||
@extend .fa;
|
||||
@@ -741,6 +777,10 @@
|
||||
@extend .fa;
|
||||
@extend .icon-debian;
|
||||
}
|
||||
&-docker {
|
||||
@extend .fa;
|
||||
@extend .icon-docker;
|
||||
}
|
||||
&-fedora {
|
||||
@extend .fa;
|
||||
@extend .icon-fedora;
|
||||
|
||||
@@ -21,6 +21,8 @@ html.no-js(
|
||||
//- .visible-js to display content only when JavaScript is ENABLED.
|
||||
//- .hidden-js to display content only when JavaScript is DISABLED.
|
||||
script !function(d){d.className=d.className.replace(/\bno-js\b/,'js')}(document.documentElement)
|
||||
script(src = 'https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en')
|
||||
|
||||
style .no-js .visible-js,.js .hidden-js{display:none}
|
||||
|
||||
//- (TODO: confirm) For smartphones and tablets: sets the page
|
||||
|
||||
@@ -30,10 +30,10 @@ $fa-font-path: "./";
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
|
||||
@import "./chartist";
|
||||
@import "./meter";
|
||||
@import "./icons";
|
||||
@import "./usage";
|
||||
|
||||
// ROOT STYLES =================================================================
|
||||
|
||||
@@ -72,17 +72,27 @@ $select-input-height: 40px; // Bootstrap input height
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.Select-value-label {
|
||||
color: #373a3c;
|
||||
}
|
||||
|
||||
.Select-control {
|
||||
border-radius: unset;
|
||||
}
|
||||
|
||||
// Disabled option style.
|
||||
.Select-menu-outer {
|
||||
.Select-option.is-disabled {
|
||||
cursor: default;
|
||||
font-weight: bold;
|
||||
color: #777;
|
||||
}
|
||||
.Select-menu-outer .Select-option.is-disabled {
|
||||
cursor: default;
|
||||
font-weight: bold;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
.Select-placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.Select--single > .Select-control .Select-value {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
// COLORS ======================================================================
|
||||
@@ -128,24 +138,6 @@ $select-input-height: 40px; // Bootstrap input height
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// TAG STYLE ===================================================================
|
||||
|
||||
.xo-tag {
|
||||
vertical-align: middle;
|
||||
background-color: #2598d9;
|
||||
border-radius: 0.5em;
|
||||
color: white;
|
||||
padding: 0.3em;
|
||||
margin: 0.2em;
|
||||
margin-top: -0.1em;
|
||||
font-size: 0.6em;
|
||||
}
|
||||
|
||||
.add-tag-action {
|
||||
font-size: 0.8em;
|
||||
margin-left: 0.2em;
|
||||
}
|
||||
|
||||
// GENERAL STYLES ==============================================================
|
||||
|
||||
.tag-ip {
|
||||
@@ -202,35 +194,6 @@ $select-input-height: 40px; // Bootstrap input height
|
||||
background: $gray-lighter;
|
||||
}
|
||||
|
||||
// MEMORY/DISK BAR STYLE =======================================================
|
||||
|
||||
.progress-usage {
|
||||
@extend .progress;
|
||||
background-color: #eee;
|
||||
height: 2em;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 2em;
|
||||
}
|
||||
|
||||
.progress-dom0 {
|
||||
display: inline-block;
|
||||
background-color: #337ab7;
|
||||
height: 2em;
|
||||
}
|
||||
|
||||
.progress-object {
|
||||
background-color: #5cb85c;
|
||||
height: 2em;
|
||||
margin-right: 0px;
|
||||
display: inline-block;
|
||||
transition: all 0.3s ease 0s;
|
||||
}
|
||||
|
||||
.progress-object:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
// NOTIFICATIONS STYLE =========================================================
|
||||
|
||||
.notify-container {
|
||||
|
||||
29
src/keymap.js
Normal file
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;
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export default class About extends Component {
|
||||
const { user } = this.props
|
||||
const isAdmin = user && user.permission === 'admin'
|
||||
|
||||
return <Page header={HEADER}>
|
||||
return <Page header={HEADER} title='aboutPage' formatTitle>
|
||||
<Container className='text-xs-center'>
|
||||
{isAdmin && <Row>
|
||||
<Col mediumSize={6}>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export const getJobValues = job => job.values || job.items
|
||||
@@ -32,7 +32,7 @@ const Backup = routes('overview', {
|
||||
overview: Overview,
|
||||
restore: Restore
|
||||
})(
|
||||
({ children }) => <Page header={HEADER}>{children}</Page>
|
||||
({ children }) => <Page header={HEADER} title='backupPage' formatTitle>{children}</Page>
|
||||
)
|
||||
|
||||
export default Backup
|
||||
|
||||
@@ -1,27 +1,78 @@
|
||||
import _, { messages } from 'intl'
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import Component from 'base-component'
|
||||
import delay from 'lodash/delay'
|
||||
import forEach from 'lodash/forEach'
|
||||
import GenericInput from 'json-schema-input'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import Scheduler, { SchedulePreview } from 'scheduling'
|
||||
import startsWith from 'lodash/startsWith'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import Wizard, { Section } from 'wizard'
|
||||
import { Container } from 'grid'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { error } from 'notification'
|
||||
import { generateUiSchema } from 'xo-json-schema-input'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import { confirm } from 'modal'
|
||||
|
||||
import {
|
||||
createJob,
|
||||
createSchedule,
|
||||
getRemote,
|
||||
setJob,
|
||||
setSchedule
|
||||
updateSchedule
|
||||
} from 'xo'
|
||||
|
||||
import { getJobValues } from '../helpers'
|
||||
// ===================================================================
|
||||
|
||||
const NO_SMART_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
vms: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
'xo:type': 'vm'
|
||||
},
|
||||
title: 'VMs',
|
||||
description: 'Choose VMs to backup.'
|
||||
}
|
||||
},
|
||||
required: [ 'vms' ]
|
||||
}
|
||||
const NO_SMART_UI_SCHEMA = generateUiSchema(NO_SMART_SCHEMA)
|
||||
|
||||
const SMART_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
status: {
|
||||
default: 'All',
|
||||
enum: [ 'All', 'Running', 'Halted' ],
|
||||
title: 'VMs statuses',
|
||||
description: 'The statuses of VMs to backup.'
|
||||
},
|
||||
pools: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
'xo:type': 'pool'
|
||||
},
|
||||
title: 'Resident on'
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
'xo:type': 'tag'
|
||||
},
|
||||
title: 'VMs Tags',
|
||||
description: 'VMs which contains at least one of these tags. Not used if empty.'
|
||||
}
|
||||
},
|
||||
required: [ 'status', 'pools' ]
|
||||
}
|
||||
const SMART_UI_SCHEMA = generateUiSchema(SMART_SCHEMA)
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -33,15 +84,6 @@ const COMMON_SCHEMA = {
|
||||
title: 'Tag',
|
||||
description: 'Back-up tag.'
|
||||
},
|
||||
vms: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
'xo:type': 'vm'
|
||||
},
|
||||
title: 'VMs',
|
||||
description: 'Choose VMs to backup.'
|
||||
},
|
||||
_reportWhen: {
|
||||
enum: [ 'never', 'always', 'failure' ],
|
||||
title: 'Report',
|
||||
@@ -189,13 +231,12 @@ const BACKUP_METHOD_TO_INFO = {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@injectIntl
|
||||
const DEFAULT_CRON_PATTERN = '0 0 * * *'
|
||||
|
||||
export default class New extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
const { state } = this
|
||||
state.cronPattern = '* * * * *'
|
||||
this.state.cronPattern = DEFAULT_CRON_PATTERN
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
@@ -208,53 +249,114 @@ export default class New extends Component {
|
||||
}
|
||||
this.setState({
|
||||
backupInfo: BACKUP_METHOD_TO_INFO[job.method],
|
||||
cronPattern: schedule.cron
|
||||
cronPattern: schedule.cron,
|
||||
timezone: schedule.timezone || null
|
||||
}, () => delay(this._populateForm, 250, job)) // Work around.
|
||||
// Without the delay, some selects are not always ready to load a value
|
||||
// Values are displayed, but html5 compliant browsers say the value is required and empty on submit
|
||||
}
|
||||
|
||||
_populateForm = (job) => {
|
||||
let values = getJobValues(job.paramsVector)
|
||||
const { backupInput } = this.refs
|
||||
_populateForm = job => {
|
||||
let values = job.paramsVector.items
|
||||
const {
|
||||
backupInput,
|
||||
vmsInput
|
||||
} = this.refs
|
||||
|
||||
if (values.length === 1) {
|
||||
// Older versions of XenOrchestra uses only values[0].
|
||||
values = getJobValues(values[0])
|
||||
const array = values[0].values
|
||||
const config = array[0]
|
||||
const reportWhen = config._reportWhen
|
||||
|
||||
backupInput.value = {
|
||||
...values[0],
|
||||
vms: map(values, value => value.id)
|
||||
...config,
|
||||
_reportWhen:
|
||||
// Fix old reportWhen values...
|
||||
(reportWhen === 'fail' && 'failure') ||
|
||||
(reportWhen === 'alway' && 'always') ||
|
||||
reportWhen
|
||||
}
|
||||
vmsInput.value = { vms: map(array, ({ id, vm }) => id || vm) }
|
||||
} else {
|
||||
backupInput.value = {
|
||||
...getJobValues(values[1])[0],
|
||||
vms: getJobValues(values[0])
|
||||
if (values[1].type === 'map') {
|
||||
// Smart backup.
|
||||
const {
|
||||
$pool: { __or: pools },
|
||||
tags: { __or: tags } = {},
|
||||
power_state: status = 'All'
|
||||
} = values[1].collection.pattern
|
||||
|
||||
backupInput.value = values[0].values[0]
|
||||
|
||||
this.setState({
|
||||
smartBackupMode: true
|
||||
}, () => {
|
||||
vmsInput.value = {
|
||||
pools,
|
||||
status,
|
||||
tags: map(tags, tag => tag[0])
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// Normal backup.
|
||||
backupInput.value = values[1].values[0]
|
||||
vmsInput.value = { vms: values[0].values }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleSubmit = () => {
|
||||
const backup = this.refs.backupInput.value
|
||||
_handleSubmit = async () => {
|
||||
const {
|
||||
vms,
|
||||
enabled,
|
||||
...callArgs
|
||||
} = backup
|
||||
} = this.refs.backupInput.value
|
||||
const vmsInputValue = this.refs.vmsInput.value
|
||||
|
||||
const { backupInfo } = this.state
|
||||
const job = {
|
||||
type: 'call',
|
||||
key: backupInfo.jobKey,
|
||||
method: backupInfo.method,
|
||||
paramsVector: {
|
||||
const {
|
||||
backupInfo,
|
||||
smartBackupMode,
|
||||
timezone
|
||||
} = this.state
|
||||
|
||||
const paramsVector = !smartBackupMode
|
||||
? {
|
||||
type: 'crossProduct',
|
||||
items: [{
|
||||
type: 'set',
|
||||
values: map(vms, vm => ({ id: vm }))
|
||||
values: map(vmsInputValue.vms, vm => ({ id: vm }))
|
||||
}, {
|
||||
type: 'set',
|
||||
values: [ callArgs ]
|
||||
}]
|
||||
} : {
|
||||
type: 'crossProduct',
|
||||
items: [{
|
||||
type: 'set',
|
||||
values: [ callArgs ]
|
||||
}, {
|
||||
type: 'map',
|
||||
collection: {
|
||||
type: 'fetchObjects',
|
||||
pattern: {
|
||||
$pool: !vmsInputValue.pools.length ? undefined : { __or: vmsInputValue.pools },
|
||||
power_state: vmsInputValue.status === 'All' ? undefined : vmsInputValue.status,
|
||||
tags: !vmsInputValue.tags.length ? undefined : { __or: map(vmsInputValue.tags, tag => [ tag ]) },
|
||||
type: 'VM'
|
||||
}
|
||||
},
|
||||
iteratee: {
|
||||
type: 'extractProperties',
|
||||
mapping: { id: 'id' }
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
const job = {
|
||||
type: 'call',
|
||||
key: backupInfo.jobKey,
|
||||
method: backupInfo.method,
|
||||
paramsVector
|
||||
}
|
||||
|
||||
// Update backup schedule.
|
||||
@@ -262,33 +364,62 @@ export default class New extends Component {
|
||||
|
||||
if (oldJob && oldSchedule) {
|
||||
job.id = oldJob.id
|
||||
oldSchedule.cron = this.state.cronPattern
|
||||
return setJob(job).then(() => setSchedule(oldSchedule))
|
||||
return setJob(job).then(() => updateSchedule({
|
||||
...oldSchedule,
|
||||
cron: this.state.cronPattern,
|
||||
timezone
|
||||
}))
|
||||
}
|
||||
|
||||
let remoteId
|
||||
if (job.type === 'call') {
|
||||
const { paramsVector } = job
|
||||
if (paramsVector.type === 'crossProduct') {
|
||||
const { items } = paramsVector
|
||||
forEach(items, item => {
|
||||
if (item.type === 'set') {
|
||||
forEach(item.values, value => {
|
||||
if (value.remoteId) {
|
||||
remoteId = value.remoteId
|
||||
return false
|
||||
}
|
||||
})
|
||||
if (remoteId) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (remoteId) {
|
||||
const remote = await getRemote(remoteId)
|
||||
if (startsWith(remote.url, 'file:')) {
|
||||
await confirm({
|
||||
title: _('localRemoteWarningTitle'),
|
||||
body: _('localRemoteWarningMessage')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Create backup schedule.
|
||||
return createJob(job).then(jobId => {
|
||||
createSchedule(jobId, this.state.cronPattern, enabled)
|
||||
})
|
||||
return createSchedule(await createJob(job), { cron: this.state.cronPattern, enabled, timezone })
|
||||
}
|
||||
|
||||
_handleReset = () => {
|
||||
const {
|
||||
backupInput,
|
||||
scheduler
|
||||
} = this.refs
|
||||
const { backupInput } = this.refs
|
||||
|
||||
if (backupInput) {
|
||||
backupInput.value = undefined
|
||||
}
|
||||
|
||||
scheduler.value = '* * * * *'
|
||||
this.setState({
|
||||
cronPattern: DEFAULT_CRON_PATTERN
|
||||
})
|
||||
}
|
||||
|
||||
_updateCronPattern = value => {
|
||||
this.setState({
|
||||
cronPattern: value
|
||||
})
|
||||
this.setState(value)
|
||||
}
|
||||
|
||||
_handleBackupSelection = event => {
|
||||
@@ -297,68 +428,126 @@ export default class New extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
_handleSmartBackupMode = event => {
|
||||
this.setState({
|
||||
smartBackupMode: event.target.value === 'smart'
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { backupInfo, defaultValue } = this.state
|
||||
const { formatMessage } = this.props.intl
|
||||
const {
|
||||
backupInfo,
|
||||
cronPattern,
|
||||
smartBackupMode,
|
||||
timezone
|
||||
} = this.state
|
||||
|
||||
return process.env.XOA_PLAN > 1
|
||||
? (
|
||||
<Wizard>
|
||||
<Section icon='backup' title={this.props.job ? 'editVmBackup' : 'newVmBackup'}>
|
||||
<fieldset className='form-group'>
|
||||
<label htmlFor='selectBackup'>{_('newBackupSelection')}</label>
|
||||
<select
|
||||
className='form-control'
|
||||
defaultValue={(backupInfo && backupInfo.method) || null}
|
||||
id='selectBackup'
|
||||
onChange={this._handleBackupSelection}
|
||||
required
|
||||
>
|
||||
<option value={null}>{formatMessage(messages.noSelectedValue)}</option>
|
||||
{map(BACKUP_METHOD_TO_INFO, (info, key) =>
|
||||
<option key={key} value={key}>{formatMessage(messages[info.label])}</option>
|
||||
)}
|
||||
</select>
|
||||
</fieldset>
|
||||
<form className='card-block' id='form-new-vm-backup'>
|
||||
{backupInfo &&
|
||||
<GenericInput
|
||||
defaultValue={defaultValue}
|
||||
label={<span><Icon icon={backupInfo.icon} /> {formatMessage(messages[backupInfo.label])}</span>}
|
||||
required
|
||||
schema={backupInfo.schema}
|
||||
uiSchema={backupInfo.uiSchema}
|
||||
ref='backupInput'
|
||||
/>
|
||||
}
|
||||
</form>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<fieldset className='form-group'>
|
||||
<label htmlFor='selectBackup'>{_('newBackupSelection')}</label>
|
||||
<select
|
||||
className='form-control'
|
||||
value={(backupInfo && backupInfo.method) || ''}
|
||||
id='selectBackup'
|
||||
onChange={this._handleBackupSelection}
|
||||
required
|
||||
>
|
||||
{_('noSelectedValue', message => <option value=''>{message}</option>)}
|
||||
{map(BACKUP_METHOD_TO_INFO, (info, key) =>
|
||||
_(info.label, message => <option key={key} value={key}>{message}</option>)
|
||||
)}
|
||||
</select>
|
||||
</fieldset>
|
||||
<form id='form-new-vm-backup'>
|
||||
{backupInfo && (
|
||||
<div>
|
||||
<GenericInput
|
||||
label={<span><Icon icon={backupInfo.icon} /> {_(backupInfo.label)}</span>}
|
||||
ref='backupInput'
|
||||
required
|
||||
schema={backupInfo.schema}
|
||||
uiSchema={backupInfo.uiSchema}
|
||||
/>
|
||||
<fieldset className='form-group'>
|
||||
<label htmlFor='smartMode'>{_('smartBackupModeSelection')}</label>
|
||||
<select
|
||||
className='form-control'
|
||||
id='smartMode'
|
||||
onChange={this._handleSmartBackupMode}
|
||||
required
|
||||
value={smartBackupMode ? 'smart' : 'normal'}
|
||||
>
|
||||
{_('normalBackup', message => <option value='normal'>{message}</option>)}
|
||||
{_('smartBackup', message => <option value='smart'>{message}</option>)}
|
||||
</select>
|
||||
</fieldset>
|
||||
{smartBackupMode
|
||||
? (process.env.XOA_PLAN > 2
|
||||
? <GenericInput
|
||||
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
|
||||
ref='vmsInput'
|
||||
required
|
||||
schema={SMART_SCHEMA}
|
||||
uiSchema={SMART_UI_SCHEMA}
|
||||
/>
|
||||
: <Container><Upgrade place='newBackup' available={3} /></Container>
|
||||
) : <GenericInput
|
||||
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
|
||||
ref='vmsInput'
|
||||
required
|
||||
schema={NO_SMART_SCHEMA}
|
||||
uiSchema={NO_SMART_UI_SCHEMA}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Section>
|
||||
<Section icon='schedule' title='schedule'>
|
||||
<Scheduler ref='scheduler' onChange={this._updateCronPattern} />
|
||||
<Scheduler
|
||||
cronPattern={cronPattern}
|
||||
onChange={this._updateCronPattern}
|
||||
timezone={timezone}
|
||||
/>
|
||||
</Section>
|
||||
<Section icon='preview' title='preview' summary>
|
||||
<div className='card-block'>
|
||||
<SchedulePreview cron={this.state.cronPattern} />
|
||||
{process.env.XOA_PLAN < 4 && backupInfo && process.env.XOA_PLAN < REQUIRED_XOA_PLAN[backupInfo.jobKey]
|
||||
? <Upgrade place='newBackup' available={REQUIRED_XOA_PLAN[backupInfo.jobKey]} />
|
||||
: <fieldset className='pull-xs-right p-t-1'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='btn-lg m-r-1'
|
||||
disabled={!backupInfo}
|
||||
form='form-new-vm-backup'
|
||||
handler={this._handleSubmit}
|
||||
icon='save'
|
||||
redirectOnSuccess='/backup/overview'
|
||||
>
|
||||
{_('saveBackupJob')}
|
||||
</ActionButton>
|
||||
<button type='button' className='btn btn-lg btn-secondary' onClick={this._handleReset}>
|
||||
{_('selectTableReset')}
|
||||
</button>
|
||||
</fieldset>
|
||||
}
|
||||
</div>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<SchedulePreview cronPattern={cronPattern} />
|
||||
{process.env.XOA_PLAN < 4 && backupInfo && process.env.XOA_PLAN < REQUIRED_XOA_PLAN[backupInfo.jobKey]
|
||||
? <Upgrade place='newBackup' available={REQUIRED_XOA_PLAN[backupInfo.jobKey]} />
|
||||
: (smartBackupMode && process.env.XOA_PLAN < 3
|
||||
? <Upgrade place='newBackup' available={3} />
|
||||
: <fieldset className='pull-xs-right p-t-1'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
className='btn-lg m-r-1'
|
||||
disabled={!backupInfo}
|
||||
form='form-new-vm-backup'
|
||||
handler={this._handleSubmit}
|
||||
icon='save'
|
||||
redirectOnSuccess='/backup/overview'
|
||||
>
|
||||
{_('saveBackupJob')}
|
||||
</ActionButton>
|
||||
<button type='button' className='btn btn-lg btn-secondary' onClick={this._handleReset}>
|
||||
{_('selectTableReset')}
|
||||
</button>
|
||||
</fieldset>)
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Section>
|
||||
</Wizard>
|
||||
)
|
||||
|
||||
@@ -25,8 +25,6 @@ import {
|
||||
subscribeScheduleTable
|
||||
} from 'xo'
|
||||
|
||||
import { getJobValues } from '../helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const jobKeyToLabel = {
|
||||
@@ -95,12 +93,12 @@ export default class Overview extends Component {
|
||||
_getScheduleTag (schedule, job = {}) {
|
||||
try {
|
||||
const { paramsVector } = job
|
||||
const values = getJobValues(paramsVector)
|
||||
const values = paramsVector.items
|
||||
|
||||
// Old versions of XenOrchestra uses values[0]
|
||||
return (
|
||||
getJobValues(values[0])[0].tag ||
|
||||
getJobValues(values[1])[0].tag
|
||||
values[0].values[0].tag ||
|
||||
values[1].values[0].tag
|
||||
)
|
||||
} catch (_) {}
|
||||
|
||||
@@ -136,7 +134,7 @@ export default class Overview extends Component {
|
||||
<div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h5><Icon icon='schedule' /> Schedules</h5>
|
||||
<h5><Icon icon='schedule' /> {_('backupSchedules')}</h5>
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{schedules.length ? (
|
||||
@@ -146,6 +144,7 @@ export default class Overview extends Component {
|
||||
<th>{_('job')}</th>
|
||||
<th>{_('jobTag')}</th>
|
||||
<th className='hidden-xs-down'>{_('jobScheduling')}</th>
|
||||
<th className='hidden-xs-down'>{_('jobTimezone')}</th>
|
||||
<th>{_('jobState')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -155,9 +154,10 @@ export default class Overview extends Component {
|
||||
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td>{this._getJobLabel(job)}</td>
|
||||
<td>{job.id} ({this._getJobLabel(job)})</td>
|
||||
<td>{this._getScheduleTag(schedule, job)}</td>
|
||||
<td className='hidden-xs-down'>{schedule.cron}</td>
|
||||
<td className='hidden-xs-down'>{schedule.timezone || _('jobServerTimezone')}</td>
|
||||
<td>
|
||||
{this._getScheduleToggle(schedule)}
|
||||
<fieldset className='pull-xs-right'>
|
||||
|
||||
@@ -36,9 +36,9 @@ const parseDate = date => +moment(date, 'YYYYMMDDTHHmmssZ').format('x')
|
||||
const isEmptyRemote = remote => !remote.backupInfoByVm || !size(remote.backupInfoByVm)
|
||||
|
||||
const backupOptionRenderer = backup => <span>
|
||||
{backup.type === 'delta' && <span><span className='tag tag-info'>{_('delta')}</span>{' '}</span>}
|
||||
{backup.tag}
|
||||
{' '}
|
||||
{backup.type === 'delta' && <span><span className='tag tag-info'>{_('delta')}</span>{' '}</span>}
|
||||
{backup.tag}
|
||||
{' '}
|
||||
<FormattedDate value={new Date(backup.date)} month='long' day='numeric' year='numeric' hour='2-digit' minute='2-digit' second='2-digit' />
|
||||
</span>
|
||||
|
||||
@@ -137,7 +137,7 @@ export default class Restore extends Component {
|
||||
{r.enabled && <span className='tag tag-success'>{_('remoteEnabled')}</span>}
|
||||
{r.error && <span className='tag tag-danger'>{_('remoteError')}</span>}
|
||||
<span className='pull-right'>
|
||||
<ActionButton disabled={!r.enabled} icon='refresh' btnStyle='default' handler={this._list} handlerParam={r.id} />
|
||||
<Tooltip content={_('displayBackup')}><ActionButton disabled={!r.enabled} icon='refresh' btnStyle='default' handler={this._list} handlerParam={r.id} /></Tooltip>
|
||||
</span>
|
||||
{r.backupInfoByVm && <div>
|
||||
<br />
|
||||
@@ -195,7 +195,8 @@ const BK_COLUMNS = [
|
||||
{
|
||||
name: _('lastBackupColumn'),
|
||||
itemRenderer: info => <span><FormattedDate value={info.last.date} month='long' day='numeric' year='numeric' hour='2-digit' minute='2-digit' second='2-digit' /> ({info.last.type})</span>,
|
||||
sortCriteria: info => info.last.date
|
||||
sortCriteria: info => info.last.date,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
{
|
||||
name: _('availableBackupsColumn'),
|
||||
|
||||
@@ -2,9 +2,11 @@ import _ from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import Tooltip from 'tooltip'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import React, { Component } from 'react'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
@@ -25,7 +27,7 @@ import {
|
||||
|
||||
const SrColContainer = connectStore(() => ({
|
||||
container: createGetObject()
|
||||
}))(({ container }) => <span>{container.name_label}</span>)
|
||||
}))(({ container }) => <Link to={`pools/${container.id}`}>{container.name_label}</Link>)
|
||||
|
||||
const VdiColSr = connectStore(() => ({
|
||||
sr: createGetObject()
|
||||
@@ -64,9 +66,14 @@ const SR_COLUMNS = [
|
||||
sortCriteria: sr => sr.size
|
||||
},
|
||||
{
|
||||
default: true,
|
||||
name: _('srUsage'),
|
||||
itemRenderer: sr => sr.size > 1 && <meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90'></meter>,
|
||||
sortCriteria: sr => sr.physical_usage / sr.size
|
||||
itemRenderer: sr => sr.size > 1 &&
|
||||
<Tooltip content={_('spaceLeftTooltip', {used: Math.round((sr.physical_usage / sr.size) * 100), free: formatSize(sr.size - sr.physical_usage)})}>
|
||||
<meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90' />
|
||||
</Tooltip>,
|
||||
sortCriteria: sr => sr.physical_usage / sr.size,
|
||||
sortOrder: 'desc'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -74,7 +81,8 @@ const VDI_COLUMNS = [
|
||||
{
|
||||
name: _('snapshotDate'),
|
||||
itemRenderer: vdi => <span><FormattedTime value={vdi.snapshot_time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={vdi.snapshot_time * 1000} />)</span>,
|
||||
sortCriteria: vdi => vdi.snapshot_time
|
||||
sortCriteria: vdi => vdi.snapshot_time,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
{
|
||||
name: _('vdiNameLabel'),
|
||||
@@ -112,7 +120,8 @@ const VM_COLUMNS = [
|
||||
{
|
||||
name: _('snapshotDate'),
|
||||
itemRenderer: vm => <span><FormattedTime value={vm.snapshot_time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={vm.snapshot_time * 1000} />)</span>,
|
||||
sortCriteria: vm => vm.snapshot_time
|
||||
sortCriteria: vm => vm.snapshot_time,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
{
|
||||
name: _('vmNameLabel'),
|
||||
@@ -147,7 +156,8 @@ const ALARM_COLUMNS = [
|
||||
itemRenderer: message => (
|
||||
<span><FormattedTime value={message.time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={message.time * 1000} />)</span>
|
||||
),
|
||||
sortCriteria: message => message.time
|
||||
sortCriteria: message => message.time,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
{
|
||||
name: _('alarmContent'),
|
||||
@@ -226,6 +236,8 @@ export default class Health extends Component {
|
||||
)
|
||||
)
|
||||
|
||||
_getSrUrl = sr => `srs/${sr.id}`
|
||||
|
||||
render () {
|
||||
return process.env.XOA_PLAN > 3
|
||||
? <Container>
|
||||
@@ -240,7 +252,11 @@ export default class Health extends Component {
|
||||
? <p className='text-xs-center'>{_('noSrs')}</p>
|
||||
: <Row>
|
||||
<Col>
|
||||
<SortedTable collection={this.props.userSrs} columns={SR_COLUMNS} defaultColumn={4} />
|
||||
<SortedTable
|
||||
collection={this.props.userSrs}
|
||||
columns={SR_COLUMNS}
|
||||
rowLink={this._getSrUrl}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ const Dashboard = routes('overview', {
|
||||
stats: Stats,
|
||||
visualizations: Visualizations
|
||||
})(
|
||||
({ children }) => <Page header={HEADER}>{children}</Page>
|
||||
({ children }) => <Page header={HEADER} title='dashboardPage' formatTitle>{children}</Page>
|
||||
)
|
||||
|
||||
export default Dashboard
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ChartistGraph from 'react-chartist'
|
||||
import Component from 'base-component'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Icon from 'icon'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import Link from 'link'
|
||||
import propTypes from 'prop-types'
|
||||
import Link, { BlockLink } from 'link'
|
||||
import map from 'lodash/map'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import HostsPatchesTable from 'hosts-patches-table'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import size from 'lodash/size'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
import { Card, CardBlock, CardHeader } from 'card'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
createCollectionWrapper,
|
||||
createCounter,
|
||||
createFilter,
|
||||
createGetObjectsOfType,
|
||||
createGetHostMetrics,
|
||||
createSelector,
|
||||
@@ -27,8 +26,6 @@ import {
|
||||
formatSize
|
||||
} from 'utils'
|
||||
import {
|
||||
getHostMissingPatches,
|
||||
installAllHostPatches,
|
||||
isSrWritable,
|
||||
subscribeUsers
|
||||
} from 'xo'
|
||||
@@ -37,149 +34,26 @@ import styles from './index.css'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const MISSING_PATCHES_COLUMNS = [
|
||||
{
|
||||
name: _('srPool'),
|
||||
itemRenderer: (host, { pools }) => pools[host.$pool].name_label,
|
||||
sortCriteria: (host, { pools }) => pools[host.$pool].name_label
|
||||
},
|
||||
{
|
||||
name: _('srHost'),
|
||||
itemRenderer: host => host.name_label,
|
||||
sortCriteria: host => host.name_label
|
||||
},
|
||||
{
|
||||
name: _('hostDescription'),
|
||||
itemRenderer: host => host.name_description,
|
||||
sortCriteria: host => host.name_description
|
||||
},
|
||||
{
|
||||
name: _('hostMissingPatches'),
|
||||
itemRenderer: (host, { missingPatches }) => missingPatches[host.id],
|
||||
sortCriteria: (host, { missingPatches }) => missingPatches[host.id]
|
||||
},
|
||||
{
|
||||
name: _('patchUpdateButton'),
|
||||
itemRenderer: (host, { installAllHostPatches }) => (
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
handler={installAllHostPatches}
|
||||
handlerParam={host}
|
||||
icon='host-patch-update'
|
||||
/>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@connectStore(() => {
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
const getHosts = createGetObjectsOfType('host').sort()
|
||||
|
||||
return {
|
||||
pools: getPools,
|
||||
hosts: getHosts
|
||||
}
|
||||
@propTypes({
|
||||
hosts: propTypes.object.isRequired
|
||||
})
|
||||
class MissingPatchesPanel extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state.missingPatches = {}
|
||||
}
|
||||
|
||||
_getHosts = createFilter(
|
||||
() => this.props.hosts,
|
||||
() => this.state.missingPatches,
|
||||
[ (host, missingPatches) => missingPatches[host.id] ]
|
||||
)
|
||||
|
||||
_refreshMissingPatches = () => (
|
||||
Promise.all(
|
||||
map(this.props.hosts, this._refreshHostMissingPatches)
|
||||
)
|
||||
)
|
||||
|
||||
_installAllMissingPatches = () => (
|
||||
Promise.all(map(this._getHosts(), this._installAllHostPatches))
|
||||
)
|
||||
|
||||
_refreshHostMissingPatches = host => (
|
||||
getHostMissingPatches(host).then(patches => {
|
||||
this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[host.id]: patches.length
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
_installAllHostPatches = host => (
|
||||
installAllHostPatches(host).then(() =>
|
||||
this._refreshHostMissingPatches(host)
|
||||
)
|
||||
)
|
||||
|
||||
componentWillMount () {
|
||||
this._refreshMissingPatches()
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
forEach(nextProps.hosts, host => {
|
||||
const { id } = host
|
||||
|
||||
if (this.state.missingPatches[id] !== undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
missingPatches: {
|
||||
...this.state.missingPatches,
|
||||
[id]: 0
|
||||
}
|
||||
})
|
||||
|
||||
this._refreshHostMissingPatches(host)
|
||||
})
|
||||
}
|
||||
class PatchesCard extends Component {
|
||||
_getContainer = () => this.refs.container
|
||||
|
||||
render () {
|
||||
const hosts = this._getHosts()
|
||||
const noPatches = isEmpty(hosts)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='host-patch-update' /> {_('update')}
|
||||
<ButtonGroup className='pull-right'>
|
||||
<ActionButton
|
||||
btnStyle='secondary'
|
||||
handler={this._refreshMissingPatches}
|
||||
icon='refresh'
|
||||
/>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
disabled={noPatches}
|
||||
handler={this._installAllMissingPatches}
|
||||
icon='host-patch-update'
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<div ref='container' className='pull-right' />
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{!noPatches
|
||||
? (
|
||||
<SortedTable
|
||||
collection={hosts}
|
||||
columns={MISSING_PATCHES_COLUMNS}
|
||||
userData={{
|
||||
installAllHostPatches: this._installAllHostPatches,
|
||||
missingPatches: this.state.missingPatches,
|
||||
pools: this.props.pools
|
||||
}}
|
||||
/>
|
||||
) : <p>{_('patchNothing')}</p>
|
||||
}
|
||||
<HostsPatchesTable
|
||||
buttonsGroupContainer={this._getContainer}
|
||||
container={ButtonGroup}
|
||||
displayPools
|
||||
hosts={this.props.hosts}
|
||||
/>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
)
|
||||
@@ -261,6 +135,7 @@ class MissingPatchesPanel extends Component {
|
||||
|
||||
return {
|
||||
hostMetrics: getHostMetrics,
|
||||
hosts: getHosts,
|
||||
nAlarmMessages: getNumberOfAlarmMessages,
|
||||
nHosts: getNumberOfHosts,
|
||||
nPools: getNumberOfPools,
|
||||
@@ -278,9 +153,9 @@ export default class Overview extends Component {
|
||||
})
|
||||
}
|
||||
render () {
|
||||
const { state } = this
|
||||
const { props, state } = this
|
||||
const users = state && state.users
|
||||
const nUsers = users && Object.keys(users).length
|
||||
const nUsers = size(users)
|
||||
|
||||
return process.env.XOA_PLAN > 2
|
||||
? <Container>
|
||||
@@ -288,11 +163,11 @@ export default class Overview extends Component {
|
||||
<Col mediumSize={4}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='pool' /> {_('poolPanel', { pools: this.props.nPools })}
|
||||
<Icon icon='pool' /> {_('poolPanel', { pools: props.nPools })}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<p className={styles.bigCardContent}>
|
||||
<Link to='/home?t=pool'>{this.props.nPools}</Link>
|
||||
<Link to='/home?t=pool'>{props.nPools}</Link>
|
||||
</p>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
@@ -300,11 +175,11 @@ export default class Overview extends Component {
|
||||
<Col mediumSize={4}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='host' /> {_('hostPanel', { hosts: this.props.nHosts })}
|
||||
<Icon icon='host' /> {_('hostPanel', { hosts: props.nHosts })}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<p className={styles.bigCardContent}>
|
||||
<Link to='/home?t=host'>{this.props.nHosts}</Link>
|
||||
<Link to='/home?t=host'>{props.nHosts}</Link>
|
||||
</p>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
@@ -312,11 +187,11 @@ export default class Overview extends Component {
|
||||
<Col mediumSize={4}>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<Icon icon='vm' /> {_('vmPanel', { vms: this.props.nVms })}
|
||||
<Icon icon='vm' /> {_('vmPanel', { vms: props.nVms })}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<p className={styles.bigCardContent}>
|
||||
<Link to='/home?t=VM'>{this.props.nVms}</Link>
|
||||
<Link to='/home?s=&t=VM'>{props.nVms}</Link>
|
||||
</p>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
@@ -332,11 +207,17 @@ export default class Overview extends Component {
|
||||
<ChartistGraph
|
||||
data={{
|
||||
labels: ['Used Memory', 'Total Memory'],
|
||||
series: [this.props.hostMetrics.memoryUsage, this.props.hostMetrics.memoryTotal - this.props.hostMetrics.memoryUsage]
|
||||
series: [props.hostMetrics.memoryUsage, props.hostMetrics.memoryTotal - props.hostMetrics.memoryUsage]
|
||||
}}
|
||||
options={{ donut: true, donutWidth: 40, showLabel: false }}
|
||||
type='Pie' />
|
||||
<p className='text-xs-center'>{formatSize(this.props.hostMetrics.memoryUsage)} ({_('ofUsage')} {formatSize(this.props.hostMetrics.memoryTotal)})</p>
|
||||
type='Pie'
|
||||
/>
|
||||
<p className='text-xs-center'>
|
||||
{_('ofUsage', {
|
||||
total: formatSize(props.hostMetrics.memoryTotal),
|
||||
usage: formatSize(props.hostMetrics.memoryUsage)
|
||||
})}
|
||||
</p>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -350,11 +231,17 @@ export default class Overview extends Component {
|
||||
<ChartistGraph
|
||||
data={{
|
||||
labels: ['vCPUs', 'CPUs'],
|
||||
series: [this.props.vmMetrics.vcpus, this.props.hostMetrics.cpus]
|
||||
series: [props.vmMetrics.vcpus, props.hostMetrics.cpus]
|
||||
}}
|
||||
options={{ showLabel: false, showGrid: false, distributeSeries: true }}
|
||||
type='Bar' />
|
||||
<p className='text-xs-center'>{this.props.vmMetrics.vcpus} vCPUS ({_('ofUsage')} {this.props.hostMetrics.cpus} CPUs)</p>
|
||||
type='Bar'
|
||||
/>
|
||||
<p className='text-xs-center'>
|
||||
{_('ofUsage', {
|
||||
total: `${props.vmMetrics.vcpus} vCPUS`,
|
||||
usage: `${props.hostMetrics.cpus} CPUs`
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
@@ -366,14 +253,22 @@ export default class Overview extends Component {
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<div className='ct-chart'>
|
||||
<ChartistGraph
|
||||
data={{
|
||||
labels: ['Used Space', 'Total Space'],
|
||||
series: [this.props.srMetrics.srUsage, this.props.srMetrics.srTotal - this.props.srMetrics.srUsage]
|
||||
}}
|
||||
options={{ donut: true, donutWidth: 40, showLabel: false }}
|
||||
type='Pie' />
|
||||
<p className='text-xs-center'>{formatSize(this.props.srMetrics.srUsage)} ({_('ofUsage')} {formatSize(this.props.srMetrics.srTotal)})</p>
|
||||
<BlockLink to='/dashboard/health'>
|
||||
<ChartistGraph
|
||||
data={{
|
||||
labels: ['Used Space', 'Total Space'],
|
||||
series: [props.srMetrics.srUsage, props.srMetrics.srTotal - props.srMetrics.srUsage]
|
||||
}}
|
||||
options={{ donut: true, donutWidth: 40, showLabel: false }}
|
||||
type='Pie'
|
||||
/>
|
||||
<p className='text-xs-center'>
|
||||
{_('ofUsage', {
|
||||
total: formatSize(props.srMetrics.srTotal),
|
||||
usage: formatSize(props.srMetrics.srUsage)
|
||||
})}
|
||||
</p>
|
||||
</BlockLink>
|
||||
</div>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
@@ -386,8 +281,8 @@ export default class Overview extends Component {
|
||||
<Icon icon='alarm' /> {_('alarmMessage')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<p className={`${styles.bigCardContent} ${this.props.nAlarmMessages > 0 ? 'text-warning' : ''}`}>
|
||||
{this.props.nAlarmMessages}
|
||||
<p className={styles.bigCardContent}>
|
||||
<Link to='/dashboard/health' className={props.nAlarmMessages > 0 ? 'text-warning' : ''}>{props.nAlarmMessages}</Link>
|
||||
</p>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
@@ -398,7 +293,9 @@ export default class Overview extends Component {
|
||||
<Icon icon='task' /> {_('taskStatePanel')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<p className={styles.bigCardContent}>{this.props.nTasks}</p>
|
||||
<p className={styles.bigCardContent}>
|
||||
<Link to='/tasks'>{props.nTasks}</Link>
|
||||
</p>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -408,7 +305,9 @@ export default class Overview extends Component {
|
||||
<Icon icon='user' /> {_('usersStatePanel')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<p className={styles.bigCardContent}>{nUsers}</p>
|
||||
<p className={styles.bigCardContent}>
|
||||
<Link to='/settings/users'>{nUsers}</Link>
|
||||
</p>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -420,14 +319,19 @@ export default class Overview extends Component {
|
||||
<Icon icon='vm-force-shutdown' /> {_('vmStatePanel')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<ChartistGraph
|
||||
data={{
|
||||
labels: ['Running', 'Halted', 'Other'],
|
||||
series: [this.props.vmMetrics.running, this.props.vmMetrics.halted, this.props.vmMetrics.other]
|
||||
}}
|
||||
options={{ showLabel: false }}
|
||||
type='Pie' />
|
||||
<p className='text-xs-center'>{this.props.vmMetrics.running} running ({this.props.vmMetrics.halted} halted)</p>
|
||||
<BlockLink to='/home?t=VM'>
|
||||
<ChartistGraph
|
||||
data={{
|
||||
labels: ['Running', 'Halted', 'Other'],
|
||||
series: [props.vmMetrics.running, props.vmMetrics.halted, props.vmMetrics.other]
|
||||
}}
|
||||
options={{ showLabel: false }}
|
||||
type='Pie'
|
||||
/>
|
||||
<p className='text-xs-center'>
|
||||
{_('vmsStates', { running: props.vmMetrics.running, halted: props.vmMetrics.halted })}
|
||||
</p>
|
||||
</BlockLink>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
@@ -437,21 +341,24 @@ export default class Overview extends Component {
|
||||
<Icon icon='disk' /> {_('srTopUsageStatePanel')}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
<ChartistGraph
|
||||
style={{strokeWidth: '30px'}}
|
||||
data={{
|
||||
labels: map(this.props.userSrs, 'name_label'),
|
||||
series: map(this.props.userSrs, sr => (sr.physical_usage / sr.size) * 100)
|
||||
}}
|
||||
options={{ showLabel: false, showGrid: false, distributeSeries: true, high: 100 }}
|
||||
type='Bar' />
|
||||
<BlockLink to='/dashboard/health'>
|
||||
<ChartistGraph
|
||||
style={{strokeWidth: '30px'}}
|
||||
data={{
|
||||
labels: map(props.userSrs, 'name_label'),
|
||||
series: map(props.userSrs, sr => (sr.physical_usage / sr.size) * 100)
|
||||
}}
|
||||
options={{ showLabel: false, showGrid: false, distributeSeries: true, high: 100 }}
|
||||
type='Bar'
|
||||
/>
|
||||
</BlockLink>
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<MissingPatchesPanel />
|
||||
<PatchesCard hosts={props.hosts} />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as ComplexMatcher from 'complex-matcher'
|
||||
import * as homeFilters from 'home-filters'
|
||||
import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ceil from 'lodash/ceil'
|
||||
@@ -8,23 +9,34 @@ import debounce from 'lodash/debounce'
|
||||
import forEach from 'lodash/forEach'
|
||||
import Icon from 'icon'
|
||||
import invoke from 'invoke'
|
||||
import keys from 'lodash/keys'
|
||||
import includes from 'lodash/includes'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import isString from 'lodash/isString'
|
||||
import keys from 'lodash/keys'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import Page from '../page'
|
||||
import React from 'react'
|
||||
import Shortcuts from 'shortcuts'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import size from 'lodash/size'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Card, CardHeader, CardBlock } from 'card'
|
||||
import {
|
||||
addCustomFilter,
|
||||
copyVms,
|
||||
deleteTemplates,
|
||||
deleteVms,
|
||||
emergencyShutdownHosts,
|
||||
migrateVms,
|
||||
restartHosts,
|
||||
restartHostsAgents,
|
||||
restartVms,
|
||||
snapshotVms,
|
||||
startVms,
|
||||
stopVms
|
||||
stopHosts,
|
||||
stopVms,
|
||||
subscribeCurrentUser
|
||||
} from 'xo'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import {
|
||||
@@ -33,7 +45,10 @@ import {
|
||||
SelectTag
|
||||
} from 'select-objects'
|
||||
import {
|
||||
connectStore
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
firstDefined,
|
||||
noop
|
||||
} from 'utils'
|
||||
import {
|
||||
areObjectsFetched,
|
||||
@@ -57,46 +72,105 @@ import styles from './index.css'
|
||||
import HostItem from './host-item'
|
||||
import PoolItem from './pool-item'
|
||||
import VmItem from './vm-item'
|
||||
import TemplateItem from './template-item'
|
||||
|
||||
const ITEMS_PER_PAGE = 20
|
||||
|
||||
const OPTIONS = {
|
||||
host: {
|
||||
defaultFilter: 'power_state:running ',
|
||||
filters: {
|
||||
homeFilterRunningHosts: 'power_state:running ',
|
||||
homeFilterTags: 'tags:'
|
||||
},
|
||||
Item: HostItem
|
||||
filters: homeFilters.host,
|
||||
mainActions: [
|
||||
{ handler: stopHosts, icon: 'host-stop', tooltip: _('stopHostLabel') },
|
||||
{ handler: restartHostsAgents, icon: 'host-restart-agent', tooltip: _('restartHostAgent') },
|
||||
{ handler: emergencyShutdownHosts, icon: 'host-emergency-shutdown', tooltip: _('emergencyModeLabel') },
|
||||
{ handler: restartHosts, icon: 'host-reboot', tooltip: _('rebootHostLabel') }
|
||||
],
|
||||
Item: HostItem,
|
||||
showPoolsSelector: true,
|
||||
sortOptions: [
|
||||
{ labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
|
||||
{ labelId: 'homeSortByPowerstate', sortBy: 'power_state', sortOrder: 'desc' },
|
||||
{ labelId: 'homeSortByRAM', sortBy: 'memory.size', sortOrder: 'desc' },
|
||||
{ labelId: 'homeSortByCpus', sortBy: 'CPUs.cpu_count', sortOrder: 'desc' }
|
||||
]
|
||||
},
|
||||
VM: {
|
||||
defaultFilter: 'power_state:running ',
|
||||
filters: {
|
||||
homeFilterPendingVms: 'current_operations:"" ',
|
||||
homeFilterNonRunningVms: '!power_state:running ',
|
||||
homeFilterHvmGuests: 'virtualizationMode:hvm ',
|
||||
homeFilterRunningVms: 'power_state:running ',
|
||||
homeFilterTags: 'tags:'
|
||||
},
|
||||
Item: VmItem
|
||||
filters: homeFilters.VM,
|
||||
mainActions: [
|
||||
{ handler: stopVms, icon: 'vm-stop', tooltip: _('stopVmLabel') },
|
||||
{ handler: startVms, icon: 'vm-start', tooltip: _('startVmLabel') },
|
||||
{ handler: restartVms, icon: 'vm-reboot', tooltip: _('rebootVmLabel') },
|
||||
{ handler: migrateVms, icon: 'vm-migrate', tooltip: _('migrateVmLabel') },
|
||||
{ handler: copyVms, icon: 'vm-copy', tooltip: _('copyVmLabel') }
|
||||
],
|
||||
otherActions: [{
|
||||
handler: restartVms,
|
||||
icon: 'vm-force-reboot',
|
||||
labelId: 'forceRebootVmLabel',
|
||||
params: true
|
||||
}, {
|
||||
handler: stopVms,
|
||||
icon: 'vm-force-shutdown',
|
||||
labelId: 'forceShutdownVmLabel',
|
||||
params: true
|
||||
}, {
|
||||
handler: snapshotVms,
|
||||
icon: 'vm-snapshot',
|
||||
labelId: 'snapshotVmLabel'
|
||||
}, {
|
||||
handler: deleteVms,
|
||||
icon: 'vm-delete',
|
||||
labelId: 'vmRemoveButton'
|
||||
}],
|
||||
Item: VmItem,
|
||||
showPoolsSelector: true,
|
||||
showHostsSelector: true,
|
||||
sortOptions: [
|
||||
{ labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
|
||||
{ labelId: 'homeSortByPowerstate', sortBy: 'power_state', sortOrder: 'desc' },
|
||||
{ labelId: 'homeSortByRAM', sortBy: 'memory.size', sortOrder: 'desc' },
|
||||
{ labelId: 'homeSortByCpus', sortBy: 'CPUs.number', sortOrder: 'desc' }
|
||||
]
|
||||
},
|
||||
pool: {
|
||||
defaultFilter: '',
|
||||
filters: {
|
||||
homeFilterTags: 'tags:'
|
||||
},
|
||||
Item: PoolItem
|
||||
filters: homeFilters.pool,
|
||||
getActions: noop,
|
||||
Item: PoolItem,
|
||||
sortOptions: [
|
||||
{ labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' }
|
||||
]
|
||||
},
|
||||
'VM-template': {
|
||||
defaultFilter: '',
|
||||
filters: homeFilters.vmTemplate,
|
||||
mainActions: [
|
||||
{ handler: deleteTemplates, icon: 'delete', tooltip: _('templateDelete') }
|
||||
],
|
||||
Item: TemplateItem,
|
||||
showPoolsSelector: true,
|
||||
sortOptions: [
|
||||
{ labelId: 'homeSortByName', sortBy: 'name_label', sortOrder: 'asc' },
|
||||
{ labelId: 'homeSortByRAM', sortBy: 'memory.size', sortOrder: 'desc' },
|
||||
{ labelId: 'homeSortByCpus', sortBy: 'CPUs.number', sortOrder: 'desc' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const TYPES = {
|
||||
VM: _('homeTypeVm'),
|
||||
'VM-template': _('homeTypeVmTemplate'),
|
||||
host: _('homeTypeHost'),
|
||||
pool: _('homeTypePool')
|
||||
}
|
||||
|
||||
const DEFAULT_TYPE = 'VM'
|
||||
|
||||
@addSubscriptions({
|
||||
user: subscribeCurrentUser
|
||||
})
|
||||
@connectStore(() => {
|
||||
const noServersConnected = invoke(
|
||||
createGetObjectsOfType('host'),
|
||||
@@ -129,6 +203,9 @@ export default class Home extends Component {
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
this._initFilter(props)
|
||||
if (props.type !== this.props.type) {
|
||||
this.setState({ highlighted: undefined })
|
||||
}
|
||||
}
|
||||
|
||||
_getNumberOfItems = createCounter(() => this.props.items)
|
||||
@@ -143,6 +220,33 @@ export default class Home extends Component {
|
||||
pathname,
|
||||
query: { ...query, t: type, s: undefined }
|
||||
})
|
||||
this.setState({ highlighted: undefined })
|
||||
}
|
||||
|
||||
_getDefaultFilter (props = this.props) {
|
||||
const { type, user } = props
|
||||
|
||||
const defaultFilter = OPTIONS[type].defaultFilter
|
||||
|
||||
// No user.
|
||||
if (!user) {
|
||||
return defaultFilter
|
||||
}
|
||||
|
||||
const { defaultHomeFilters = {}, filters = {} } = user.preferences || {}
|
||||
const filterName = defaultHomeFilters[type]
|
||||
|
||||
// No filter defined in preferences.
|
||||
if (!filterName) {
|
||||
return defaultFilter
|
||||
}
|
||||
|
||||
// Filter defined.
|
||||
return firstDefined(
|
||||
homeFilters[type][filterName],
|
||||
filters[type][filterName],
|
||||
defaultFilter
|
||||
)
|
||||
}
|
||||
|
||||
_initFilter (props) {
|
||||
@@ -150,7 +254,8 @@ export default class Home extends Component {
|
||||
|
||||
// If filter is null, set a default filter.
|
||||
if (filter == null) {
|
||||
const defaultFilter = OPTIONS[props.type].defaultFilter
|
||||
const defaultFilter = this._getDefaultFilter(props)
|
||||
|
||||
if (defaultFilter != null) {
|
||||
this._setFilter(defaultFilter, props)
|
||||
}
|
||||
@@ -174,7 +279,6 @@ export default class Home extends Component {
|
||||
const { filterInput } = this.refs
|
||||
if (filterInput && filterInput.value !== filter) {
|
||||
filterInput.value = filter
|
||||
filterInput.focus()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,18 +333,14 @@ export default class Home extends Component {
|
||||
|
||||
_getVisibleItems = createPager(
|
||||
this._getFilteredItems,
|
||||
() => this.state.activePage || 1
|
||||
() => this.state.activePage || 1,
|
||||
ITEMS_PER_PAGE
|
||||
)
|
||||
|
||||
_expandAll = () => this.setState({ expandAll: !this.state.expandAll })
|
||||
|
||||
_onPageSelection = (_, event) => { this.page = event.eventKey }
|
||||
|
||||
_sortByName = () => this.setState({ sortBy: 'name_label', sortOrder: 'asc' })
|
||||
_sortByPowerState = () => this.setState({ sortBy: 'power_state', sortOrder: 'desc' })
|
||||
_sortByRam = () => this.setState({ sortBy: 'memory.size', sortOrder: 'desc' })
|
||||
_sortByVcpus = () => this.setState({ sortBy: 'CPUs.number', sortOrder: 'desc' })
|
||||
|
||||
_tick = isCriteria => <Icon icon={isCriteria ? 'success' : undefined} fixedWidth />
|
||||
|
||||
_updateSelectedPools = pools => {
|
||||
@@ -311,37 +411,90 @@ export default class Home extends Component {
|
||||
this._updateMasterCheckbox()
|
||||
}
|
||||
|
||||
_addCustomFilter = () => {
|
||||
return addCustomFilter(
|
||||
this._getType(),
|
||||
this._getFilter()
|
||||
)
|
||||
}
|
||||
|
||||
_getCustomFilters () {
|
||||
const { preferences } = this.props.user || {}
|
||||
|
||||
if (!preferences) {
|
||||
return
|
||||
}
|
||||
|
||||
const customFilters = preferences.filters || {}
|
||||
return customFilters[this._getType()]
|
||||
}
|
||||
|
||||
_getShortcutsHandler = createSelector(
|
||||
() => this._getVisibleItems(),
|
||||
items => (command, event) => {
|
||||
event.preventDefault()
|
||||
switch (command) {
|
||||
case 'SEARCH':
|
||||
this.refs.filterInput.focus()
|
||||
break
|
||||
case 'NAV_DOWN':
|
||||
this.setState({ highlighted: (this.state.highlighted + items.length + 1) % items.length || 0 })
|
||||
break
|
||||
case 'NAV_UP':
|
||||
this.setState({ highlighted: (this.state.highlighted + items.length - 1) % items.length || 0 })
|
||||
break
|
||||
case 'SELECT':
|
||||
this._selectItem(items[this.state.highlighted].id)
|
||||
break
|
||||
case 'JUMP_INTO':
|
||||
const item = items[this.state.highlighted]
|
||||
if (includes(['VM', 'host', 'pool'], item.type)) {
|
||||
this.context.router.push({
|
||||
pathname: `${item.type.toLowerCase()}s/${item.id}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
_typesDropdownItems = map(TYPES, (label, type) =>
|
||||
<MenuItem onClick={() => this._setType(type)}>{label}</MenuItem>
|
||||
)
|
||||
|
||||
_renderHeader () {
|
||||
const { filters } = OPTIONS[this.props.type]
|
||||
const { type } = this.props
|
||||
const { filters } = OPTIONS[type]
|
||||
const customFilters = this._getCustomFilters()
|
||||
|
||||
return <Container>
|
||||
<Row className={styles.itemRowHeader}>
|
||||
<Col mediumSize={3}>
|
||||
<DropdownButton id='typeMenu' bsStyle='info' title={TYPES[this.props.type]}>
|
||||
<MenuItem onClick={() => this._setType('VM')}>
|
||||
VM
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => this._setType('host')}>
|
||||
Host
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => this._setType('pool')}>
|
||||
Pool
|
||||
</MenuItem>
|
||||
<DropdownButton id='typeMenu' bsStyle='info' title={TYPES[this._getType()]}>
|
||||
{this._typesDropdownItems}
|
||||
</DropdownButton>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<div className='input-group'>
|
||||
{!isEmpty(filters) && <div className='input-group-btn'>
|
||||
<DropdownButton id='filter' bsStyle='info' title={_('homeFilters')}>
|
||||
{map(filters, (filter, label) =>
|
||||
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
|
||||
{_(label)}
|
||||
</MenuItem>
|
||||
)}
|
||||
</DropdownButton>
|
||||
</div>}
|
||||
{!isEmpty(filters) && (
|
||||
<div className='input-group-btn'>
|
||||
<DropdownButton id='filter' bsStyle='info' title={_('homeFilters')}>
|
||||
{!isEmpty(customFilters) && [
|
||||
map(customFilters, (filter, name) =>
|
||||
<MenuItem key={`custom-${name}`} onClick={() => this._setFilter(filter)}>
|
||||
{name}
|
||||
</MenuItem>
|
||||
),
|
||||
<MenuItem divider />
|
||||
]}
|
||||
{map(filters, (filter, label) =>
|
||||
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
|
||||
{_(label)}
|
||||
</MenuItem>
|
||||
)}
|
||||
</DropdownButton>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
autoFocus
|
||||
className='form-control'
|
||||
defaultValue={this._getFilter()}
|
||||
onChange={this._onFilterChange}
|
||||
@@ -349,11 +502,18 @@ export default class Home extends Component {
|
||||
type='text'
|
||||
/>
|
||||
<div className='input-group-btn'>
|
||||
<button
|
||||
<a
|
||||
className='btn btn-secondary'
|
||||
onClick={this._clearFilter}>
|
||||
<Icon icon='clear-search' />
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
<div className='input-group-btn'>
|
||||
<ActionButton
|
||||
btnStyle='primary'
|
||||
handler={this._addCustomFilter}
|
||||
icon='save'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Col>
|
||||
@@ -425,40 +585,41 @@ export default class Home extends Component {
|
||||
<p className='text-muted'>{_('homeNewVmMessage')}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<h2>{_('homeNoVmsOr')}</h2>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<Link to='/import'>
|
||||
<Icon icon='menu-new-import' size={4} />
|
||||
<h4>{_('homeImportVm')}</h4>
|
||||
</Link>
|
||||
<p className='text-muted'>{_('homeImportVmMessage')}</p>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<Link to='/backup/restore'>
|
||||
<Icon icon='backup' size={4} />
|
||||
<h4>{_('homeRestoreBackup')}</h4>
|
||||
</Link>
|
||||
<p className='text-muted'>{_('homeRestoreBackupMessage')}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
{isAdmin && <div>
|
||||
<h2>{_('homeNoVmsOr')}</h2>
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<Link to='/import'>
|
||||
<Icon icon='menu-new-import' size={4} />
|
||||
<h4>{_('homeImportVm')}</h4>
|
||||
</Link>
|
||||
<p className='text-muted'>{_('homeImportVmMessage')}</p>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<Link to='/backup/restore'>
|
||||
<Icon icon='backup' size={4} />
|
||||
<h4>{_('homeRestoreBackup')}</h4>
|
||||
</Link>
|
||||
<p className='text-muted'>{_('homeRestoreBackupMessage')}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
</CenterPanel>
|
||||
}
|
||||
|
||||
const filteredItems = this._getFilteredItems()
|
||||
const selectedItemsIds = keys(this._selectedItems)
|
||||
const visibleItems = this._getVisibleItems()
|
||||
const { activePage, sortBy } = this.state
|
||||
const items = {
|
||||
'VM': VmItem,
|
||||
'host': HostItem,
|
||||
'pool': PoolItem
|
||||
}
|
||||
const Item = items[props.type] || items[DEFAULT_TYPE]
|
||||
const { activePage, sortBy, highlighted } = this.state
|
||||
const { type } = props
|
||||
const options = OPTIONS[type]
|
||||
const { Item } = options
|
||||
const { mainActions, otherActions } = options
|
||||
const selectedItemsIds = keys(this._selectedItems)
|
||||
|
||||
return <Page header={this._renderHeader()}>
|
||||
<Shortcuts name='Home' handler={this._getShortcutsHandler()} targetNodeSelector='body' stopPropagation={false} />
|
||||
<div>
|
||||
<div className={styles.itemContainer}>
|
||||
<SingleLineRow className={styles.itemContainerHeader}>
|
||||
@@ -467,118 +628,120 @@ export default class Home extends Component {
|
||||
{' '}
|
||||
<span className='text-muted'>
|
||||
{size(this._selectedItems)
|
||||
? _('homeSelectedVms', { selected: size(this._selectedItems), total: nItems, vmIcon: <Icon icon='vm' /> })
|
||||
: _('homeDisplayedVms', { displayed: filteredItems.length, total: nItems, vmIcon: <Icon icon='vm' /> })
|
||||
? _('homeSelectedItems', {
|
||||
icon: <Icon icon={type.toLowerCase()} />,
|
||||
selected: size(this._selectedItems),
|
||||
total: nItems
|
||||
})
|
||||
: _('homeDisplayedItems', {
|
||||
displayed: filteredItems.length,
|
||||
icon: <Icon icon={type.toLowerCase()} />,
|
||||
total: nItems
|
||||
})
|
||||
}
|
||||
</span>
|
||||
</Col>
|
||||
<Col mediumSize={8} className='text-xs-right hidden-sm-down'>
|
||||
{this.state.displayActions
|
||||
? <div className='btn-group'>
|
||||
<ActionButton btnStyle='secondary' handler={stopVms} handlerParam={selectedItemsIds} icon='vm-stop' />
|
||||
<ActionButton btnStyle='secondary' handler={startVms} handlerParam={selectedItemsIds} icon='vm-start' />
|
||||
<ActionButton btnStyle='secondary' handler={restartVms} handlerParam={selectedItemsIds} icon='vm-reboot' />
|
||||
<ActionButton btnStyle='secondary' handler={migrateVms} handlerParam={selectedItemsIds} icon='vm-migrate' />
|
||||
<DropdownButton bsStyle='secondary' id='advanced' title={_('homeMore')}>
|
||||
<MenuItem onClick={() => { restartVms(selectedItemsIds, true) }}>
|
||||
<Icon icon='vm-force-reboot' fixedWidth /> {_('forceRebootVmLabel')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { stopVms(selectedItemsIds, true) }}>
|
||||
<Icon icon='vm-force-shutdown' fixedWidth /> {_('forceShutdownVmLabel')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { snapshotVms(selectedItemsIds) }}>
|
||||
<Icon icon='vm-snapshot' fixedWidth /> {_('snapshotVmLabel')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => { deleteVms(selectedItemsIds) }}>
|
||||
<Icon icon='vm-delete' fixedWidth /> {_('vmRemoveButton')}
|
||||
</MenuItem>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
: <div>
|
||||
<OverlayTrigger
|
||||
trigger='click'
|
||||
rootClose
|
||||
placement='bottom'
|
||||
overlay={
|
||||
<Popover className={styles.selectObject} id='poolPopover'>
|
||||
<SelectPool
|
||||
autoFocus
|
||||
multi
|
||||
onChange={this._updateSelectedPools}
|
||||
value={this.state.selectedPools}
|
||||
/>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<Button className='btn-link'><Icon icon='pool' /> {_('homeAllPools')}</Button>
|
||||
</OverlayTrigger>
|
||||
{' '}
|
||||
<OverlayTrigger
|
||||
trigger='click'
|
||||
rootClose
|
||||
placement='bottom'
|
||||
overlay={
|
||||
<Popover className={styles.selectObject} id='HostPopover'>
|
||||
<SelectHost
|
||||
autoFocus
|
||||
multi
|
||||
onChange={this._updateSelectedHosts}
|
||||
value={this.state.selectedHosts}
|
||||
/>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<Button className='btn-link'><Icon icon='host' /> {_('homeAllHosts')}</Button>
|
||||
</OverlayTrigger>
|
||||
{' '}
|
||||
<OverlayTrigger
|
||||
autoFocus
|
||||
trigger='click'
|
||||
rootClose
|
||||
placement='bottom'
|
||||
overlay={
|
||||
<Popover className={styles.selectObject} id='tagPopover'>
|
||||
<SelectTag
|
||||
autoFocus
|
||||
multi
|
||||
objects={props.items}
|
||||
onChange={this._updateSelectedTags}
|
||||
value={this.state.selectedTags}
|
||||
/>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<Button className='btn-link'><Icon icon='tags' /> {_('homeAllTags')}</Button>
|
||||
</OverlayTrigger>
|
||||
{' '}
|
||||
<DropdownButton bsStyle='link' id='sort' title={_('homeSortBy')}>
|
||||
<MenuItem onClick={this._sortByName}>
|
||||
{this._tick(sortBy === 'name_label')}
|
||||
{sortBy === 'name_label'
|
||||
? <strong>{_('homeSortByName')}</strong>
|
||||
: _('homeSortByName')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={this._sortByPowerState}>
|
||||
{this._tick(sortBy === 'power_state')}
|
||||
{sortBy === 'power_state'
|
||||
? <strong>{_('homeSortByPowerstate')}</strong>
|
||||
: _('homeSortByPowerstate')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={this._sortByRam}>
|
||||
{this._tick(sortBy === 'memory.size')}
|
||||
{sortBy === 'memory.size'
|
||||
? <strong>{_('homeSortByRAM')}</strong>
|
||||
: _('homeSortByRAM')}
|
||||
</MenuItem>
|
||||
<MenuItem onClick={this._sortByVcpus}>
|
||||
{this._tick(sortBy === 'CPUs.number')}
|
||||
{sortBy === 'CPUs.number'
|
||||
? <strong>{_('homeSortByvCPUs')}</strong>
|
||||
: _('homeSortByvCPUs')}
|
||||
</MenuItem>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
}
|
||||
{this.state.displayActions
|
||||
? (
|
||||
<div>
|
||||
{mainActions && (
|
||||
<div className='btn-group'>
|
||||
{map(mainActions, (action, key) => (
|
||||
<Tooltip content={action.tooltip} key={key}>
|
||||
<ActionButton
|
||||
btnStyle='secondary'
|
||||
{...action}
|
||||
handlerParam={selectedItemsIds}
|
||||
/>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{otherActions && (
|
||||
<DropdownButton bsStyle='secondary' id='advanced' title={_('homeMore')}>
|
||||
{map(otherActions, (action, key) => (
|
||||
<MenuItem key={key} onClick={() => { action.handler(selectedItemsIds, action.params) }}>
|
||||
<Icon icon={action.icon} fixedWidth /> {_(action.labelId)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</DropdownButton>
|
||||
)}
|
||||
</div>
|
||||
) : <div>
|
||||
{options.showPoolsSelector && (
|
||||
<OverlayTrigger
|
||||
trigger='click'
|
||||
rootClose
|
||||
placement='bottom'
|
||||
overlay={
|
||||
<Popover className={styles.selectObject} id='poolPopover'>
|
||||
<SelectPool
|
||||
autoFocus
|
||||
multi
|
||||
onChange={this._updateSelectedPools}
|
||||
value={this.state.selectedPools}
|
||||
/>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<Button className='btn-link'><Icon icon='pool' /> {_('homeAllPools')}</Button>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
{' '}
|
||||
{options.showHostsSelector && (
|
||||
<OverlayTrigger
|
||||
trigger='click'
|
||||
rootClose
|
||||
placement='bottom'
|
||||
overlay={
|
||||
<Popover className={styles.selectObject} id='HostPopover'>
|
||||
<SelectHost
|
||||
autoFocus
|
||||
multi
|
||||
onChange={this._updateSelectedHosts}
|
||||
value={this.state.selectedHosts}
|
||||
/>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<Button className='btn-link'><Icon icon='host' /> {_('homeAllHosts')}</Button>
|
||||
</OverlayTrigger>
|
||||
)}
|
||||
{' '}
|
||||
<OverlayTrigger
|
||||
autoFocus
|
||||
trigger='click'
|
||||
rootClose
|
||||
placement='bottom'
|
||||
overlay={
|
||||
<Popover className={styles.selectObject} id='tagPopover'>
|
||||
<SelectTag
|
||||
autoFocus
|
||||
multi
|
||||
objects={props.items}
|
||||
onChange={this._updateSelectedTags}
|
||||
value={this.state.selectedTags}
|
||||
/>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<Button className='btn-link'><Icon icon='tags' /> {_('homeAllTags')}</Button>
|
||||
</OverlayTrigger>
|
||||
{' '}
|
||||
<DropdownButton bsStyle='link' id='sort' title={_('homeSortBy')}>
|
||||
{map(options.sortOptions, ({ labelId, sortBy: _sortBy, sortOrder }, key) => (
|
||||
<MenuItem key={key} onClick={() => this.setState({ sortBy: _sortBy, sortOrder })}>
|
||||
{this._tick(_sortBy === sortBy)}
|
||||
{_sortBy === sortBy
|
||||
? <strong>{_(labelId)}</strong>
|
||||
: _(labelId)
|
||||
}
|
||||
</MenuItem>
|
||||
))}
|
||||
</DropdownButton>
|
||||
</div>
|
||||
}
|
||||
</Col>
|
||||
<Col smallsize={1} mediumSize={1} className='text-xs-right'>
|
||||
<button className='btn btn-secondary'
|
||||
@@ -587,15 +750,24 @@ export default class Home extends Component {
|
||||
</button>
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
{map(visibleItems, item =>
|
||||
<Item
|
||||
expandAll={this.state.expandAll}
|
||||
item={item}
|
||||
key={item.id}
|
||||
onSelect={this._selectItem}
|
||||
selected={this._selectedItems[item.id]}
|
||||
/>
|
||||
)}
|
||||
{isEmpty(filteredItems)
|
||||
? <p className='text-xs-center m-t-1'>
|
||||
<a className='btn btn-link' onClick={this._clearFilter}>
|
||||
<Icon icon='info' /> {_('homeNoMatches')}
|
||||
</a>
|
||||
</p>
|
||||
: map(visibleItems, (item, index) => (
|
||||
<div className={highlighted === index && styles.highlight}>
|
||||
<Item
|
||||
expandAll={this.state.expandAll}
|
||||
item={item}
|
||||
key={item.id}
|
||||
onSelect={this._selectItem}
|
||||
selected={this._selectedItems[item.id]}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
{filteredItems.length > ITEMS_PER_PAGE && <Row>
|
||||
<div style={{display: 'flex', width: '100%'}}>
|
||||
|
||||
@@ -9,8 +9,8 @@ import SingleLineRow from 'single-line-row'
|
||||
import size from 'lodash/size'
|
||||
import Tags from 'tags'
|
||||
import Tooltip from 'tooltip'
|
||||
import { BlockLink } from 'link'
|
||||
import { Row, Col } from 'grid'
|
||||
import Link, { BlockLink } from 'link'
|
||||
import { Col } from 'grid'
|
||||
import { Text } from 'editable'
|
||||
import {
|
||||
addTag,
|
||||
@@ -48,7 +48,8 @@ import styles from './index.css'
|
||||
|
||||
return {
|
||||
hostMetrics: getHostMetrics,
|
||||
missingPaths: getMissingPatches
|
||||
missingPaths: getMissingPatches,
|
||||
poolHosts: getPoolHosts
|
||||
}
|
||||
})
|
||||
export default class PoolItem extends Component {
|
||||
@@ -64,7 +65,7 @@ export default class PoolItem extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { item: pool, expandAll, selected, hostMetrics } = this.props
|
||||
const { item: pool, expandAll, selected, hostMetrics, poolHosts } = this.props
|
||||
const { missingPatchCount } = this.state
|
||||
return <div className={styles.item}>
|
||||
<BlockLink to={`/pools/${pool.id}`}>
|
||||
@@ -106,11 +107,9 @@ export default class PoolItem extends Component {
|
||||
</Col>
|
||||
<Col largeSize={4} className='hidden-lg-down'>
|
||||
<span>
|
||||
{hostMetrics.count}x <Icon icon='host' />
|
||||
{' '}
|
||||
{hostMetrics.cpus}x <Icon icon='cpu' />
|
||||
{' '}
|
||||
{formatSize(hostMetrics.memoryTotal)}
|
||||
<Tooltip content={_('memoryLeftTooltip', {used: Math.round((hostMetrics.memoryUsage / hostMetrics.memoryTotal) * 100), free: formatSize(hostMetrics.memoryTotal - hostMetrics.memoryUsage)})}>
|
||||
<progress style={{margin: 0}} className='progress' value={(hostMetrics.memoryUsage / hostMetrics.memoryTotal) * 100} max='100' />
|
||||
</Tooltip>
|
||||
</span>
|
||||
</Col>
|
||||
<Col mediumSize={1} className={styles.itemExpandRow}>
|
||||
@@ -122,22 +121,27 @@ export default class PoolItem extends Component {
|
||||
</SingleLineRow>
|
||||
</BlockLink>
|
||||
{(this.state.expanded || expandAll) &&
|
||||
<Row>
|
||||
<Col mediumSize={6} className={styles.itemExpanded}>
|
||||
<SingleLineRow>
|
||||
<Col mediumSize={3} className={styles.itemExpanded}>
|
||||
<span>
|
||||
{hostMetrics.count}x <Icon icon='host' />
|
||||
{' '}
|
||||
{hostMetrics.cpus}x <Icon icon='cpu' />
|
||||
{' '}
|
||||
{formatSize(hostMetrics.memory)}
|
||||
{formatSize(hostMetrics.memoryTotal)}
|
||||
</span>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
<Col mediumSize={4} className={styles.itemExpanded}>
|
||||
<span>
|
||||
{_('homePoolMaster')} <Link to={`/hosts/${pool.master}`}>{poolHosts && poolHosts[pool.master].name_label}</Link>
|
||||
</span>
|
||||
</Col>
|
||||
<Col mediumSize={5}>
|
||||
<span style={{fontSize: '1.4em'}}>
|
||||
<Tags labels={pool.tags} onDelete={this._removeTag} onAdd={this._addTag} />
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
</SingleLineRow>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
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='m-r-1' />
|
||||
<Size value={firstDefined(vm.memory.size, null)} onChange={this._setMemory} /> <Icon icon='memory' />
|
||||
</span>
|
||||
</Col>
|
||||
<Col largeSize={4} className={styles.itemExpanded}>
|
||||
{map(vm.addresses, address => <span key={address} className='tag tag-info tag-ip'>{address}</span>)}
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
<span style={{fontSize: '1.4em'}}>
|
||||
<Tags labels={vm.tags} onDelete={this._removeTag} onAdd={this._addTag} />
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -86,22 +86,28 @@ export default class VmItem extends Component {
|
||||
<span className={styles.itemActionButons}>
|
||||
{this._isRunning
|
||||
? <span>
|
||||
<Tooltip content={_('vmConsoleLabel')}>
|
||||
<Link to={`/vms/${vm.id}/console`}>
|
||||
<Icon icon='vm-console' size='1' fixedWidth />
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Tooltip content={_('stopVmLabel')}>
|
||||
<a onClick={this._stop}>
|
||||
<Icon icon='vm-stop' size='1' />
|
||||
<Icon icon='vm-stop' size='1' fixedWidth />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
: <span>
|
||||
<Icon fixedWidth />
|
||||
<Tooltip content={_('startVmLabel')}>
|
||||
<a onClick={this._start}>
|
||||
<Icon icon='vm-start' size='1' />
|
||||
<Icon icon='vm-start' size='1' fixedWidth />
|
||||
</a>
|
||||
</Tooltip>
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
<Icon className='text-info' icon={vm.os_version && osFamily(vm.os_version.distro)} fixedWidth />
|
||||
<Tooltip content={vm.os_version ? vm.os_version.name : _('unknownOsName')}><Icon className='text-info' icon={vm.os_version && osFamily(vm.os_version.distro)} fixedWidth /></Tooltip>
|
||||
{' '}
|
||||
<Ellipsis>
|
||||
<Text value={vm.name_description} onChange={this._setNameDescription} placeholder={_('vmHomeDescriptionPlaceholder')} useLongClick />
|
||||
|
||||
@@ -10,6 +10,7 @@ import Page from '../page'
|
||||
import pick from 'lodash/pick'
|
||||
import React, { cloneElement, Component } from 'react'
|
||||
import sortBy from 'lodash/sortBy'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Text } from 'editable'
|
||||
import { editHost, fetchHostStats, getHostMissingPatches, installAllHostPatches, installHostPatch } from 'xo'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
@@ -18,6 +19,7 @@ import {
|
||||
routes
|
||||
} from 'utils'
|
||||
import {
|
||||
createDoesHostNeedRestart,
|
||||
createGetObject,
|
||||
createGetObjectsOfType,
|
||||
createSelector
|
||||
@@ -86,26 +88,18 @@ const isRunning = host => host && host.power_state === 'Running'
|
||||
)
|
||||
)
|
||||
|
||||
const getPoolPatches = createGetObjectsOfType('pool_patch').pick(
|
||||
createSelector(
|
||||
createGetObjectsOfType(
|
||||
'host_patch',
|
||||
createSelector(getHost, host => host.patches)
|
||||
),
|
||||
hostPatches => map(hostPatches, patch => patch.pool_patch)
|
||||
)
|
||||
).sort()
|
||||
|
||||
const getPbds = createGetObjectsOfType('PBD').pick(
|
||||
createSelector(getHost, host => host.$PBDs)
|
||||
const getHostPatches = createSelector(
|
||||
createGetObjectsOfType('pool_patch'),
|
||||
createGetObjectsOfType('host_patch').pick(
|
||||
createSelector(getHost, host => host.patches)
|
||||
),
|
||||
(poolsPatches, hostsPatches) => map(hostsPatches, hostPatch => ({
|
||||
...hostPatch,
|
||||
poolPatch: poolsPatches[hostPatch.pool_patch]
|
||||
}))
|
||||
)
|
||||
|
||||
const getSrs = createGetObjectsOfType('SR').pick(
|
||||
createSelector(
|
||||
getPbds,
|
||||
pbds => map(pbds, pbd => pbd.SR)
|
||||
)
|
||||
)
|
||||
const doesNeedRestart = createDoesHostNeedRestart(getHost)
|
||||
|
||||
return (state, props) => {
|
||||
const host = getHost(state, props)
|
||||
@@ -115,19 +109,22 @@ const isRunning = host => host && host.power_state === 'Running'
|
||||
|
||||
return {
|
||||
host,
|
||||
hostPatches: getHostPatches(state, props),
|
||||
logs: getLogs(state, props),
|
||||
needsRestart: doesNeedRestart(state, props),
|
||||
networks: getNetworks(state, props),
|
||||
pbds: getPbds(state, props),
|
||||
pifs: getPifs(state, props),
|
||||
pool: getPool(state, props),
|
||||
poolPatches: getPoolPatches(state, props),
|
||||
srs: getSrs(state, props),
|
||||
vmController: getVmController(state, props),
|
||||
vms: getHostVms(state, props)
|
||||
}
|
||||
}
|
||||
})
|
||||
export default class Host extends Component {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
}
|
||||
|
||||
loop (host = this.props.host) {
|
||||
if (this.cancel) {
|
||||
this.cancel()
|
||||
@@ -181,6 +178,10 @@ export default class Host extends Component {
|
||||
}
|
||||
|
||||
const hostCur = this.props.host
|
||||
if (hostCur && !hostNext) {
|
||||
this.context.router.push('/')
|
||||
}
|
||||
|
||||
if (!hostCur) {
|
||||
this._getMissingPatches(hostNext)
|
||||
}
|
||||
@@ -233,7 +234,7 @@ export default class Host extends Component {
|
||||
value={host.name_description}
|
||||
onChange={this._setNameDescription}
|
||||
/>
|
||||
<span className='text-muted'> - <Link to={`/pools/${pool.id}`}>{pool.name_label}</Link></span>
|
||||
{pool && <span className='text-muted'> - <Link to={`/pools/${pool.id}`}>{pool.name_label}</Link></span>}
|
||||
</span>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
@@ -251,7 +252,10 @@ export default class Host extends Component {
|
||||
<NavLink to={`/hosts/${host.id}/console`}>{_('consoleTabName')}</NavLink>
|
||||
<NavLink to={`/hosts/${host.id}/network`}>{_('networkTabName')}</NavLink>
|
||||
<NavLink to={`/hosts/${host.id}/storage`}>{_('storageTabName')}</NavLink>
|
||||
<NavLink to={`/hosts/${host.id}/patches`}>{_('patchesTabName')} {isEmpty(missingPatches) ? null : <span className='tag tag-pill tag-danger'>{missingPatches.length}</span>}</NavLink>
|
||||
<NavLink to={`/hosts/${host.id}/patches`}>
|
||||
{_('patchesTabName')} {isEmpty(missingPatches) ? null : <span className='tag tag-pill tag-danger'>{missingPatches.length}</span>}
|
||||
{(this.props.needsRestart && isEmpty(missingPatches)) && <Tooltip content={_('rebootUpdateHostLabel')}><Icon icon='alarm' /></Tooltip>}
|
||||
</NavLink>
|
||||
<NavLink to={`/hosts/${host.id}/logs`}>{_('logsTabName')}</NavLink>
|
||||
<NavLink to={`/hosts/${host.id}/advanced`}>{_('advancedTabName')}</NavLink>
|
||||
</NavTabs>
|
||||
@@ -261,17 +265,17 @@ export default class Host extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { host } = this.props
|
||||
const { host, pool } = this.props
|
||||
if (!host) {
|
||||
return <h1>Loading…</h1>
|
||||
return <h1>{_('statusLoading')}</h1>
|
||||
}
|
||||
const childProps = assign(pick(this.props, [
|
||||
'host',
|
||||
'hostPatches',
|
||||
'logs',
|
||||
'networks',
|
||||
'pbds',
|
||||
'pifs',
|
||||
'poolPatches',
|
||||
'srs',
|
||||
'vmController',
|
||||
'vms'
|
||||
@@ -283,7 +287,7 @@ export default class Host extends Component {
|
||||
installPatch: this._installPatch
|
||||
}
|
||||
)
|
||||
return <Page header={this.header()}>
|
||||
return <Page header={this.header()} title={`${host.name_label}${pool ? ` (${pool.name_label})` : ''}`}>
|
||||
{cloneElement(this.props.children, childProps)}
|
||||
</Page>
|
||||
}
|
||||
|
||||
@@ -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,13 +3,15 @@ import Copiable from 'copiable'
|
||||
import Icon from 'icon'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import store from 'store'
|
||||
import Tags from 'tags'
|
||||
import Tooltip from 'tooltip'
|
||||
import { addTag, removeTag } from 'xo'
|
||||
import { BlockLink } from 'link'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { FormattedRelative } from 'react-intl'
|
||||
import { formatSize } from 'utils'
|
||||
import Usage, { UsageElement } from 'usage'
|
||||
import { getObject } from 'selectors'
|
||||
import {
|
||||
CpuSparkLines,
|
||||
MemorySparkLines,
|
||||
@@ -22,75 +24,78 @@ export default ({
|
||||
host,
|
||||
vmController,
|
||||
vms
|
||||
}) => <Container>
|
||||
<br />
|
||||
<Row className='text-xs-center'>
|
||||
<Col mediumSize={3}>
|
||||
<h2>{host.CPUs.cpu_count}x <Icon icon='cpu' size='lg' /></h2>
|
||||
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <CpuSparkLines data={statsOverview} />}</BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<h2>{formatSize(host.memory.size)} <Icon icon='memory' size='lg' /></h2>
|
||||
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <MemorySparkLines data={statsOverview} />}</BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<BlockLink to={`/hosts/${host.id}/network`}><h2>{host.$PIFs.length}x <Icon icon='network' size='lg' /></h2></BlockLink>
|
||||
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <PifSparkLines data={statsOverview} />}</BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<BlockLink to={`/hosts/${host.id}/disks`}><h2>{host.$PBDs.length}x <Icon icon='disk' size='lg' /></h2></BlockLink>
|
||||
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <LoadSparkLines data={statsOverview} />}</BlockLink>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<Row className='text-xs-center'>
|
||||
<Col mediumSize={3}>
|
||||
<p className='text-xs-center'>{_('started', { ago: <FormattedRelative value={host.startTime * 1000} /> })}</p>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<p>{host.license_params.sku_marketing_name} {host.license_params.version} ({host.license_params.sku_type})</p>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<Copiable tagName='p'>
|
||||
{host.address}
|
||||
</Copiable>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<p>{host.bios_strings['system-manufacturer']} {host.bios_strings['system-product-name']}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col className='text-xs-center'>
|
||||
<h5>RAM usage:</h5>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col smallOffset={1} mediumSize={10}>
|
||||
<span className='progress-usage'>
|
||||
<Tooltip content='XenServer'>
|
||||
<span
|
||||
className='progress-dom0'
|
||||
style={{ width: (vmController.memory.size / host.memory.size) * 100 + '%' }}>
|
||||
</span>
|
||||
</Tooltip>
|
||||
{map(vms, vm => (
|
||||
<Tooltip key={vm.id} content={vm.name_label}>
|
||||
<a
|
||||
href={`#/vms/${vm.id}`}
|
||||
className='progress-object'
|
||||
style={{ width: (vm.memory.size / host.memory.size) * 100 + '%' }}>
|
||||
</a>
|
||||
</Tooltip>
|
||||
)
|
||||
)}
|
||||
</span>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<h2 className='text-xs-center'>
|
||||
<Tags labels={host.tags} onDelete={tag => removeTag(host.id, tag)} onAdd={tag => addTag(host.id, tag)} />
|
||||
</h2>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
}) => {
|
||||
const pool = getObject(store.getState(), host.$pool)
|
||||
return <Container>
|
||||
<br />
|
||||
<Row className='text-xs-center'>
|
||||
<Col mediumSize={3}>
|
||||
<h2>{host.CPUs.cpu_count}x <Icon icon='cpu' size='lg' /></h2>
|
||||
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <CpuSparkLines data={statsOverview} />}</BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<h2>{formatSize(host.memory.size)} <Icon icon='memory' size='lg' /></h2>
|
||||
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <MemorySparkLines data={statsOverview} />}</BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<BlockLink to={`/hosts/${host.id}/network`}><h2>{host.$PIFs.length}x <Icon icon='network' size='lg' /></h2></BlockLink>
|
||||
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <PifSparkLines data={statsOverview} />}</BlockLink>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<BlockLink to={`/hosts/${host.id}/storage`}><h2>{host.$PBDs.length}x <Icon icon='disk' size='lg' /></h2></BlockLink>
|
||||
<BlockLink to={`/hosts/${host.id}/stats`}>{statsOverview && <LoadSparkLines data={statsOverview} />}</BlockLink>
|
||||
</Col>
|
||||
</Row>
|
||||
<br />
|
||||
<Row className='text-xs-center'>
|
||||
<Col mediumSize={3}>
|
||||
<p className='text-xs-center'>{_('started', { ago: <FormattedRelative value={host.startTime * 1000} /> })}</p>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<p>{host.license_params.sku_marketing_name} {host.version} ({host.license_params.sku_type})</p>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<Copiable tagName='p'>
|
||||
{host.address}
|
||||
</Copiable>
|
||||
</Col>
|
||||
<Col mediumSize={3}>
|
||||
<p>{host.bios_strings['system-manufacturer']} {host.bios_strings['system-product-name']}</p>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col className='text-xs-center'>
|
||||
<h5>{_('memoryStatePanel')}</h5>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col smallOffset={1} mediumSize={10}>
|
||||
<Usage total={host.memory.size}>
|
||||
<UsageElement
|
||||
highlight
|
||||
tooltip='XenServer'
|
||||
value={vmController.memory.size}
|
||||
/>
|
||||
{map(vms, vm => <UsageElement
|
||||
tooltip={vm.name_label}
|
||||
key={vm.id}
|
||||
value={vm.memory.size}
|
||||
href={`#/vms/${vm.id}`}
|
||||
/>)}
|
||||
</Usage>
|
||||
</Col>
|
||||
</Row>
|
||||
{pool && host.id === pool.master && <Row className='text-xs-center'>
|
||||
<Col>
|
||||
<h3><span className='tag tag-pill tag-info'>{_('pillMaster')}</span></h3>
|
||||
</Col>
|
||||
</Row>}
|
||||
<Row>
|
||||
<Col>
|
||||
<h2 className='text-xs-center'>
|
||||
<Tags labels={host.tags} onDelete={tag => removeTag(host.id, tag)} onAdd={tag => addTag(host.id, tag)} />
|
||||
</h2>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ const LOG_COLUMNS = [
|
||||
{
|
||||
name: _('logDate'),
|
||||
itemRenderer: log => <span><FormattedTime value={log.time * 1000} minute='numeric' hour='numeric' day='numeric' month='long' year='numeric' /> (<FormattedRelative value={log.time * 1000} />)</span>,
|
||||
sortCriteria: log => log.time
|
||||
sortCriteria: log => log.time,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
{
|
||||
name: _('logName'),
|
||||
|
||||
@@ -3,15 +3,35 @@ import ActionRowButton from 'action-row-button'
|
||||
import React from 'react'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import map from 'lodash/map'
|
||||
import some from 'lodash/some'
|
||||
import TabButton from 'tab-button'
|
||||
import { connectPif, createNetwork, deletePif, disconnectPif } from 'xo'
|
||||
import Tooltip from 'tooltip'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { Toggle } from 'form'
|
||||
import { connectStore } from 'utils'
|
||||
import { createGetObjectsOfType } from 'selectors'
|
||||
import {
|
||||
connectPif,
|
||||
createNetwork,
|
||||
deletePif,
|
||||
disconnectPif,
|
||||
editNetwork
|
||||
} from 'xo'
|
||||
|
||||
export default ({
|
||||
const _toggleDefaultLockingMode = (component, tooltip) => tooltip
|
||||
? <Tooltip content={tooltip}>
|
||||
{component}
|
||||
</Tooltip>
|
||||
: component
|
||||
|
||||
export default connectStore(() => ({
|
||||
vifsByNetwork: createGetObjectsOfType('VIF').groupBy('$network')
|
||||
}))(({
|
||||
host,
|
||||
networks,
|
||||
pifs
|
||||
pifs,
|
||||
vifsByNetwork
|
||||
}) => <Container>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
@@ -37,13 +57,16 @@ export default ({
|
||||
<th>{_('pifAddressLabel')}</th>
|
||||
<th>{_('pifMacLabel')}</th>
|
||||
<th>{_('pifMtuLabel')}</th>
|
||||
<th>{_('defaultLockingMode')}</th>
|
||||
<th>{_('pifStatusLabel')}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(pifs, pif =>
|
||||
<tr key={pif.id}>
|
||||
{map(pifs, pif => {
|
||||
const pifInUse = some(vifsByNetwork[pif.$network], vif => vif.attached)
|
||||
|
||||
return <tr key={pif.id}>
|
||||
<td>{pif.device}</td>
|
||||
<td>{networks[pif.$network].name_label}</td>
|
||||
<td>{pif.vlan === -1
|
||||
@@ -53,13 +76,23 @@ export default ({
|
||||
<td>{pif.ip} ({pif.mode})</td>
|
||||
<td><pre>{pif.mac}</pre></td>
|
||||
<td>{pif.mtu}</td>
|
||||
<td className='text-xs-center'>
|
||||
{_toggleDefaultLockingMode(
|
||||
<Toggle
|
||||
disabled={pifInUse}
|
||||
onChange={() => editNetwork(pif.$network, { defaultIsLocked: !networks[pif.$network].defaultIsLocked })}
|
||||
value={networks[pif.$network].defaultIsLocked}
|
||||
/>,
|
||||
pifInUse && _('pifInUse')
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{pif.attached
|
||||
? <span className='tag tag-success'>
|
||||
{_('pifStatusConnected')}
|
||||
{_('pifStatusConnected')}
|
||||
</span>
|
||||
: <span className='tag tag-default'>
|
||||
{_('pifStatusDisconnected')}
|
||||
{_('pifStatusDisconnected')}
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
@@ -68,21 +101,23 @@ export default ({
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
disabled={pif.attached && (pif.management || pif.disallowUnplug)}
|
||||
icon={pif.attached ? 'disconnect' : 'connect'}
|
||||
handler={pif.attached ? disconnectPif : connectPif}
|
||||
handlerParam={pif}
|
||||
icon={pif.attached ? 'disconnect' : 'connect'}
|
||||
tooltip={pif.attached ? _('disconnectPif') : _('connectPif')}
|
||||
/>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
disabled={pif.physical || pif.disallowUnplug || pif.management}
|
||||
icon='delete'
|
||||
handler={deletePif}
|
||||
handlerParam={{ pif }}
|
||||
icon='delete'
|
||||
tooltip={_('deletePif')}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</span>
|
||||
@@ -90,4 +125,4 @@ export default ({
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
</Container>)
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import _ from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import React, { Component } from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import TabButton from 'tab-button'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import React, { Component } from 'react'
|
||||
import { connectStore, formatSize } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { formatSize } from 'utils'
|
||||
import { createDoesHostNeedRestart } from 'selectors'
|
||||
import { FormattedRelative, FormattedTime } from 'react-intl'
|
||||
import { restartHost } from 'xo'
|
||||
|
||||
const MISSING_PATCH_COLUMNS = [
|
||||
{
|
||||
@@ -17,13 +19,18 @@ const MISSING_PATCH_COLUMNS = [
|
||||
},
|
||||
{
|
||||
name: _('patchDescription'),
|
||||
itemRenderer: patch => patch.description,
|
||||
itemRenderer: patch => (
|
||||
<a href={patch.documentationUrl} target='_blank'>
|
||||
{patch.description}
|
||||
</a>
|
||||
),
|
||||
sortCriteria: patch => patch.description
|
||||
},
|
||||
{
|
||||
name: _('patchReleaseDate'),
|
||||
itemRenderer: patch => <span><FormattedTime value={patch.date} day='numeric' month='long' year='numeric' /> (<FormattedRelative value={patch.date} />)</span>,
|
||||
sortCriteria: patch => patch.date
|
||||
sortCriteria: patch => patch.date,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
{
|
||||
name: _('patchGuidance'),
|
||||
@@ -46,59 +53,85 @@ const MISSING_PATCH_COLUMNS = [
|
||||
const INSTALLED_PATCH_COLUMNS = [
|
||||
{
|
||||
name: _('patchNameLabel'),
|
||||
itemRenderer: patch => patch.name,
|
||||
sortCriteria: patch => patch.name
|
||||
itemRenderer: patch => patch.poolPatch.name,
|
||||
sortCriteria: patch => patch.poolPatch.name
|
||||
},
|
||||
{
|
||||
name: _('patchDescription'),
|
||||
itemRenderer: patch => patch.description,
|
||||
sortCriteria: patch => patch.description
|
||||
itemRenderer: patch => patch.poolPatch.description,
|
||||
sortCriteria: patch => patch.poolPatch.description
|
||||
},
|
||||
{
|
||||
default: true,
|
||||
name: _('patchApplied'),
|
||||
itemRenderer: patch => {
|
||||
const time = patch.time * 1000
|
||||
return (
|
||||
<span>
|
||||
<FormattedTime value={time} day='numeric' month='long' year='numeric' />
|
||||
{' '}
|
||||
(<FormattedRelative value={time} />)
|
||||
</span>
|
||||
)
|
||||
},
|
||||
sortCriteria: patch => patch.time,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
{
|
||||
name: _('patchSize'),
|
||||
itemRenderer: patch => formatSize(patch.size),
|
||||
sortCriteria: patch => patch.size
|
||||
itemRenderer: patch => formatSize(patch.poolPatch.size),
|
||||
sortCriteria: patch => patch.poolPatch.size
|
||||
}
|
||||
]
|
||||
|
||||
@connectStore(() => ({
|
||||
needsRestart: createDoesHostNeedRestart((_, props) => props.host)
|
||||
}))
|
||||
export default class HostPatches extends Component {
|
||||
render () {
|
||||
const { poolPatches, missingPatches, installAllPatches, installPatch } = this.props
|
||||
return process.env.XOA_PLAN > 2
|
||||
const { host, hostPatches, missingPatches, installAllPatches, installPatch } = this.props
|
||||
return process.env.XOA_PLAN > 1
|
||||
? <Container>
|
||||
<Row>
|
||||
<Col>
|
||||
<Col className='text-xs-right'>
|
||||
{(this.props.needsRestart && isEmpty(missingPatches)) && <TabButton
|
||||
btnStyle='warning'
|
||||
handler={restartHost}
|
||||
handlerParam={host}
|
||||
icon='host-reboot'
|
||||
labelId='rebootUpdateHostLabel'
|
||||
/>}
|
||||
{isEmpty(missingPatches)
|
||||
? <h4>{_('hostUpToDate')}</h4>
|
||||
: <span>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
<TabButton
|
||||
btnStyle='primary'
|
||||
handler={installAllPatches}
|
||||
icon='host-patch-update'
|
||||
labelId='patchUpdateButton'
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<h3>{_('hostMissingPatches')}</h3>
|
||||
<SortedTable collection={missingPatches} userData={installPatch} columns={MISSING_PATCH_COLUMNS} />
|
||||
</Col>
|
||||
</Row>
|
||||
</span>
|
||||
? <TabButton
|
||||
disabled
|
||||
handler={installAllPatches}
|
||||
icon='success'
|
||||
labelId='hostUpToDate'
|
||||
/>
|
||||
: <TabButton
|
||||
btnStyle='primary'
|
||||
handler={installAllPatches}
|
||||
icon='host-patch-update'
|
||||
labelId='patchUpdateButton'
|
||||
/>
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
{!isEmpty(missingPatches) && <Row>
|
||||
<Col>
|
||||
<h3>{_('hostMissingPatches')}</h3>
|
||||
<SortedTable collection={missingPatches} userData={installPatch} columns={MISSING_PATCH_COLUMNS} />
|
||||
</Col>
|
||||
</Row>}
|
||||
<Row>
|
||||
<Col>
|
||||
{!isEmpty(poolPatches)
|
||||
? <span>
|
||||
<h3>{_('hostInstalledPatches')}</h3>
|
||||
<SortedTable collection={poolPatches} columns={INSTALLED_PATCH_COLUMNS} />
|
||||
{!isEmpty(hostPatches)
|
||||
? (
|
||||
<span>
|
||||
<h3>{_('hostAppliedPatches')}</h3>
|
||||
<SortedTable collection={hostPatches} columns={INSTALLED_PATCH_COLUMNS} />
|
||||
</span>
|
||||
: <h4 className='text-xs-center'>{_('patchNothing')}</h4>
|
||||
) : <h4 className='text-xs-center'>{_('patchNothing')}</h4>
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -2,6 +2,7 @@ import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import Upgrade from 'xoa-upgrade'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { Toggle } from 'form'
|
||||
@@ -95,9 +96,9 @@ export default class HostStats extends Component {
|
||||
<Row>
|
||||
<Col mediumSize={6}>
|
||||
<div className='form-group'>
|
||||
<label>{_('useCombinedValuesOnStats')}</label>
|
||||
{' '}
|
||||
<Toggle value={useCombinedValues} onChange={this.linkState('useCombinedValues')} />
|
||||
<Tooltip content={_('useStackedValuesOnStats')}>
|
||||
<Toggle value={useCombinedValues} onChange={this.linkState('useCombinedValues')} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
{selectStatsLoading && (
|
||||
<div className='text-xs-right'>
|
||||
|
||||
@@ -1,108 +1,149 @@
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import React from 'react'
|
||||
import _ from 'intl'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
import Link from 'link'
|
||||
import map from 'lodash/map'
|
||||
import { BlockLink } from 'link'
|
||||
import { TabButtonLink } from 'tab-button'
|
||||
import { formatSize } from 'utils'
|
||||
import React from 'react'
|
||||
import SortedTable from 'sorted-table'
|
||||
import Tooltip from 'tooltip'
|
||||
import { ButtonGroup } from 'react-bootstrap-4/lib'
|
||||
import { connectPbd, disconnectPbd, deletePbd, editSr, isSrShared } from 'xo'
|
||||
import { connectStore, formatSize } from 'utils'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
import { createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { TabButtonLink } from 'tab-button'
|
||||
import { Text } from 'editable'
|
||||
import { connectPbd, disconnectPbd, deletePbd, editSr } from 'xo'
|
||||
|
||||
export default ({
|
||||
host,
|
||||
srs,
|
||||
pbds
|
||||
}) => <Container>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
<TabButtonLink
|
||||
icon='add'
|
||||
labelId='addSrDeviceButton'
|
||||
to={`/new/sr?host=${host.id}`}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
{!isEmpty(pbds)
|
||||
? <span>
|
||||
<table className='table'>
|
||||
<thead className='thead-default'>
|
||||
<tr>
|
||||
<th>{_('srNameLabel')}</th>
|
||||
<th>{_('srFormat')}</th>
|
||||
<th>{_('srSize')}</th>
|
||||
<th>{_('srUsage')}</th>
|
||||
<th>{_('srType')}</th>
|
||||
<th>{_('pdbStatus')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{map(pbds, pbd => {
|
||||
const sr = srs[pbd.SR]
|
||||
return <BlockLink key={pbd.id} to={`/srs/${sr.id}/general`} tagName='tr'>
|
||||
<td>
|
||||
<Text value={sr.name_label} onChange={nameLabel => editSr(sr, { nameLabel })} useLongClick />
|
||||
</td>
|
||||
<td>{sr.SR_type}</td>
|
||||
<td>{formatSize(sr.size)}</td>
|
||||
<td>
|
||||
{sr.size > 1 &&
|
||||
<meter value={(sr.physical_usage / sr.size) * 100} min='0' max='100' optimum='40' low='80' high='90'></meter>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
{sr.$PBDs.length > 1
|
||||
? _('srShared')
|
||||
: _('srNotShared')
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
{pbd.attached
|
||||
? <span>
|
||||
<span className='tag tag-success'>
|
||||
{_('pbdStatusConnected')}
|
||||
</span>
|
||||
<ButtonGroup className='pull-xs-right'>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
icon='disconnect'
|
||||
handler={disconnectPbd}
|
||||
handlerParam={pbd}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</span>
|
||||
: <span>
|
||||
<span className='tag tag-default'>
|
||||
{_('pbdStatusDisconnected')}
|
||||
</span>
|
||||
<ButtonGroup className='pull-xs-right'>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
icon='connect'
|
||||
handler={connectPbd}
|
||||
handlerParam={pbd}
|
||||
/>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
icon='sr-forget'
|
||||
handler={deletePbd}
|
||||
handlerParam={pbd}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
</BlockLink>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
const SR_COLUMNS = [
|
||||
{
|
||||
name: _('srName'),
|
||||
itemRenderer: storage =>
|
||||
<Link to={`/srs/${storage.id}`}>
|
||||
<Text
|
||||
onChange={nameLabel => editSr(storage.id, { nameLabel })}
|
||||
useLongClick
|
||||
value={storage.nameLabel}
|
||||
/>
|
||||
</Link>,
|
||||
sortCriteria: 'nameLabel'
|
||||
},
|
||||
{
|
||||
name: _('srFormat'),
|
||||
itemRenderer: storage => storage.format,
|
||||
sortCriteria: 'format'
|
||||
},
|
||||
{
|
||||
name: _('srSize'),
|
||||
itemRenderer: storage => formatSize(storage.size),
|
||||
sortCriteria: 'size'
|
||||
},
|
||||
{
|
||||
default: true,
|
||||
name: _('srUsage'),
|
||||
itemRenderer: storage => storage.size !== 0 &&
|
||||
<Tooltip content={_('spaceLeftTooltip', {used: storage.usagePercentage, free: formatSize(storage.free)})}>
|
||||
<meter value={storage.usagePercentage} min='0' max='100' optimum='40' low='80' high='90' />
|
||||
</Tooltip>,
|
||||
sortCriteria: storage => storage.usagePercentage,
|
||||
sortOrder: 'desc'
|
||||
},
|
||||
{
|
||||
name: _('srType'),
|
||||
itemRenderer: storage => storage.shared ? _('srShared') : _('srNotShared'),
|
||||
sortCriteria: 'shared'
|
||||
},
|
||||
{
|
||||
name: _('pbdStatus'),
|
||||
itemRenderer: storage => storage.attached
|
||||
? <span>
|
||||
<span className='tag tag-success'>
|
||||
{_('pbdStatusConnected')}
|
||||
</span>
|
||||
: <h4 className='text-xs-center'>{_('pbdNoSr')}</h4>
|
||||
<ButtonGroup className='pull-xs-right'>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
handler={disconnectPbd}
|
||||
handlerParam={storage.pbdId}
|
||||
icon='disconnect'
|
||||
tooltip={_('pbdDisconnect')}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</span>
|
||||
: <span>
|
||||
<span className='tag tag-default'>
|
||||
{_('pbdStatusDisconnected')}
|
||||
</span>
|
||||
<ButtonGroup className='pull-xs-right'>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
handler={connectPbd}
|
||||
handlerParam={storage.pbdId}
|
||||
icon='connect'
|
||||
tooltip={_('pbdConnect')}
|
||||
/>
|
||||
<ActionRowButton
|
||||
btnStyle='default'
|
||||
handler={deletePbd}
|
||||
handlerParam={storage.pbdId}
|
||||
icon='sr-forget'
|
||||
tooltip={_('pbdForget')}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</span>
|
||||
}
|
||||
]
|
||||
|
||||
export default connectStore(() => {
|
||||
const pbds = createGetObjectsOfType('PBD').pick(
|
||||
(_, props) => props.host.$PBDs
|
||||
)
|
||||
const srs = createGetObjectsOfType('SR').pick(
|
||||
createSelector(
|
||||
pbds,
|
||||
pbds => map(pbds, pbd => pbd.SR)
|
||||
)
|
||||
)
|
||||
|
||||
const storages = createSelector(
|
||||
pbds,
|
||||
srs,
|
||||
(pbds, srs) => map(pbds, pbd => {
|
||||
const sr = srs[pbd.SR]
|
||||
const { physical_usage: usage, size } = sr
|
||||
|
||||
return {
|
||||
attached: pbd.attached,
|
||||
format: sr.SR_type,
|
||||
free: size > 0 ? size - usage : 0,
|
||||
id: sr.id,
|
||||
nameLabel: sr.name_label,
|
||||
pbdId: pbd.id,
|
||||
shared: isSrShared(sr),
|
||||
size: size > 0 ? size : 0,
|
||||
usagePercentage: size > 0 && Math.round(100 * usage / size)
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
})
|
||||
)
|
||||
|
||||
return { storages }
|
||||
})(({ host, storages }) =>
|
||||
<Container>
|
||||
<Row>
|
||||
<Col className='text-xs-right'>
|
||||
<TabButtonLink
|
||||
icon='add'
|
||||
labelId='addSrDeviceButton'
|
||||
to={`/new/sr?host=${host.id}`}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
{isEmpty(storages)
|
||||
? <h4 className='text-xs-center'>{_('pbdNoSr')}</h4>
|
||||
: <SortedTable columns={SR_COLUMNS} collection={storages} />
|
||||
}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import Component from 'base-component'
|
||||
import cookies from 'cookies-js'
|
||||
import Icon from 'icon'
|
||||
import isArray from 'lodash/isArray'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import Shortcuts from 'shortcuts'
|
||||
import _, { IntlProvider } from 'intl'
|
||||
import { blockXoaAccess } from 'xoa-updater'
|
||||
import { connectStore, routes } from 'utils'
|
||||
import { Notification } from 'notification'
|
||||
import { ShortcutManager } from 'react-shortcuts'
|
||||
import { TooltipViewer } from 'tooltip'
|
||||
import { Container, Row, Col } from 'grid'
|
||||
// import {
|
||||
// keyHandler
|
||||
// } from 'react-key-handler'
|
||||
@@ -29,6 +36,10 @@ import Vm from './vm'
|
||||
import VmImport from './vm-import'
|
||||
import XoaUpdates from './xoa-updates'
|
||||
|
||||
import keymap, { help } from '../keymap'
|
||||
|
||||
const shortcutManager = new ShortcutManager(keymap)
|
||||
|
||||
const CONTAINER_STYLE = {
|
||||
display: 'flex',
|
||||
minHeight: '100vh',
|
||||
@@ -80,6 +91,14 @@ const BODY_STYLE = {
|
||||
}
|
||||
})
|
||||
export default class XoApp extends Component {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
}
|
||||
static childContextTypes = {
|
||||
shortcuts: React.PropTypes.object.isRequired
|
||||
}
|
||||
getChildContext = () => ({ shortcuts: shortcutManager })
|
||||
|
||||
displayOpenSourceDisclaimer () {
|
||||
const previousDisclaimer = cookies.get('previousDisclaimer')
|
||||
const now = Math.floor(Date.now() / 1e3)
|
||||
@@ -101,12 +120,60 @@ export default class XoApp extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
_shortcutsHandler = (command, event) => {
|
||||
event.preventDefault()
|
||||
switch (command) {
|
||||
case 'GO_TO_HOSTS':
|
||||
this.context.router.push('home?t=host')
|
||||
break
|
||||
case 'GO_TO_POOLS':
|
||||
this.context.router.push('home?t=pool')
|
||||
break
|
||||
case 'GO_TO_VMS':
|
||||
this.context.router.push('home?t=VM')
|
||||
break
|
||||
case 'CREATE_VM':
|
||||
this.context.router.push('vms/new')
|
||||
break
|
||||
case 'UNFOCUS':
|
||||
if (event.target.tagName === 'INPUT') {
|
||||
event.target.blur()
|
||||
}
|
||||
break
|
||||
case 'HELP':
|
||||
alert(
|
||||
<span><Icon icon='shortcuts' />{' '}{_('shortcutModalTitle')}</span>,
|
||||
<Container>
|
||||
{map(help, (context, contextKey) => context.name && [
|
||||
<Row className='m-t-1' key={contextKey}>
|
||||
<Col>
|
||||
<h4>{context.name}</h4>
|
||||
</Col>
|
||||
</Row>,
|
||||
...map(context.shortcuts, ({ message, keys }, key) => message &&
|
||||
<Row key={`${contextKey}_${key}`}>
|
||||
<Col size={2} className='text-xs-right'>
|
||||
<strong>
|
||||
{isArray(keys) ? keys[0] : keys}
|
||||
</strong>
|
||||
</Col>
|
||||
<Col size={10}>{message}</Col>
|
||||
</Row>
|
||||
)
|
||||
])}
|
||||
</Container>
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { signedUp, trial } = this.props
|
||||
const blocked = signedUp && blockXoaAccess(trial) // If we are under expired or unstable trial (signed up only)
|
||||
|
||||
return <IntlProvider>
|
||||
<div style={CONTAINER_STYLE}>
|
||||
<Shortcuts name='XoApp' handler={this._shortcutsHandler} targetNodeSelector='body' stopPropagation={false} />
|
||||
<Menu ref='menu' />
|
||||
<div ref='bodyWrapper' style={BODY_WRAPPER_STYLE}>
|
||||
<div style={BODY_STYLE}>
|
||||
@@ -115,6 +182,7 @@ export default class XoApp extends Component {
|
||||
</div>
|
||||
<Modal />
|
||||
<Notification />
|
||||
<TooltipViewer />
|
||||
</div>
|
||||
</IntlProvider>
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ const Jobs = routes('overview', {
|
||||
scheduling: Scheduling,
|
||||
'scheduling/:id/edit': SchedulingEdit
|
||||
})(
|
||||
({ children }) => <Page header={HEADER}>{children}</Page>
|
||||
({ children }) => <Page header={HEADER} title='jobsPage' formatTitle>{children}</Page>
|
||||
)
|
||||
|
||||
export default Jobs
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import _ from 'intl'
|
||||
import _, { messages } from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import ActionRowButton from 'action-row-button'
|
||||
import delay from 'lodash/delay'
|
||||
@@ -15,6 +15,7 @@ import React, { Component } from 'react'
|
||||
import { error } from 'notification'
|
||||
import { generateUiSchema } from 'xo-json-schema-input'
|
||||
import { SelectPlainObject } from 'form'
|
||||
import { injectIntl } from 'react-intl'
|
||||
|
||||
import {
|
||||
apiMethods,
|
||||
@@ -81,6 +82,7 @@ const dataToParamVectorItems = function (params, data) {
|
||||
return items
|
||||
}
|
||||
|
||||
@injectIntl
|
||||
export default class Jobs extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
@@ -311,11 +313,13 @@ export default class Jobs extends Component {
|
||||
job,
|
||||
jobs
|
||||
} = this.state
|
||||
const { formatMessage } = this.props.intl
|
||||
|
||||
return <div>
|
||||
<h1>Jobs</h1>
|
||||
<h1>{_('jobsPage')}</h1>
|
||||
<form id='newJobForm'>
|
||||
<div className='form-group'>
|
||||
<input type='text' ref='name' className='form-control' placeholder='Name of your Job' required />
|
||||
<input type='text' ref='name' className='form-control' placeholder={formatMessage(messages.jobNamePlaceholder)} pattern='[^_]+' required />
|
||||
</div>
|
||||
<SelectPlainObject ref='method' options={actions} optionKey='method' onChange={this._handleSelectMethod} placeholder={_('jobActionPlaceHolder')} />
|
||||
{action && <fieldset>
|
||||
@@ -335,8 +339,8 @@ export default class Jobs extends Component {
|
||||
<tr>
|
||||
<th>{_('jobName')}</th>
|
||||
<th>{_('jobAction')}</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user