Compare commits
272 Commits
v5.2.4
...
xo-web/v5.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f95370124b | ||
|
|
2564343816 | ||
|
|
03734eb761 | ||
|
|
29d63a9fdd | ||
|
|
ca94b236a8 | ||
|
|
fa1ec30ba5 | ||
|
|
2b1423aebe | ||
|
|
373332141f | ||
|
|
ecf2cf15b5 | ||
|
|
4ee0831d93 | ||
|
|
7df2a88c13 | ||
|
|
3d52556c67 | ||
|
|
437b160a3f | ||
|
|
5c87b82e0c | ||
|
|
7f2bc79d5f | ||
|
|
837a61acf3 | ||
|
|
5971eed72a | ||
|
|
1b8224030b | ||
|
|
ed3ec3fa8b | ||
|
|
aa98ca49e5 | ||
|
|
44d35c2351 | ||
|
|
df8eb7a000 | ||
|
|
ac061c8750 | ||
|
|
656d3e55ac | ||
|
|
50641287f8 | ||
|
|
0bc072aa65 | ||
|
|
9d7d665520 | ||
|
|
819ea94e7b | ||
|
|
40753568df | ||
|
|
8793aed561 | ||
|
|
377a50bc09 | ||
|
|
fe5a43fbdf | ||
|
|
7f44220220 | ||
|
|
0df1610ca9 | ||
|
|
24c8b9e02d | ||
|
|
01b311f2ba | ||
|
|
a2bb3182f4 | ||
|
|
c86e15a310 | ||
|
|
862e5a95e7 | ||
|
|
73e2c7e849 | ||
|
|
0b0937e233 | ||
|
|
6bf114859f | ||
|
|
db6d67eeb7 | ||
|
|
a345d89aac | ||
|
|
e8f8ebb112 | ||
|
|
1dad5b5c3a | ||
|
|
5cc5ee4e87 | ||
|
|
e8d2b32a14 | ||
|
|
f492909e42 | ||
|
|
7ea17750a1 | ||
|
|
663e1f1a4b | ||
|
|
079310c67e | ||
|
|
5cf7f1f886 | ||
|
|
9f64af859e | ||
|
|
007aa776cb | ||
|
|
66bc092edd | ||
|
|
140a88ee12 | ||
|
|
f42758938d | ||
|
|
e19fd81536 | ||
|
|
73835ded96 | ||
|
|
1ec1a8bd94 | ||
|
|
f0b6d57ba8 | ||
|
|
f9a3ad14d1 | ||
|
|
1b86f533f7 | ||
|
|
46416fb026 | ||
|
|
54ed37c95d | ||
|
|
fd79b47d9e | ||
|
|
be8333824b | ||
|
|
55daffc791 | ||
|
|
375baf7fe5 | ||
|
|
815e74c93c | ||
|
|
547d6fbc93 | ||
|
|
b45a4b9e6c | ||
|
|
3436d0256a | ||
|
|
2627cfd426 | ||
|
|
34b18c00a1 | ||
|
|
e13af7f5f0 | ||
|
|
ca08613292 | ||
|
|
4ab63591a0 | ||
|
|
5b4f98b03b | ||
|
|
f396d61633 | ||
|
|
9f8c0c8cdf | ||
|
|
198777ffab | ||
|
|
29c5ca1132 | ||
|
|
05d6f3d1ed | ||
|
|
536e82de3d | ||
|
|
c59be7c315 | ||
|
|
b327bb5bd0 | ||
|
|
a3103587f5 | ||
|
|
1bb11b574f | ||
|
|
405efe6a31 | ||
|
|
73663c3703 | ||
|
|
421ee7125b | ||
|
|
1a6166b63c | ||
|
|
3828e75b7d | ||
|
|
154da142c7 | ||
|
|
312cd60dd1 | ||
|
|
6bf522f72f | ||
|
|
a844f8d459 | ||
|
|
8ee206174b | ||
|
|
1a08e24a5c | ||
|
|
086cd0e038 | ||
|
|
42d123318c | ||
|
|
89f160317c | ||
|
|
9ccd1a0362 | ||
|
|
d116d014bc | ||
|
|
7956cabcf4 | ||
|
|
36c61ad357 | ||
|
|
25d60360d5 | ||
|
|
1e5579e3ad | ||
|
|
77d43b2280 | ||
|
|
33e8929e8b | ||
|
|
b79fa9cb9f | ||
|
|
a2812a85bd | ||
|
|
e8ff46a8ba | ||
|
|
351c01d642 | ||
|
|
e333b1d083 | ||
|
|
5ad49de642 | ||
|
|
b45bb5c144 | ||
|
|
9402596f69 | ||
|
|
096687ae2c | ||
|
|
210b5de992 | ||
|
|
f742fdbf1b | ||
|
|
e7026c522d | ||
|
|
c21fc4beda | ||
|
|
edf6fe782e | ||
|
|
3cbb6c4a98 | ||
|
|
568a50acc5 | ||
|
|
fbcb756cef | ||
|
|
81eb4ba4f9 | ||
|
|
0cc14d2ab8 | ||
|
|
6aedadc982 | ||
|
|
a8d10dab3c | ||
|
|
1ff6ff1d7a | ||
|
|
8afe4a85dc | ||
|
|
c57fbdce63 | ||
|
|
bdc0278fd1 | ||
|
|
c3ac8d0587 | ||
|
|
f3a5e1e97c | ||
|
|
919aa5fc43 | ||
|
|
416c98ffd2 | ||
|
|
8094447183 | ||
|
|
575375d3e0 | ||
|
|
4296ae02dc | ||
|
|
0e40af0515 | ||
|
|
5d3a0e7a41 | ||
|
|
8ae2aae37a | ||
|
|
83b3cf406a | ||
|
|
1643ced4e0 | ||
|
|
b2a1840da7 | ||
|
|
b9f20d1e80 | ||
|
|
0c77781be8 | ||
|
|
83245af1e2 | ||
|
|
7db806a461 | ||
|
|
92b15fb1e2 | ||
|
|
7b5182111c | ||
|
|
82b1b81999 | ||
|
|
f0a430f350 | ||
|
|
90f95b7270 | ||
|
|
15e6a93fac | ||
|
|
01541a2577 | ||
|
|
8c70bc0a17 | ||
|
|
9d96074604 | ||
|
|
114a4028f4 | ||
|
|
b342a4ba17 | ||
|
|
fcbf037619 | ||
|
|
a8e4ab433d | ||
|
|
6613ba02ab | ||
|
|
2af7fde83f | ||
|
|
19a0d4bc98 | ||
|
|
9ed49b1f27 | ||
|
|
d56df30a22 | ||
|
|
64908068d9 | ||
|
|
fe69d59aeb | ||
|
|
b65e737f84 | ||
|
|
bd274fdc3c | ||
|
|
ac19249c63 | ||
|
|
2abff1fec8 | ||
|
|
f1a6cfae0d | ||
|
|
e43e90ed3c | ||
|
|
0ee88fe0dc | ||
|
|
07e7f2e14d | ||
|
|
366ab95a2f | ||
|
|
ca723068a1 | ||
|
|
e424a105b3 | ||
|
|
32d2f92413 | ||
|
|
898e2ff010 | ||
|
|
dfa5e76870 | ||
|
|
c93dd12fae | ||
|
|
dbb1b1e582 | ||
|
|
76388ee160 | ||
|
|
5ec2eee69a | ||
|
|
31875a36fe | ||
|
|
c50598b78e | ||
|
|
2f0c81d9ad | ||
|
|
c22f89c6bb | ||
|
|
568a23cd35 | ||
|
|
eb7c4c131d | ||
|
|
f0664cd2c7 | ||
|
|
570eb7bc89 | ||
|
|
1ee91b4925 | ||
|
|
69fee37f00 | ||
|
|
49be66ae69 | ||
|
|
a0efe6895c | ||
|
|
8ef07e917d | ||
|
|
d3995b7bab | ||
|
|
c353e71ce7 | ||
|
|
a3570a1c9f | ||
|
|
c593c98e6d | ||
|
|
a4b5b532f2 | ||
|
|
6357f23aeb | ||
|
|
01d9b3bd0e | ||
|
|
6b428f7587 | ||
|
|
f829aa76d7 | ||
|
|
a72051e96f | ||
|
|
797622ba66 | ||
|
|
39342cd662 | ||
|
|
051a3ac122 | ||
|
|
f842a321ba | ||
|
|
3cd2dd65d3 | ||
|
|
5ce7e0b108 | ||
|
|
71c2058cc8 | ||
|
|
f200d39d23 | ||
|
|
7932845ac5 | ||
|
|
94bda6ac9e | ||
|
|
7a65f80406 | ||
|
|
36ab58dad9 | ||
|
|
e9be9e3761 | ||
|
|
b54645c86c | ||
|
|
ab77d8430c | ||
|
|
c6f683b532 | ||
|
|
a2604f5156 | ||
|
|
5ae7f683d6 | ||
|
|
f953c89979 | ||
|
|
bb8aab02ea | ||
|
|
af0c03ff6a | ||
|
|
8859900537 | ||
|
|
130852ab85 | ||
|
|
65fa8f96b4 | ||
|
|
0a84e9e363 | ||
|
|
163c69454b | ||
|
|
49d3fde0f3 | ||
|
|
bb67e2254e | ||
|
|
6d2abc4e74 | ||
|
|
4875450053 | ||
|
|
19184ca8a0 | ||
|
|
654c3d324b | ||
|
|
c5b4811f16 | ||
|
|
7a9dc4fd59 | ||
|
|
e79096626a | ||
|
|
332d074d32 | ||
|
|
e511ecd76e | ||
|
|
bcfbd5eba9 | ||
|
|
9fa3db395b | ||
|
|
52a41ceb04 | ||
|
|
e65d67266d | ||
|
|
0d1045821c | ||
|
|
45d526dda2 | ||
|
|
e52f998e78 | ||
|
|
42ed3b9355 | ||
|
|
563b4cb1ec | ||
|
|
45bad231cf | ||
|
|
d76bd2484b | ||
|
|
445b60bb63 | ||
|
|
3214e0e41e | ||
|
|
c61230e145 | ||
|
|
fac6a29226 | ||
|
|
7a8f414748 | ||
|
|
9f450d282e | ||
|
|
31787067e3 | ||
|
|
1a769b23e2 | ||
|
|
ae002abafc |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,7 @@
|
||||
/dist/
|
||||
/node_modules/
|
||||
/src/common/intl/locales/index.js
|
||||
/src/common/themes/index.js
|
||||
|
||||
npm-debug.log
|
||||
npm-debug.log.*
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 'stable'
|
||||
#- '4' # Disabled for now because npm 2 cannot properly handled broken peer dependencies.
|
||||
- '6'
|
||||
#- '4' # npm 3's flat tree is needed because some packages do not
|
||||
# declare their deps correctly (e.g. chartist-plugin-tooltip)
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
cache: yarn
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
|
||||
224
CHANGELOG.md
224
CHANGELOG.md
@@ -1,9 +1,172 @@
|
||||
# ChangeLog
|
||||
|
||||
## **5.2.3** (2016-10-03)
|
||||
## **5.7.0** (2017-03-31)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Improve ActionButton error reporting [\#2048](https://github.com/vatesfr/xo-web/issues/2048)
|
||||
- Home view master checkbox UI issue [\#2027](https://github.com/vatesfr/xo-web/issues/2027)
|
||||
- HU Translation [\#2019](https://github.com/vatesfr/xo-web/issues/2019)
|
||||
- [Usage report] Add name for all objects [\#2017](https://github.com/vatesfr/xo-web/issues/2017)
|
||||
- [Home] Improve inter-types linkage [\#2012](https://github.com/vatesfr/xo-web/issues/2012)
|
||||
- Remove bootable checkboxes in VM creation [\#2007](https://github.com/vatesfr/xo-web/issues/2007)
|
||||
- Do not display bootable toggles for disks of non-PV VMs [\#1996](https://github.com/vatesfr/xo-web/issues/1996)
|
||||
- Try to match network VLAN for VM migration modal [\#1990](https://github.com/vatesfr/xo-web/issues/1990)
|
||||
- [Usage reports] Add VM names in addition to UUIDs [\#1984](https://github.com/vatesfr/xo-web/issues/1984)
|
||||
- Host affinity in "advanced" VM creation [\#1983](https://github.com/vatesfr/xo-web/issues/1983)
|
||||
- Add job tag in backup logs [\#1982](https://github.com/vatesfr/xo-web/issues/1982)
|
||||
- Possibility to add a label/description to servers [\#1965](https://github.com/vatesfr/xo-web/issues/1965)
|
||||
- Possibility to create shared VM in a resource set [\#1964](https://github.com/vatesfr/xo-web/issues/1964)
|
||||
- Clearer display of disabled (backup) jobs [\#1958](https://github.com/vatesfr/xo-web/issues/1958)
|
||||
- Job should have a configurable timeout [\#1956](https://github.com/vatesfr/xo-web/issues/1956)
|
||||
- Sort failed VMs in backup report [\#1950](https://github.com/vatesfr/xo-web/issues/1950)
|
||||
- Support for UNIX socket path [\#1944](https://github.com/vatesfr/xo-web/issues/1944)
|
||||
- Interface - Host Patching - Button Verbiage [\#1911](https://github.com/vatesfr/xo-web/issues/1911)
|
||||
- Display if a VM is in Self Service (and which group) [\#1905](https://github.com/vatesfr/xo-web/issues/1905)
|
||||
- Install supplemental pack on a whole pool [\#1896](https://github.com/vatesfr/xo-web/issues/1896)
|
||||
- Allow VM snapshots with ACLs [\#1865](https://github.com/vatesfr/xo-web/issues/1886)
|
||||
- Icon to indicate if a snapshot is quiesce [\#1858](https://github.com/vatesfr/xo-web/issues/1858)
|
||||
- Pool Ips input too permissive [\#1731](https://github.com/vatesfr/xo-web/issues/1731)
|
||||
- Select is going on top after each choice [\#1359](https://github.com/vatesfr/xo-web/issues/1359)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Missing objects should be displayed in backup edition [\#2052](https://github.com/vatesfr/xo-web/issues/2052)
|
||||
- Search bar content changes while typing [\#2035](https://github.com/vatesfr/xo-web/issues/2035)
|
||||
- VM.$guest_metrics.PV_drivers_up_to_date is deprecated in XS 7.1 [\#2024](https://github.com/vatesfr/xo-web/issues/2024)
|
||||
- Bootable flag selection checkbox for extra disk not fetched [\#1994](https://github.com/vatesfr/xo-web/issues/1994)
|
||||
- Home view − Changing type must reset paging [\#1993](https://github.com/vatesfr/xo-web/issues/1993)
|
||||
- XOSAN menu item should only be displayed to admins [\#1968](https://github.com/vatesfr/xo-web/issues/1968)
|
||||
- Object type change are not correctly handled in UI [\#1967](https://github.com/vatesfr/xo-web/issues/1967)
|
||||
- VM creation is stuck when using ISO/DVD as install method [\#1966](https://github.com/vatesfr/xo-web/issues/1966)
|
||||
- Install pack on whole pool fails [\#1957](https://github.com/vatesfr/xo-web/issues/1957)
|
||||
- Consoles are broken in next-release [\#1954](https://github.com/vatesfr/xo-web/issues/1954)
|
||||
- [VHD merge] Increase BAT when necessary [\#1939](https://github.com/vatesfr/xo-web/issues/1939)
|
||||
- Issue on VM restore time [\#1936](https://github.com/vatesfr/xo-web/issues/1936)
|
||||
- Two remotes should not be able to have the same name [\#1879](https://github.com/vatesfr/xo-web/issues/1879)
|
||||
- Selfservice limits not honored after VM creation [\#1695](https://github.com/vatesfr/xo-web/issues/1695)
|
||||
|
||||
## **5.6.0** (2017-01-27)
|
||||
|
||||
Reporting, LVM File level restore.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Do not stop patches install if already applied [\#1904](https://github.com/vatesfr/xo-web/issues/1904)
|
||||
- Improve scheduling UI [\#1893](https://github.com/vatesfr/xo-web/issues/1893)
|
||||
- Smart backup and tag [\#1885](https://github.com/vatesfr/xo-web/issues/1885)
|
||||
- Missing embeded API documention [\#1882](https://github.com/vatesfr/xo-web/issues/1882)
|
||||
- Add local DVD in CD selector [\#1880](https://github.com/vatesfr/xo-web/issues/1880)
|
||||
- File level restore for LVM [\#1878](https://github.com/vatesfr/xo-web/issues/1878)
|
||||
- Restore multiple files from file level restore [\#1877](https://github.com/vatesfr/xo-web/issues/1877)
|
||||
- Add a VM tab for host & pool views [\#1864](https://github.com/vatesfr/xo-web/issues/1864)
|
||||
- Icon to indicate if a snapshot is quiesce [\#1858](https://github.com/vatesfr/xo-web/issues/1858)
|
||||
- UI for disconnect hosts comp [\#1833](https://github.com/vatesfr/xo-web/issues/1833)
|
||||
- Eject all xs-guest.iso in a pool [\#1798](https://github.com/vatesfr/xo-web/issues/1798)
|
||||
- Display installed supplemental pack on host [\#1506](https://github.com/vatesfr/xo-web/issues/1506)
|
||||
- Install supplemental pack on host comp [\#1460](https://github.com/vatesfr/xo-web/issues/1460)
|
||||
- Pool-wide combined stats [\#1324](https://github.com/vatesfr/xo-web/issues/1324)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- IP-address not released when VM removed [\#1906](https://github.com/vatesfr/xo-web/issues/1906)
|
||||
- Interface broken due to new Bootstrap Alpha [\#1871](https://github.com/vatesfr/xo-web/issues/1871)
|
||||
- Self service recompute all limits broken [\#1866](https://github.com/vatesfr/xo-web/issues/1866)
|
||||
- Patch not found error for XS 6.5 [\#1863](https://github.com/vatesfr/xo-web/issues/1863)
|
||||
- Convert To Template issues [\#1855](https://github.com/vatesfr/xo-web/issues/1855)
|
||||
- Removing PIF seems to fail [\#1853](https://github.com/vatesfr/xo-web/issues/1853)
|
||||
- Depth should be >= 1 in backup creation [\#1851](https://github.com/vatesfr/xo-web/issues/1851)
|
||||
- Wrong link in Dashboard > Health [\#1850](https://github.com/vatesfr/xo-web/issues/1850)
|
||||
- Incorrect file dates shown in new File Restore feature [\#1840](https://github.com/vatesfr/xo-web/issues/1840)
|
||||
- IP allocation problem [\#1747](https://github.com/vatesfr/xo-web/issues/1747)
|
||||
- Selfservice limits not honored after VM creation [\#1695](https://github.com/vatesfr/xo-web/issues/1695)
|
||||
|
||||
## **5.5.0** (2016-12-20)
|
||||
|
||||
File level restore.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Better auto select network when migrate VM [\#1788](https://github.com/vatesfr/xo-web/issues/1788)
|
||||
- Plugin for passive backup job reporting in Nagios [\#1664](https://github.com/vatesfr/xo-web/issues/1664)
|
||||
- File level restore for delta backup [\#1590](https://github.com/vatesfr/xo-web/issues/1590)
|
||||
- Better select filters for ACLs [\#1515](https://github.com/vatesfr/xo-web/issues/1515)
|
||||
- All pools and "negative" filters [\#1503](https://github.com/vatesfr/xo-web/issues/1503)
|
||||
- VM copy with disk selection [\#826](https://github.com/vatesfr/xo-web/issues/826)
|
||||
- Disable metadata exports [\#1818](https://github.com/vatesfr/xo-web/issues/1818)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Tool small selector [\#1832](https://github.com/vatesfr/xo-web/issues/1832)
|
||||
- Replication does not work from a VM created by a CR or delta backup [\#1811](https://github.com/vatesfr/xo-web/issues/1811)
|
||||
- Can't add a SSH key in VM creation [\#1805](https://github.com/vatesfr/xo-web/issues/1805)
|
||||
- Issue when no default SR in a pool [\#1804](https://github.com/vatesfr/xo-web/issues/1804)
|
||||
- XOA doesn't refresh after an update anymore [\#1801](https://github.com/vatesfr/xo-web/issues/1801)
|
||||
- Shortcuts not inhibited on inputs on Safari [\#1691](https://github.com/vatesfr/xo-web/issues/1691)
|
||||
|
||||
## **5.4.0** (2016-11-23)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- XML display in alerts [\#1776](https://github.com/vatesfr/xo-web/issues/1776)
|
||||
- Remove some view for non admin users [\#1773](https://github.com/vatesfr/xo-web/issues/1773)
|
||||
- Complex matcher should support matching boolean values [\#1768](https://github.com/vatesfr/xo-web/issues/1768)
|
||||
- Home SR view [\#1764](https://github.com/vatesfr/xo-web/issues/1764)
|
||||
- Filter on tag click [\#1763](https://github.com/vatesfr/xo-web/issues/1763)
|
||||
- Testable plugins [\#1749](https://github.com/vatesfr/xo-web/issues/1749)
|
||||
- Backup/Restore Design fix. [\#1734](https://github.com/vatesfr/xo-web/issues/1734)
|
||||
- Display the owner of a \(backup\) job [\#1733](https://github.com/vatesfr/xo-web/issues/1733)
|
||||
- Use paginated table for backup jobs [\#1726](https://github.com/vatesfr/xo-web/issues/1726)
|
||||
- SR view / Disks: should display snapshot VDIs [\#1723](https://github.com/vatesfr/xo-web/issues/1723)
|
||||
- Restored VM should have an identifiable name [\#1719](https://github.com/vatesfr/xo-web/issues/1719)
|
||||
- If host reboot action returns NO\_HOSTS\_AVAILABLE, ask to force [\#1717](https://github.com/vatesfr/xo-web/issues/1717)
|
||||
- Hide xo-server timezone in backups [\#1706](https://github.com/vatesfr/xo-web/issues/1706)
|
||||
- Enable hyperlink for Hostname for Issues [\#1700](https://github.com/vatesfr/xo-web/issues/1700)
|
||||
- Pool/network - Modify column [\#1696](https://github.com/vatesfr/xo-web/issues/1696)
|
||||
- UI - Plugins - Display a message if no plugins [\#1670](https://github.com/vatesfr/xo-web/issues/1670)
|
||||
- Display warning/error for delta backup on XS older than 6.5 [\#1647](https://github.com/vatesfr/xo-web/issues/1647)
|
||||
- XO without internet access doesn't work [\#1629](https://github.com/vatesfr/xo-web/issues/1629)
|
||||
- Improve backup restore view [\#1609](https://github.com/vatesfr/xo-web/issues/1609)
|
||||
- UI Enhancement - Acronym for dummy [\#1604](https://github.com/vatesfr/xo-web/issues/1604)
|
||||
- Slack XO plugin for backup report [\#1593](https://github.com/vatesfr/xo-web/issues/1593)
|
||||
- Expose XAPI exceptions in the UI [\#1481](https://github.com/vatesfr/xo-web/issues/1481)
|
||||
- Running VMs in the host overview, all VMs in the pool overview [\#1432](https://github.com/vatesfr/xo-web/issues/1432)
|
||||
- Move location of NFS mount point [\#1405](https://github.com/vatesfr/xo-web/issues/1405)
|
||||
- Home: Pool list - additionnal informations for pool [\#1226](https://github.com/vatesfr/xo-web/issues/1226)
|
||||
- Modify VLAN of an existing network [\#1092](https://github.com/vatesfr/xo-web/issues/1092)
|
||||
- Wrong instructions for CLI upgrade [\#787](https://github.com/vatesfr/xo-web/issues/787)
|
||||
- Ability to export/import XO config [\#786](https://github.com/vatesfr/xo-web/issues/786)
|
||||
- Test button for transport-email plugin [\#697](https://github.com/vatesfr/xo-web/issues/697)
|
||||
- Merge `scheduler` API into `schedule` [\#664](https://github.com/vatesfr/xo-web/issues/664)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Should jobs be accessible to non admins? [\#1759](https://github.com/vatesfr/xo-web/issues/1759)
|
||||
- Schedules deletion is not working [\#1737](https://github.com/vatesfr/xo-web/issues/1737)
|
||||
- Editing a job from the jobs overview page does not work [\#1736](https://github.com/vatesfr/xo-web/issues/1736)
|
||||
- Editing a schedule from jobs overview does not work [\#1728](https://github.com/vatesfr/xo-web/issues/1728)
|
||||
- ACLs not correctly imported [\#1722](https://github.com/vatesfr/xo-web/issues/1722)
|
||||
- Some Bootstrap style broken [\#1721](https://github.com/vatesfr/xo-web/issues/1721)
|
||||
- Not properly sign out on auth token expiration [\#1711](https://github.com/vatesfr/xo-web/issues/1711)
|
||||
- Hosts/<UUID>/network status is incorrect [\#1702](https://github.com/vatesfr/xo-web/issues/1702)
|
||||
- Patches application fails "Found : Moved Temporarily" [\#1701](https://github.com/vatesfr/xo-web/issues/1701)
|
||||
- Password generation for user creation is not working [\#1678](https://github.com/vatesfr/xo-web/issues/1678)
|
||||
- \#/dashboard/health Remove All Orphaned VDIs [\#1622](https://github.com/vatesfr/xo-web/issues/1622)
|
||||
- Create a new SR - CIFS/SAMBA Broken [\#1615](https://github.com/vatesfr/xo-web/issues/1615)
|
||||
- xo-cli --list-objects: truncated output ? 64k buffer limitation ? [\#1356](https://github.com/vatesfr/xo-web/issues/1356)
|
||||
|
||||
## **5.3.0** (2016-10-20)
|
||||
|
||||
### Enhancements
|
||||
|
||||
- Missing favicon [\#1660](https://github.com/vatesfr/xo-web/issues/1660)
|
||||
- ipPools quota [\#1565](https://github.com/vatesfr/xo-web/issues/1565)
|
||||
- Dashboard - orphaned VDI [\#1654](https://github.com/vatesfr/xo-web/issues/1654)
|
||||
- Stats in home/host view when expanded [\#1634](https://github.com/vatesfr/xo-web/issues/1634)
|
||||
- Bar for used and total RAM on home pool view [\#1625](https://github.com/vatesfr/xo-web/issues/1625)
|
||||
- Can't translate some text [\#1624](https://github.com/vatesfr/xo-web/issues/1624)
|
||||
- Dynamic RAM allocation at creation time [\#1603](https://github.com/vatesfr/xo-web/issues/1603)
|
||||
- Display memory bar in home/host view [\#1616](https://github.com/vatesfr/xo-web/issues/1616)
|
||||
- Improve keyboard navigation [\#1578](https://github.com/vatesfr/xo-web/issues/1578)
|
||||
- Strongly suggest to install the guest tools [\#1575](https://github.com/vatesfr/xo-web/issues/1575)
|
||||
- Missing tooltip [\#1568](https://github.com/vatesfr/xo-web/issues/1568)
|
||||
@@ -13,24 +176,6 @@
|
||||
- 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)
|
||||
@@ -48,9 +193,36 @@
|
||||
- Color code on host PIF stats can be misleading [\#1265](https://github.com/vatesfr/xo-web/issues/1265)
|
||||
- Sign in page is not rendered correctly [\#1161](https://github.com/vatesfr/xo-web/issues/1161)
|
||||
- Template management [\#1091](https://github.com/vatesfr/xo-web/issues/1091)
|
||||
- On pool view: collapse network list [\#1461](https://github.com/vatesfr/xo-web/issues/1461)
|
||||
- Alert when trying to reboot/halt the pool master XS [\#1458](https://github.com/vatesfr/xo-web/issues/1458)
|
||||
- Adding tooltip on Home page [\#1456](https://github.com/vatesfr/xo-web/issues/1456)
|
||||
- Docker container management functionality missing from v5 [\#1442](https://github.com/vatesfr/xo-web/issues/1442)
|
||||
- bad error message - delete snapshot [\#1433](https://github.com/vatesfr/xo-web/issues/1433)
|
||||
- Create tag during VM creation [\#1431](https://github.com/vatesfr/xo-web/issues/1431)
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Display issues on plugin array edition [\#1663](https://github.com/vatesfr/xo-web/issues/1663)
|
||||
- Import of delta backups fails [\#1656](https://github.com/vatesfr/xo-web/issues/1656)
|
||||
- Host - Missing IP config for PIF [\#1651](https://github.com/vatesfr/xo-web/issues/1651)
|
||||
- Remote copy is always activating compression [\#1645](https://github.com/vatesfr/xo-web/issues/1645)
|
||||
- LB plugin UI problems [\#1630](https://github.com/vatesfr/xo-web/issues/1630)
|
||||
- Keyboard shortcuts should not work when a modal is open [\#1589](https://github.com/vatesfr/xo-web/issues/1589)
|
||||
- UI small bug in drop-down lists [\#1411](https://github.com/vatesfr/xo-web/issues/1411)
|
||||
- md5 delta backup error [\#1672](https://github.com/vatesfr/xo-web/issues/1672)
|
||||
- Can't edit VIF network [\#1640](https://github.com/vatesfr/xo-web/issues/1640)
|
||||
- Do not expose shortcuts while console is focused [\#1614](https://github.com/vatesfr/xo-web/issues/1614)
|
||||
- All users can see VM templates [\#1621](https://github.com/vatesfr/xo-web/issues/1621)
|
||||
- Profile page is broken [\#1612](https://github.com/vatesfr/xo-web/issues/1612)
|
||||
- SR delete should redirect to home [\#1611](https://github.com/vatesfr/xo-web/issues/1611)
|
||||
- Delta VHD backup checksum is invalidated by chaining [\#1606](https://github.com/vatesfr/xo-web/issues/1606)
|
||||
- VM with long description break on 2 lines [\#1580](https://github.com/vatesfr/xo-web/issues/1580)
|
||||
- Network status on VM edition [\#1573](https://github.com/vatesfr/xo-web/issues/1573)
|
||||
- VM template deletion fails [\#1571](https://github.com/vatesfr/xo-web/issues/1571)
|
||||
- Template edition - "no such object" [\#1569](https://github.com/vatesfr/xo-web/issues/1569)
|
||||
- missing links / element not displayed as links [\#1567](https://github.com/vatesfr/xo-web/issues/1567)
|
||||
- Backup restore stalled on some SMB shares [\#1412](https://github.com/vatesfr/xo-web/issues/1412)
|
||||
- Wrong bond display [\#1156](https://github.com/vatesfr/xo-web/issues/1156)
|
||||
- Multiple reboot selection doesn't work [\#1562](https://github.com/vatesfr/xo-web/issues/1562)
|
||||
- Server logs should be displayed in reverse chonological order [\#1547](https://github.com/vatesfr/xo-web/issues/1547)
|
||||
- Cannot create resource sets without limits [\#1537](https://github.com/vatesfr/xo-web/issues/1537)
|
||||
@@ -65,20 +237,6 @@
|
||||
- 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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Xen Orchestra Web [](https://travis-ci.org/vatesfr/xo-web)
|
||||
# Xen Orchestra Web [](https://go.crisp.im/chat/embed/?website_id=-JzqzzwddSV7bKGtEyAQ) [](https://travis-ci.org/vatesfr/xo-web)
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ function browserify (path, opts) {
|
||||
|
||||
var bundler = require('browserify')(path, {
|
||||
basedir: SRC_DIR,
|
||||
debug: DEVELOPMENT, // TODO: enable also in production but need to make it work with gulp-uglify.
|
||||
debug: true,
|
||||
extensions: opts.extensions,
|
||||
fullPaths: false,
|
||||
paths: SRC_DIR + '/common',
|
||||
@@ -238,8 +238,8 @@ function browserify (path, opts) {
|
||||
|
||||
gulp.task(function buildPages () {
|
||||
return pipe(
|
||||
src('index.jade', { sourcemaps: true }),
|
||||
require('gulp-jade')(),
|
||||
src('index.pug', { sourcemaps: true }),
|
||||
require('gulp-pug')(),
|
||||
DEVELOPMENT && require('gulp-embedlr')({
|
||||
port: LIVERELOAD_PORT
|
||||
}),
|
||||
@@ -257,6 +257,7 @@ gulp.task(function buildScripts () {
|
||||
}]
|
||||
]
|
||||
}),
|
||||
require('gulp-sourcemaps').init({ loadMaps: true }),
|
||||
PRODUCTION && require('gulp-uglify')(),
|
||||
dest()
|
||||
)
|
||||
|
||||
96
package.json
96
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "xo-web",
|
||||
"version": "5.2.4",
|
||||
"version": "5.7.1",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "Web interface client for Xen-Orchestra",
|
||||
"keywords": [
|
||||
@@ -33,8 +33,9 @@
|
||||
"devDependencies": {
|
||||
"ansi_up": "^1.3.0",
|
||||
"asap": "^2.0.4",
|
||||
"ava": "^0.16.0",
|
||||
"babel-eslint": "^7.0.0",
|
||||
"babel-plugin-dev": "^1.0.0",
|
||||
"babel-plugin-lodash": "^3.2.11",
|
||||
"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",
|
||||
@@ -44,67 +45,77 @@
|
||||
"babel-preset-es2015": "^6.6.0",
|
||||
"babel-preset-react": "^6.5.0",
|
||||
"babel-preset-stage-0": "^6.5.0",
|
||||
"babel-register": "^6.16.3",
|
||||
"babel-runtime": "^6.6.1",
|
||||
"babelify": "^7.2.0",
|
||||
"benchmark": "^2.1.0",
|
||||
"bootstrap": "github:twbs/bootstrap#v4-dev",
|
||||
"browserify": "^13.0.0",
|
||||
"bootstrap": "4.0.0-alpha.5",
|
||||
"browserify": "^14.1.0",
|
||||
"bundle-collapser": "^1.2.1",
|
||||
"chartist-plugin-legend": "^0.5.0",
|
||||
"chartist": "^0.10.1",
|
||||
"chartist-plugin-legend": "^0.6.1",
|
||||
"chartist-plugin-tooltip": "0.0.11",
|
||||
"classnames": "^2.2.3",
|
||||
"cookies-js": "^1.2.2",
|
||||
"d3": "^4.0.0-alpha.50",
|
||||
"d3": "^4.2.8",
|
||||
"dependency-check": "^2.5.1",
|
||||
"enzyme": "^2.6.0",
|
||||
"enzyme-to-json": "^1.4.4",
|
||||
"event-to-promise": "^0.7.0",
|
||||
"font-awesome": "^4.5.0",
|
||||
"font-awesome": "^4.7.0",
|
||||
"font-mfizz": "github:fizzed/font-mfizz",
|
||||
"get-stream": "^2.3.0",
|
||||
"ghooks": "^1.1.1",
|
||||
"globby": "^6.0.0",
|
||||
"gulp": "github:gulpjs/gulp#4.0",
|
||||
"gulp-autoprefixer": "^3.1.0",
|
||||
"gulp-csso": "^2.0.0",
|
||||
"gulp-csso": "^3.0.0",
|
||||
"gulp-embedlr": "^0.5.2",
|
||||
"gulp-jade": "^1.1.0",
|
||||
"gulp-plumber": "^1.1.0",
|
||||
"gulp-pug": "^3.1.0",
|
||||
"gulp-refresh": "^1.1.0",
|
||||
"gulp-sass": "^2.2.0",
|
||||
"gulp-sass": "^3.0.0",
|
||||
"gulp-sourcemaps": "^2.2.3",
|
||||
"gulp-uglify": "^2.0.0",
|
||||
"gulp-watch": "^4.3.5",
|
||||
"human-format": "^0.6.0",
|
||||
"human-format": "^0.7.0",
|
||||
"husky": "^0.13.1",
|
||||
"index-modules": "^0.3.0",
|
||||
"is-ip": "^1.0.0",
|
||||
"jsonrpc-websocket-client": "0.0.1-5",
|
||||
"jest": "^19.0.2",
|
||||
"jsonrpc-websocket-client": "^0.1.1",
|
||||
"kindof": "^2.0.0",
|
||||
"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.27.1",
|
||||
"modular-css": "^4.1.1",
|
||||
"moment": "^2.13.0",
|
||||
"moment-timezone": "^0.5.4",
|
||||
"notifyjs": "^2.0.1",
|
||||
"notifyjs": "^3.0.0",
|
||||
"novnc-node": "^0.5.3",
|
||||
"promise-toolbox": "^0.5.0",
|
||||
"promise-toolbox": "^0.8.0",
|
||||
"random-password": "^0.1.2",
|
||||
"react": "^15.0.0",
|
||||
"react": "^15.4.1",
|
||||
"react-addons-shallow-compare": "^15.1.0",
|
||||
"react-addons-test-utils": "^15.4.1",
|
||||
"react-bootstrap-4": "^0.29.1",
|
||||
"react-chartist": "^0.10.1",
|
||||
"react-chartist": "^0.12.0",
|
||||
"react-copy-to-clipboard": "^4.0.2",
|
||||
"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-dom": "^15.4.1",
|
||||
"react-dropzone": "^3.5.0",
|
||||
"react-intl": "^2.0.1",
|
||||
"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-overlays": "^0.6.0",
|
||||
"react-redux": "^5.0.0",
|
||||
"react-router": "^3.0.0",
|
||||
"react-select": "^1.0.0-rc.3",
|
||||
"react-shortcuts": "^1.3.1",
|
||||
"react-sparklines": "^1.5.0",
|
||||
"react-virtualized": "^8.0.8",
|
||||
"readable-stream": "^2.0.6",
|
||||
@@ -114,26 +125,31 @@
|
||||
"redux-devtools-log-monitor": "^1.0.5",
|
||||
"redux-thunk": "^2.0.1",
|
||||
"reselect": "^2.2.1",
|
||||
"standard": "^8.2.0",
|
||||
"superagent": "^2.0.0",
|
||||
"semver": "^5.3.0",
|
||||
"standard": "^8.4.0",
|
||||
"styled-components": "^1.4.4",
|
||||
"superagent": "^3.5.0",
|
||||
"tar-stream": "^1.5.2",
|
||||
"uncontrollable-input": "^0.0.0",
|
||||
"vinyl": "^2.0.0",
|
||||
"watchify": "^3.7.0",
|
||||
"xml2js": "^0.4.17",
|
||||
"xo-acl-resolver": "^0.2.2",
|
||||
"xo-acl-resolver": "^0.2.3",
|
||||
"xo-common": "0.1.0",
|
||||
"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",
|
||||
"build-indexes": "index-modules --auto src",
|
||||
"commitmsg": "npm test",
|
||||
"dev": "npm run build-indexes && NODE_ENV=development gulp build",
|
||||
"dev-test": "ava --watch",
|
||||
"dev-test": "jest --watch",
|
||||
"lint": "standard",
|
||||
"posttest": "npm run lint",
|
||||
"prepublish": "npm run build",
|
||||
"test": "ava"
|
||||
"test": "jest"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
@@ -141,15 +157,6 @@
|
||||
"loose-envify"
|
||||
]
|
||||
},
|
||||
"ava": {
|
||||
"babel": "inherit",
|
||||
"files": [
|
||||
"src/**/*.spec.js"
|
||||
],
|
||||
"require": [
|
||||
"babel-register"
|
||||
]
|
||||
},
|
||||
"babel": {
|
||||
"env": {
|
||||
"development": {
|
||||
@@ -166,6 +173,8 @@
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"dev",
|
||||
"lodash",
|
||||
"transform-decorators-legacy",
|
||||
"transform-runtime"
|
||||
],
|
||||
@@ -175,12 +184,15 @@
|
||||
"stage-0"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"ghooks": {
|
||||
"commit-msg": "npm test"
|
||||
}
|
||||
"jest": {
|
||||
"snapshotSerializers": [
|
||||
"enzyme-to-json/serializer"
|
||||
]
|
||||
},
|
||||
"standard": {
|
||||
"globals": [
|
||||
"__DEV__"
|
||||
],
|
||||
"ignore": [
|
||||
"dist"
|
||||
],
|
||||
|
||||
@@ -27,6 +27,13 @@ $ct-series-colors: (
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
// safari has a bug in flex computing that prevent charts from showing see #1755
|
||||
// by fixing the height with a value found in Chrome it seems like it fixes the issue without breaking the layout
|
||||
// elsewhere
|
||||
.dashboardItem .ct-chart {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
// Line in charts with only 2px in width
|
||||
.ct-line {
|
||||
stroke-width: 2px;
|
||||
|
||||
19
src/common/__snapshots__/grid.spec.js.snap
Normal file
19
src/common/__snapshots__/grid.spec.js.snap
Normal file
@@ -0,0 +1,19 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Col 1`] = `
|
||||
<div
|
||||
className="col-xs-12"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Container 1`] = `
|
||||
<div
|
||||
className="container-fluid"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`Row 1`] = `
|
||||
<div
|
||||
className=" row"
|
||||
/>
|
||||
`;
|
||||
@@ -2,7 +2,6 @@ import _ from 'intl'
|
||||
import ActionButton from 'action-button'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import Tooltip from 'tooltip'
|
||||
import {
|
||||
ButtonGroup
|
||||
} from 'react-bootstrap-4/lib'
|
||||
@@ -18,17 +17,16 @@ const ActionBar = ({ actions, param }) => (
|
||||
}
|
||||
|
||||
const { handler, handlerParam = param, label, icon, redirectOnSuccess } = button
|
||||
return <Tooltip key={index} content={_(label)}>
|
||||
<ActionButton
|
||||
key={index}
|
||||
btnStyle='secondary'
|
||||
handler={handler || noop}
|
||||
handlerParam={handlerParam}
|
||||
icon={icon}
|
||||
redirectOnSuccess={redirectOnSuccess}
|
||||
size='large'
|
||||
/>
|
||||
</Tooltip>
|
||||
return <ActionButton
|
||||
key={index}
|
||||
btnStyle='secondary'
|
||||
handler={handler || noop}
|
||||
handlerParam={handlerParam}
|
||||
icon={icon}
|
||||
redirectOnSuccess={redirectOnSuccess}
|
||||
size='large'
|
||||
tooltip={_(label)}
|
||||
/>
|
||||
})}
|
||||
</ButtonGroup>
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import Component from './base-component'
|
||||
import logError from './log-error'
|
||||
import propTypes from './prop-types'
|
||||
import Tooltip from './tooltip'
|
||||
import { error as _error } from './notification'
|
||||
|
||||
@propTypes({
|
||||
btnStyle: propTypes.string,
|
||||
@@ -36,8 +37,10 @@ export default class ActionButton extends Component {
|
||||
}
|
||||
|
||||
const {
|
||||
children,
|
||||
handler,
|
||||
handlerParam
|
||||
handlerParam,
|
||||
tooltip
|
||||
} = this.props
|
||||
|
||||
try {
|
||||
@@ -68,6 +71,7 @@ export default class ActionButton extends Component {
|
||||
// ignore when undefined because it usually means that the action has been canceled
|
||||
if (error !== undefined) {
|
||||
logError(error)
|
||||
_error(children || tooltip || error.name, error.message || String(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,14 @@ 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 { PureComponent } from 'react'
|
||||
|
||||
import getEventValue from './get-event-value'
|
||||
import invoke from './invoke'
|
||||
import shallowEqual from './shallow-equal'
|
||||
|
||||
// Should components logs every renders?
|
||||
//
|
||||
// Usually set to process.env.NODE_ENV !== 'production'.
|
||||
const VERBOSE = false
|
||||
|
||||
const cowSet = (object, path, value, depth) => {
|
||||
if (depth >= path.length) {
|
||||
@@ -31,7 +34,7 @@ const get = (object, path, depth) => {
|
||||
: get(object[prop], path, depth)
|
||||
}
|
||||
|
||||
export default class BaseComponent extends Component {
|
||||
export default class BaseComponent extends PureComponent {
|
||||
constructor (props, context) {
|
||||
super(props, context)
|
||||
|
||||
@@ -40,12 +43,12 @@ export default class BaseComponent extends Component {
|
||||
|
||||
this._linkedState = null
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
this.render = invoke(this.render, render => () => {
|
||||
if (VERBOSE) {
|
||||
this.render = (render => () => {
|
||||
console.log('render', this.constructor.name)
|
||||
|
||||
return render.call(this)
|
||||
})
|
||||
})(this.render)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,16 +110,9 @@ export default class BaseComponent extends Component {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
shouldComponentUpdate (newProps, newState) {
|
||||
return !(
|
||||
shallowEqual(this.props, newProps) &&
|
||||
shallowEqual(this.state, newState)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (VERBOSE) {
|
||||
const diff = (name, old, cur) => {
|
||||
const keys = []
|
||||
|
||||
|
||||
@@ -7,9 +7,14 @@ import propTypes from './prop-types'
|
||||
@propTypes({
|
||||
children: propTypes.any.isRequired,
|
||||
className: propTypes.string,
|
||||
buttonText: propTypes.any.isRequired
|
||||
buttonText: propTypes.any.isRequired,
|
||||
defaultOpen: propTypes.bool
|
||||
})
|
||||
export default class Collapse extends Component {
|
||||
state = {
|
||||
isOpened: this.props.defaultOpen
|
||||
}
|
||||
|
||||
_onClick = () => {
|
||||
this.setState({
|
||||
isOpened: !this.state.isOpened
|
||||
|
||||
@@ -15,6 +15,8 @@ import styles from './index.css'
|
||||
@propTypes({
|
||||
defaultValue: propTypes.any,
|
||||
disabled: propTypes.bool,
|
||||
max: propTypes.number,
|
||||
min: propTypes.number,
|
||||
options: propTypes.oneOfType([
|
||||
propTypes.arrayOf(propTypes.string),
|
||||
propTypes.number,
|
||||
@@ -62,6 +64,8 @@ export default class Combobox extends Component {
|
||||
className='form-control'
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
max={props.max}
|
||||
min={props.min}
|
||||
options={options}
|
||||
onChange={this._handleChange}
|
||||
placeholder={props.placeholder}
|
||||
|
||||
@@ -3,10 +3,11 @@ import {
|
||||
createOr,
|
||||
createNot,
|
||||
createProperty,
|
||||
createString
|
||||
createString,
|
||||
createTruthyProperty
|
||||
} from './'
|
||||
|
||||
export const pattern = 'foo !"\\\\ \\"" name:|(wonderwoman batman)'
|
||||
export const pattern = 'foo !"\\\\ \\"" name:|(wonderwoman batman) hasCape?'
|
||||
|
||||
export const ast = createAnd([
|
||||
createString('foo'),
|
||||
@@ -14,5 +15,6 @@ export const ast = createAnd([
|
||||
createProperty('name', createOr([
|
||||
createString('wonderwoman'),
|
||||
createString('batman')
|
||||
]))
|
||||
])),
|
||||
createTruthyProperty('hasCape')
|
||||
])
|
||||
|
||||
@@ -56,19 +56,22 @@ export const createProperty = (name, child) => ({ type: 'property', name, child
|
||||
|
||||
export const createString = value => ({ type: 'string', value })
|
||||
|
||||
export const createTruthyProperty = name => ({ type: 'truthyProperty', name })
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// *and = terms
|
||||
// terms = term+
|
||||
// term = ws (groupedAnd | or | not | property | string) ws
|
||||
// ws = ' '*
|
||||
// groupedAnd = "(" and ")"
|
||||
// *or = "|" ws "(" terms ")"
|
||||
// *not = "!" term
|
||||
// *property = string ws ":" term
|
||||
// *string = quotedString | rawString
|
||||
// quotedString = "\"" ( /[^"\]/ | "\\\\" | "\\\"" )+
|
||||
// rawString = /[a-z0-9-_.]+/i
|
||||
// *and = terms
|
||||
// terms = term+
|
||||
// term = ws (groupedAnd | or | not | property | truthyProperty | string) ws
|
||||
// ws = ' '*
|
||||
// groupedAnd = "(" and ")"
|
||||
// *or = "|" ws "(" terms ")"
|
||||
// *not = "!" term
|
||||
// *property = string ws ":" term
|
||||
// *truthyProperty = string ws "?"
|
||||
// *string = quotedString | rawString
|
||||
// quotedString = "\"" ( /[^"\]/ | "\\\\" | "\\\"" )+
|
||||
// rawString = /[a-z0-9-_.]+/i
|
||||
export const parse = invoke(() => {
|
||||
let i
|
||||
let n
|
||||
@@ -108,6 +111,7 @@ export const parse = invoke(() => {
|
||||
parseOr() ||
|
||||
parseNot() ||
|
||||
parseProperty() ||
|
||||
parseTruthyProperty() ||
|
||||
parseString()
|
||||
)
|
||||
if (child) {
|
||||
@@ -203,6 +207,16 @@ export const parse = invoke(() => {
|
||||
return value
|
||||
}
|
||||
}
|
||||
const parseTruthyProperty = backtrace(() => {
|
||||
let name
|
||||
if (
|
||||
(name = parseString()) &&
|
||||
parseWs() &&
|
||||
input[i++] === '?'
|
||||
) {
|
||||
return createTruthyProperty(name.value)
|
||||
}
|
||||
})
|
||||
|
||||
return input_ => {
|
||||
if (!input_) {
|
||||
@@ -341,6 +355,7 @@ export const execute = invoke(() => {
|
||||
property: ({ name, child }, value) => (
|
||||
value != null && child::execute(value[name])
|
||||
),
|
||||
truthyProperty: ({ name }, value) => !!value[name],
|
||||
string: invoke(() => {
|
||||
const match = (pattern, value) => {
|
||||
if (isString(value)) {
|
||||
@@ -378,7 +393,8 @@ export const toString = invoke(() => {
|
||||
property: ({ name, child }) => `${toString(createString(name))}:${toString(child)}`,
|
||||
string: ({ value }) => isRawString(value)
|
||||
? value
|
||||
: `"${value.replace(/\\|"/g, match => `\\${match}`)}"`
|
||||
: `"${value.replace(/\\|"/g, match => `\\${match}`)}"`,
|
||||
truthyProperty: ({ name }) => `${toString(createString(name))}?`
|
||||
}
|
||||
|
||||
const toString = node => visitors[node.type](node)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import test from 'ava'
|
||||
/* eslint-env jest */
|
||||
|
||||
import {
|
||||
getPropertyClausesStrings,
|
||||
@@ -11,43 +11,36 @@ import {
|
||||
pattern
|
||||
} from './index.fixtures'
|
||||
|
||||
test('getPropertyClausesStrings', t => {
|
||||
let tmp = parse('foo bar:baz baz:|(foo bar)')::getPropertyClausesStrings()
|
||||
t.deepEqual(
|
||||
tmp,
|
||||
{
|
||||
bar: [ 'baz' ],
|
||||
baz: [ 'foo', 'bar' ]
|
||||
}
|
||||
)
|
||||
it('getPropertyClausesStrings', () => {
|
||||
const tmp = parse('foo bar:baz baz:|(foo bar)')::getPropertyClausesStrings()
|
||||
expect(tmp).toEqual({
|
||||
bar: [ 'baz' ],
|
||||
baz: [ 'foo', 'bar' ]
|
||||
})
|
||||
})
|
||||
|
||||
test('parse', t => {
|
||||
t.deepEqual(parse(pattern), ast)
|
||||
it('parse', () => {
|
||||
expect(parse(pattern)).toEqual(ast)
|
||||
})
|
||||
|
||||
test('setPropertyClause', t => {
|
||||
t.is(
|
||||
null::setPropertyClause('foo', 'bar')::toString(),
|
||||
'foo:bar'
|
||||
)
|
||||
it('setPropertyClause', () => {
|
||||
expect(
|
||||
null::setPropertyClause('foo', 'bar')::toString()
|
||||
).toBe('foo:bar')
|
||||
|
||||
t.is(
|
||||
parse('baz')::setPropertyClause('foo', 'bar')::toString(),
|
||||
'baz foo:bar'
|
||||
)
|
||||
expect(
|
||||
parse('baz')::setPropertyClause('foo', 'bar')::toString()
|
||||
).toBe('baz foo:bar')
|
||||
|
||||
t.is(
|
||||
parse('plip foo:baz plop')::setPropertyClause('foo', 'bar')::toString(),
|
||||
'plip plop foo:bar'
|
||||
)
|
||||
expect(
|
||||
parse('plip foo:baz plop')::setPropertyClause('foo', 'bar')::toString()
|
||||
).toBe('plip plop foo:bar')
|
||||
|
||||
t.is(
|
||||
parse('foo:|(baz plop)')::setPropertyClause('foo', 'bar')::toString(),
|
||||
'foo:bar'
|
||||
)
|
||||
expect(
|
||||
parse('foo:|(baz plop)')::setPropertyClause('foo', 'bar')::toString()
|
||||
).toBe('foo:bar')
|
||||
})
|
||||
|
||||
test('toString', t => {
|
||||
t.is(pattern, ast::toString())
|
||||
it('toString', () => {
|
||||
expect(pattern).toBe(ast::toString())
|
||||
})
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import { isPromise } from 'promise-toolbox'
|
||||
|
||||
const toString = value => JSON.stringify(value, null, 2)
|
||||
const toString = value => value === undefined
|
||||
? 'undefined'
|
||||
: JSON.stringify(value, null, 2)
|
||||
|
||||
// This component does not handle changes in its `promise` property.
|
||||
class DebugAsync extends Component {
|
||||
|
||||
22
src/common/dropzone/index.css
Normal file
22
src/common/dropzone/index.css
Normal file
@@ -0,0 +1,22 @@
|
||||
@value dropzoneColor: #8f8686;
|
||||
|
||||
.dropzone {
|
||||
border-radius: 4px;
|
||||
border: 2px dashed dropzoneColor;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 12em;
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.activeDropzone {
|
||||
background: #f0f0f0;
|
||||
border-style: solid;
|
||||
}
|
||||
|
||||
.dropzoneText {
|
||||
color: dropzoneColor;
|
||||
font-size: 1.2em;
|
||||
margin: auto;
|
||||
}
|
||||
20
src/common/dropzone/index.js
Normal file
20
src/common/dropzone/index.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import Component from 'base-component'
|
||||
import propTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import ReactDropzone from 'react-dropzone'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@propTypes({
|
||||
onDrop: propTypes.func,
|
||||
message: propTypes.node
|
||||
})
|
||||
export default class Dropzone extends Component {
|
||||
render () {
|
||||
const { onDrop, message } = this.props
|
||||
|
||||
return <ReactDropzone onDrop={onDrop} className={styles.dropzone} activeClassName={styles.activeDropzone}>
|
||||
<div className={styles.dropzoneText}>{message}</div>
|
||||
</ReactDropzone>
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import React from 'react'
|
||||
|
||||
import _ from '../intl'
|
||||
import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
import Icon from '../icon'
|
||||
import logError from '../log-error'
|
||||
import propTypes from '../prop-types'
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
SelectNetwork,
|
||||
SelectPool,
|
||||
SelectRemote,
|
||||
SelectResourceSetIp,
|
||||
SelectSr,
|
||||
SelectSubject,
|
||||
SelectTag,
|
||||
@@ -134,7 +136,8 @@ class Editable extends Component {
|
||||
this._closeEdition()
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
error: isString(error) ? error : error.message,
|
||||
// `error` may be undefined if the action has been cancelled
|
||||
error: error !== undefined && (isString(error) ? error : error.message),
|
||||
saving: false
|
||||
})
|
||||
logError(error)
|
||||
@@ -306,60 +309,66 @@ export class Number extends Component {
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
labelProp: propTypes.string.isRequired,
|
||||
options: propTypes.oneOfType([
|
||||
propTypes.array,
|
||||
propTypes.object
|
||||
]).isRequired
|
||||
]).isRequired,
|
||||
renderer: propTypes.func
|
||||
})
|
||||
export class Select extends Editable {
|
||||
constructor (props) {
|
||||
super()
|
||||
|
||||
this._defaultValue = findKey(props.options, option => option === props.value)
|
||||
componentWillReceiveProps (props) {
|
||||
if (
|
||||
props.value !== this.props.value ||
|
||||
props.options !== this.props.options
|
||||
) {
|
||||
this.setState({ valueKey: findKey(props.options, option => option === props.value) })
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.props.options[this._select.value]
|
||||
return this.props.options[this.state.valueKey]
|
||||
}
|
||||
|
||||
_onChange = event => {
|
||||
this._save()
|
||||
this.setState({ valueKey: getEventValue(event) }, this._save)
|
||||
}
|
||||
_optionToJsx = (option, index) => {
|
||||
const { labelProp } = this.props
|
||||
|
||||
_optionToJsx = (option, key) => {
|
||||
const { renderer } = this.props
|
||||
|
||||
return <option
|
||||
key={index}
|
||||
value={index}
|
||||
key={key}
|
||||
value={key}
|
||||
>
|
||||
{labelProp ? option[labelProp] : option}
|
||||
{renderer ? renderer(option) : option}
|
||||
</option>
|
||||
}
|
||||
|
||||
_onEditionMount = ref => {
|
||||
this._select = ref
|
||||
// Seems to work in Google Chrome (not in Firefox)
|
||||
ref && ref.dispatchEvent(new window.MouseEvent('mousedown'))
|
||||
}
|
||||
|
||||
_renderDisplay () {
|
||||
return this.props.children ||
|
||||
<span>{this.props.value[this.props.labelProp]}</span>
|
||||
const { children, renderer, value } = this.props
|
||||
|
||||
return children ||
|
||||
<span>{renderer ? renderer(value) : value}</span>
|
||||
}
|
||||
|
||||
_renderEdition () {
|
||||
const { saving } = this.state
|
||||
const { saving, valueKey } = this.state
|
||||
const { options } = this.props
|
||||
|
||||
return <select
|
||||
autoFocus
|
||||
className={classNames('form-control', styles.select)}
|
||||
defaultValue={this._defaultValue}
|
||||
onBlur={this._closeEdition}
|
||||
onChange={this._onChange}
|
||||
onKeyDown={this._onKeyDown}
|
||||
readOnly={saving}
|
||||
ref={this._onEditionMount}
|
||||
value={valueKey}
|
||||
>
|
||||
{map(options, this._optionToJsx)}
|
||||
</select>
|
||||
@@ -372,6 +381,7 @@ const MAP_TYPE_SELECT = {
|
||||
network: SelectNetwork,
|
||||
pool: SelectPool,
|
||||
remote: SelectRemote,
|
||||
resourceSetIp: SelectResourceSetIp,
|
||||
SR: SelectSr,
|
||||
subject: SelectSubject,
|
||||
tag: SelectTag,
|
||||
@@ -381,15 +391,14 @@ const MAP_TYPE_SELECT = {
|
||||
|
||||
@propTypes({
|
||||
labelProp: propTypes.string.isRequired,
|
||||
predicate: propTypes.func,
|
||||
value: propTypes.oneOfType([
|
||||
propTypes.string,
|
||||
propTypes.object
|
||||
]).isRequired
|
||||
])
|
||||
})
|
||||
export class XoSelect extends Editable {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
return this.state.value
|
||||
}
|
||||
|
||||
_renderDisplay () {
|
||||
@@ -397,16 +406,14 @@ export class XoSelect extends Editable {
|
||||
<span>{this.props.value[this.props.labelProp]}</span>
|
||||
}
|
||||
|
||||
_onChange = object => {
|
||||
object ? this._save() : this._closeEdition()
|
||||
}
|
||||
_onChange = object =>
|
||||
this.setState({ value: object }, object && this._save)
|
||||
|
||||
_renderEdition () {
|
||||
const {
|
||||
placeholder,
|
||||
predicate,
|
||||
saving,
|
||||
xoType
|
||||
xoType,
|
||||
...props
|
||||
} = this.props
|
||||
|
||||
const Select = MAP_TYPE_SELECT[xoType]
|
||||
@@ -420,12 +427,10 @@ export class XoSelect extends Editable {
|
||||
// when this element is clicked.
|
||||
return <a onBlur={this._closeEdition}>
|
||||
<Select
|
||||
{...props}
|
||||
autoFocus
|
||||
disabled={saving}
|
||||
onChange={this._onChange}
|
||||
placeholder={placeholder}
|
||||
predicate={predicate}
|
||||
ref='select'
|
||||
/>
|
||||
</a>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import test from 'ava'
|
||||
/* eslint-env jest */
|
||||
|
||||
import filterReduce from './filter-reduce'
|
||||
|
||||
@@ -6,23 +6,20 @@ const add = (a, b) => a + b
|
||||
const data = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
|
||||
const isEven = x => !(x & 1)
|
||||
|
||||
test('filterReduce', t => {
|
||||
it('filterReduce', () => {
|
||||
// Returns all elements not matching the predicate and the result of
|
||||
// a reduction over those who do.
|
||||
t.deepEqual(
|
||||
filterReduce(data, isEven, add),
|
||||
expect(filterReduce(data, isEven, add)).toEqual(
|
||||
[ 1, 3, 5, 7, 9, 20 ]
|
||||
)
|
||||
|
||||
// The default reducer is the identity.
|
||||
t.deepEqual(
|
||||
filterReduce(data, isEven),
|
||||
expect(filterReduce(data, isEven)).toEqual(
|
||||
[ 1, 3, 5, 7, 9, 0 ]
|
||||
)
|
||||
|
||||
// If an initial value is passed it is used.
|
||||
t.deepEqual(
|
||||
filterReduce(data, isEven, add, 22),
|
||||
expect(filterReduce(data, isEven, add, 22)).toEqual(
|
||||
[ 1, 3, 5, 7, 9, 42 ]
|
||||
)
|
||||
})
|
||||
|
||||
@@ -5,12 +5,15 @@ import map from 'lodash/map'
|
||||
import randomPassword from 'random-password'
|
||||
import React from 'react'
|
||||
import round from 'lodash/round'
|
||||
import SingleLineRow from 'single-line-row'
|
||||
import { Container, Col } from 'grid'
|
||||
import {
|
||||
DropdownButton,
|
||||
MenuItem
|
||||
} from 'react-bootstrap-4/lib'
|
||||
|
||||
import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
import propTypes from '../prop-types'
|
||||
import {
|
||||
firstDefined,
|
||||
@@ -36,7 +39,16 @@ export class Password extends Component {
|
||||
}
|
||||
|
||||
_generate = () => {
|
||||
this.refs.field.value = randomPassword(8)
|
||||
const value = randomPassword(8)
|
||||
const isControlled = this.props.value !== undefined
|
||||
if (isControlled) {
|
||||
this.props.onChange(value)
|
||||
} else {
|
||||
this.refs.field.value = value
|
||||
}
|
||||
|
||||
// FIXME: in controlled mode, visibility should only be updated
|
||||
// when the value prop is changed according to the emitted value.
|
||||
this.setState({
|
||||
visible: true
|
||||
})
|
||||
@@ -80,68 +92,45 @@ export class Password extends Component {
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
defaultValue: propTypes.number,
|
||||
max: propTypes.number.isRequired,
|
||||
min: propTypes.number.isRequired,
|
||||
onChange: propTypes.func,
|
||||
step: propTypes.number,
|
||||
onChange: propTypes.func
|
||||
value: propTypes.number
|
||||
})
|
||||
export class Range extends Component {
|
||||
constructor (props) {
|
||||
super()
|
||||
this.state = {
|
||||
value: props.defaultValue || props.min
|
||||
componentDidMount () {
|
||||
const { min, onChange, value } = this.props
|
||||
|
||||
if (!value) {
|
||||
onChange && onChange(min)
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.state.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.setState({
|
||||
value: +value
|
||||
})
|
||||
}
|
||||
|
||||
_handleChange = event => {
|
||||
const { onChange } = this.props
|
||||
const { value } = event.target
|
||||
|
||||
if (value === this.state.value) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
value
|
||||
}, onChange && (() => onChange(value)))
|
||||
}
|
||||
_onChange = value =>
|
||||
this.props.onChange(getEventValue(value))
|
||||
|
||||
render () {
|
||||
const {
|
||||
props
|
||||
} = this
|
||||
const step = props.step || 1
|
||||
const { value } = this.state
|
||||
const { max, min, step, value } = this.props
|
||||
|
||||
return (
|
||||
<div className='form-group row'>
|
||||
<label className='col-sm-2 control-label'>
|
||||
{value}
|
||||
</label>
|
||||
<div className='col-sm-10'>
|
||||
return <Container>
|
||||
<SingleLineRow>
|
||||
<Col size={2}>
|
||||
<span className='pull-right'>{value}</span>
|
||||
</Col>
|
||||
<Col size={10}>
|
||||
<input
|
||||
className='form-control'
|
||||
type='range'
|
||||
min={props.min}
|
||||
max={props.max}
|
||||
max={max}
|
||||
min={min}
|
||||
onChange={this._onChange}
|
||||
step={step}
|
||||
type='range'
|
||||
value={value}
|
||||
onChange={this._handleChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</Container>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,88 +148,125 @@ const DEFAULT_UNIT = 'GiB'
|
||||
readOnly: propTypes.bool,
|
||||
required: propTypes.bool,
|
||||
style: propTypes.object,
|
||||
value: propTypes.number
|
||||
value: propTypes.oneOfType([
|
||||
propTypes.number,
|
||||
propTypes.oneOf([ null ])
|
||||
])
|
||||
})
|
||||
export class SizeInput extends BaseComponent {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this.state = this._createStateFromBytes(firstDefined(props.value, props.defaultValue, 0))
|
||||
this.state = this._createStateFromBytes(firstDefined(props.value, props.defaultValue, null))
|
||||
}
|
||||
|
||||
componentWillReceiveProps (newProps) {
|
||||
const { value } = newProps
|
||||
if (value == null && value === this.props.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const { _bytes, _unit, _value } = this
|
||||
this._bytes = this._unit = this._value = null
|
||||
|
||||
if (value === _bytes) {
|
||||
// Update input value
|
||||
this.setState({
|
||||
unit: _unit,
|
||||
value: _value
|
||||
})
|
||||
} else {
|
||||
componentWillReceiveProps (props) {
|
||||
const { value } = props
|
||||
if (value !== undefined && value !== this.props.value) {
|
||||
this.setState(this._createStateFromBytes(value))
|
||||
}
|
||||
}
|
||||
|
||||
_createStateFromBytes = bytes => {
|
||||
const humanSize = bytes && formatSizeRaw(bytes)
|
||||
_createStateFromBytes (bytes) {
|
||||
if (bytes === this._bytes) {
|
||||
return {
|
||||
input: this._input,
|
||||
unit: this._unit
|
||||
}
|
||||
}
|
||||
|
||||
if (bytes === null) {
|
||||
return {
|
||||
input: '',
|
||||
unit: this.props.defaultUnit || DEFAULT_UNIT
|
||||
}
|
||||
}
|
||||
|
||||
const { prefix, value } = formatSizeRaw(bytes)
|
||||
return {
|
||||
unit: humanSize && humanSize.value ? humanSize.prefix + 'B' : this.props.defaultUnit || DEFAULT_UNIT,
|
||||
value: humanSize ? round(humanSize.value, 3) : ''
|
||||
input: String(round(value, 2)),
|
||||
unit: `${prefix}B`
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
try {
|
||||
const { unit, value } = this.state
|
||||
return parseSize(value + ' ' + unit)
|
||||
} catch (_) {}
|
||||
const { input, unit } = this.state
|
||||
|
||||
if (!input) {
|
||||
return null
|
||||
}
|
||||
|
||||
return parseSize(`${+input} ${unit}`)
|
||||
}
|
||||
|
||||
set value (newValue) {
|
||||
set value (value) {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
this.props.value != null
|
||||
this.props.value !== undefined
|
||||
) {
|
||||
throw new Error('cannot set value of controlled SizeInput')
|
||||
}
|
||||
this.setState(this._createStateFromBytes(newValue))
|
||||
this.setState(this._createStateFromBytes(value))
|
||||
}
|
||||
|
||||
_onChange = value =>
|
||||
this.props.onChange && this.props.onChange(value)
|
||||
_onChange (input, unit) {
|
||||
const { onChange } = this.props
|
||||
|
||||
_updateValue = event => {
|
||||
const { value } = event.target
|
||||
if (this.props.value != null) {
|
||||
this._value = value
|
||||
this._unit = this.state.unit
|
||||
this._bytes = parseSize((value || 0) + ' ' + this.state.unit)
|
||||
// Empty input equals null.
|
||||
const bytes = input
|
||||
? parseSize(`${+input} ${unit}`)
|
||||
: null
|
||||
|
||||
this._onChange(this._bytes)
|
||||
} else {
|
||||
this.setState({ value }, () => {
|
||||
this._onChange(this.value)
|
||||
})
|
||||
}
|
||||
}
|
||||
_updateUnit = unit => {
|
||||
if (this.props.value != null) {
|
||||
this._value = this.state.value
|
||||
const isControlled = this.props.value !== undefined
|
||||
if (isControlled) {
|
||||
// Store input and unit for this change to update correctly on new
|
||||
// props.
|
||||
this._bytes = bytes
|
||||
this._input = input
|
||||
this._unit = unit
|
||||
this._bytes = parseSize((this.state.value || 0) + ' ' + unit)
|
||||
|
||||
this._onChange(this._bytes)
|
||||
} else {
|
||||
this.setState({ unit }, () => {
|
||||
this._onChange(this.value)
|
||||
})
|
||||
this.setState({ input, unit })
|
||||
|
||||
// onChange is optional in uncontrolled mode.
|
||||
if (!onChange) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
onChange(bytes)
|
||||
}
|
||||
|
||||
_updateNumber = event => {
|
||||
const input = event.target.value
|
||||
|
||||
if (!input) {
|
||||
return this._onChange(input, this.state.unit)
|
||||
}
|
||||
|
||||
const number = +input
|
||||
|
||||
// NaN: do not ack this change.
|
||||
if (number !== number) { // eslint-disable-line no-self-compare
|
||||
return
|
||||
}
|
||||
|
||||
// Same numeric value: simply update the input.
|
||||
const prevInput = this.state.input
|
||||
if (prevInput && +prevInput === number) {
|
||||
return this.setState({ input })
|
||||
}
|
||||
|
||||
this._onChange(input, this.state.unit)
|
||||
}
|
||||
|
||||
_updateUnit = unit => {
|
||||
const { input } = this.state
|
||||
|
||||
// 0 is always 0, no matter the unit.
|
||||
if (+input) {
|
||||
this._onChange(input, unit)
|
||||
} else {
|
||||
this.setState({ unit })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,39 +274,30 @@ export class SizeInput extends BaseComponent {
|
||||
const {
|
||||
autoFocus,
|
||||
className,
|
||||
placeholder,
|
||||
readOnly,
|
||||
placeholder,
|
||||
required,
|
||||
style
|
||||
} = this.props
|
||||
|
||||
const {
|
||||
value,
|
||||
unit
|
||||
} = this.state
|
||||
|
||||
return <span
|
||||
className={classNames(className, 'input-group')}
|
||||
style={style}
|
||||
>
|
||||
return <span className={classNames('input-group', className)} style={style}>
|
||||
<input
|
||||
autoFocus={autoFocus}
|
||||
className='form-control'
|
||||
min={0}
|
||||
onChange={this._updateValue}
|
||||
disabled={readOnly}
|
||||
onChange={this._updateNumber}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
required={required}
|
||||
type='number'
|
||||
value={value}
|
||||
type='text'
|
||||
value={this.state.input}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
<DropdownButton
|
||||
bsStyle='secondary'
|
||||
disabled={readOnly}
|
||||
id='size'
|
||||
pullRight
|
||||
title={unit}
|
||||
disabled={readOnly}
|
||||
title={this.state.unit}
|
||||
>
|
||||
{map(UNITS, unit =>
|
||||
<MenuItem
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Component from 'base-component'
|
||||
import find from 'lodash/find'
|
||||
import map from 'lodash/map'
|
||||
import React, { Component } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
|
||||
@@ -8,7 +10,6 @@ import Select from './select'
|
||||
|
||||
@propTypes({
|
||||
autoFocus: propTypes.bool,
|
||||
defaultValue: propTypes.any,
|
||||
disabled: propTypes.bool,
|
||||
optionRenderer: propTypes.func,
|
||||
multi: propTypes.bool,
|
||||
@@ -16,13 +17,26 @@ import Select from './select'
|
||||
options: propTypes.array,
|
||||
placeholder: propTypes.string,
|
||||
predicate: propTypes.func,
|
||||
required: propTypes.bool
|
||||
required: propTypes.bool,
|
||||
value: propTypes.any
|
||||
})
|
||||
@uncontrollableInput()
|
||||
export default class SelectPlainObject extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
value: this._computeValue(props.defaultValue, props)
|
||||
componentDidMount () {
|
||||
const { options, value } = this.props
|
||||
|
||||
this.setState({
|
||||
options: this._computeOptions(options),
|
||||
value: this._computeValue(value, this.props)
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps (newProps) {
|
||||
if (newProps !== this.props) {
|
||||
this.setState({
|
||||
options: this._computeOptions(newProps.options),
|
||||
value: this._computeValue(newProps.value, newProps)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,25 +50,10 @@ export default class SelectPlainObject extends Component {
|
||||
}
|
||||
return map(value, reduceValue)
|
||||
}
|
||||
|
||||
return reduceValue(value)
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
const { options } = this.props
|
||||
|
||||
this.setState({
|
||||
options: this._computeOptions(options)
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps (newProps) {
|
||||
const { options } = newProps
|
||||
|
||||
this.setState({
|
||||
options: this._computeOptions(options)
|
||||
})
|
||||
}
|
||||
|
||||
_computeOptions (options) {
|
||||
const { optionKey = 'id' } = this.props
|
||||
const { optionRenderer = o => o.label || o[optionKey] || o } = this.props
|
||||
@@ -64,10 +63,13 @@ export default class SelectPlainObject extends Component {
|
||||
}))
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { optionKey = 'id' } = this.props
|
||||
const { value } = this.state
|
||||
const { options } = this.props
|
||||
_getObject (value) {
|
||||
if (value == null) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const { optionKey = 'id', options } = this.props
|
||||
|
||||
const pickValue = value => {
|
||||
value = value.value || value
|
||||
return find(options, option => option[optionKey] === value || option === value)
|
||||
@@ -80,18 +82,12 @@ export default class SelectPlainObject extends Component {
|
||||
return pickValue(value)
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.setState({
|
||||
value: this._computeValue(value)
|
||||
})
|
||||
}
|
||||
|
||||
_handleChange = value => {
|
||||
const { onChange } = this.props
|
||||
|
||||
this.setState({
|
||||
value: this._computeValue(value)
|
||||
}, onChange && (() => { onChange(this.value) }))
|
||||
if (onChange) {
|
||||
onChange(this._getObject(value))
|
||||
}
|
||||
}
|
||||
|
||||
_renderOption = option => option.label
|
||||
@@ -111,7 +107,8 @@ export default class SelectPlainObject extends Component {
|
||||
placeholder={props.placeholder}
|
||||
required={props.required}
|
||||
value={state.value}
|
||||
valueRenderer={this._renderOption} />
|
||||
valueRenderer={this._renderOption}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import map from 'lodash/map'
|
||||
import React, { Component } from 'react'
|
||||
import ReactSelect from 'react-select'
|
||||
import sum from 'lodash/sum'
|
||||
import {
|
||||
AutoSizer,
|
||||
CellMeasurer,
|
||||
List
|
||||
} from 'react-virtualized'
|
||||
|
||||
@@ -15,15 +18,19 @@ const SELECT_STYLE = {
|
||||
minWidth: '10em'
|
||||
}
|
||||
|
||||
const LIST_STYLE = {
|
||||
whiteSpace: 'normal'
|
||||
}
|
||||
|
||||
const MAX_OPTIONS = 5
|
||||
|
||||
// See: https://github.com/bvaughn/react-virtualized-select/blob/master/source/VirtualizedSelect/VirtualizedSelect.js
|
||||
@propTypes({
|
||||
maxHeight: propTypes.number,
|
||||
optionHeight: propTypes.number
|
||||
maxHeight: propTypes.number
|
||||
})
|
||||
export default class Select extends Component {
|
||||
static defaultProps = {
|
||||
maxHeight: 200,
|
||||
optionHeight: 40,
|
||||
optionRenderer: (option, labelKey) => option[labelKey]
|
||||
}
|
||||
|
||||
@@ -32,13 +39,10 @@ export default class Select extends Component {
|
||||
options,
|
||||
...otherOptions
|
||||
}) => {
|
||||
const {
|
||||
maxHeight,
|
||||
optionHeight
|
||||
} = this.props
|
||||
const { maxHeight } = this.props
|
||||
|
||||
const focusedOptionIndex = options.indexOf(focusedOption)
|
||||
const height = Math.min(maxHeight, options.length * optionHeight)
|
||||
let height = options.length > MAX_OPTIONS && maxHeight
|
||||
|
||||
const wrappedRowRenderer = ({ index, key, style }) =>
|
||||
this._optionRenderer({
|
||||
@@ -54,14 +58,33 @@ export default class Select extends Component {
|
||||
return (
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
<List
|
||||
height={height}
|
||||
rowCount={options.length}
|
||||
rowHeight={optionHeight}
|
||||
rowRenderer={wrappedRowRenderer}
|
||||
scrollToIndex={focusedOptionIndex}
|
||||
width={width}
|
||||
/>
|
||||
width ? (
|
||||
<CellMeasurer
|
||||
cellRenderer={({ rowIndex }) => wrappedRowRenderer({ index: rowIndex })}
|
||||
columnCount={1}
|
||||
rowCount={options.length}
|
||||
// FIXME: 16 px: ugly workaround to take into account the scrollbar
|
||||
// during the offscreen render to measure the row height
|
||||
// See https://github.com/bvaughn/react-virtualized/issues/401
|
||||
width={width - 16}
|
||||
>
|
||||
{({ getRowHeight }) => {
|
||||
if (options.length <= MAX_OPTIONS) {
|
||||
height = sum(map(options, (_, index) => getRowHeight({ index })))
|
||||
}
|
||||
|
||||
return <List
|
||||
height={height}
|
||||
rowCount={options.length}
|
||||
rowHeight={getRowHeight}
|
||||
rowRenderer={wrappedRowRenderer}
|
||||
scrollToIndex={focusedOptionIndex}
|
||||
style={LIST_STYLE}
|
||||
width={width}
|
||||
/>
|
||||
}}
|
||||
</CellMeasurer>
|
||||
) : null
|
||||
)}
|
||||
</AutoSizer>
|
||||
)
|
||||
@@ -95,7 +118,7 @@ export default class Select extends Component {
|
||||
className={className}
|
||||
onClick={!disabled && (() => selectValue(option))}
|
||||
onMouseOver={!disabled && (() => focusOption(option))}
|
||||
style={{ ...style, height: props.optionHeight }}
|
||||
style={style}
|
||||
key={key}
|
||||
>
|
||||
{props.optionRenderer(option, labelKey)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react'
|
||||
import classNames from 'classnames'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
|
||||
import Component from '../../base-component'
|
||||
import Icon from '../../icon'
|
||||
@@ -7,9 +8,9 @@ import propTypes from '../../prop-types'
|
||||
|
||||
import styles from './index.css'
|
||||
|
||||
@uncontrollableInput()
|
||||
@propTypes({
|
||||
className: propTypes.string,
|
||||
defaultValue: propTypes.bool,
|
||||
onChange: propTypes.func,
|
||||
icon: propTypes.string,
|
||||
iconOn: propTypes.string,
|
||||
@@ -24,64 +25,25 @@ export default class Toggle extends Component {
|
||||
iconSize: 2
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { props } = this
|
||||
|
||||
const { value } = props
|
||||
if (value != null) {
|
||||
return value
|
||||
}
|
||||
|
||||
const { input } = this.refs
|
||||
if (input) {
|
||||
return input.checked
|
||||
}
|
||||
|
||||
return props.defaultValue || false
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
this.props.value != null
|
||||
) {
|
||||
throw new Error('cannot set value of controlled Toggle')
|
||||
}
|
||||
|
||||
this.refs.input.checked = Boolean(value)
|
||||
this.forceUpdate()
|
||||
}
|
||||
|
||||
_onChange = event => {
|
||||
if (this.props.value == null) {
|
||||
this.forceUpdate()
|
||||
}
|
||||
|
||||
const { onChange } = this.props
|
||||
onChange && onChange(event.target.checked)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props, value } = this
|
||||
const { props } = this
|
||||
|
||||
return (
|
||||
<label
|
||||
className={classNames(
|
||||
props.disabled ? 'text-muted' : value ? 'text-success' : null,
|
||||
props.disabled ? 'text-muted' : props.value ? 'text-success' : null,
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
icon={props.icon || (value ? props.iconOn : props.iconOff)}
|
||||
icon={props.icon || (props.value ? props.iconOn : props.iconOff)}
|
||||
size={props.iconSize}
|
||||
/>
|
||||
<input
|
||||
checked={props.value}
|
||||
checked={props.value || false}
|
||||
className={styles.checkbox}
|
||||
defaultChecked={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
onChange={props.onChange}
|
||||
type='checkbox'
|
||||
/>
|
||||
</label>
|
||||
|
||||
13
src/common/grid.spec.js
Normal file
13
src/common/grid.spec.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
import React from 'react'
|
||||
import { forEach } from 'lodash'
|
||||
import { shallow } from 'enzyme'
|
||||
|
||||
import * as grid from './grid'
|
||||
|
||||
forEach(grid, (Component, name) => {
|
||||
it(name, () => {
|
||||
expect(shallow(<Component />)).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,9 @@
|
||||
const common = {
|
||||
homeFilterNone: ''
|
||||
}
|
||||
|
||||
export const VM = {
|
||||
...common,
|
||||
homeFilterPendingVms: 'current_operations:"" ',
|
||||
homeFilterNonRunningVms: '!power_state:running ',
|
||||
homeFilterHvmGuests: 'virtualizationMode:hvm ',
|
||||
@@ -7,14 +12,22 @@ export const VM = {
|
||||
}
|
||||
|
||||
export const host = {
|
||||
...common,
|
||||
homeFilterRunningHosts: 'power_state:running ',
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const pool = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const vmTemplate = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
export const SR = {
|
||||
...common,
|
||||
homeFilterTags: 'tags:'
|
||||
}
|
||||
|
||||
36
src/common/home-tags.js
Normal file
36
src/common/home-tags.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import Tags from './tags'
|
||||
import { createString, createProperty, toString } from './complex-matcher'
|
||||
|
||||
@propTypes({
|
||||
labels: propTypes.arrayOf(React.PropTypes.string).isRequired,
|
||||
onAdd: propTypes.func,
|
||||
onChange: propTypes.func,
|
||||
onDelete: propTypes.func,
|
||||
type: propTypes.string
|
||||
})
|
||||
export default class HomeTags extends Component {
|
||||
static contextTypes = {
|
||||
router: React.PropTypes.object
|
||||
}
|
||||
|
||||
_onClick = label => {
|
||||
const s = encodeURIComponent(createProperty('tags', createString(label))::toString())
|
||||
const t = encodeURIComponent(this.props.type)
|
||||
|
||||
this.context.router.push(`/home?t=${t}&s=${s}`)
|
||||
}
|
||||
|
||||
render () {
|
||||
return <Tags
|
||||
labels={this.props.labels}
|
||||
onAdd={this.props.onAdd}
|
||||
onChange={this.props.onChange}
|
||||
onClick={this._onClick}
|
||||
onDelete={this.props.onDelete}
|
||||
/>
|
||||
}
|
||||
}
|
||||
@@ -174,15 +174,15 @@ class HostsPatchesTable extends Component {
|
||||
<div>
|
||||
{!noPatches
|
||||
? (
|
||||
<SortedTable
|
||||
collection={hosts}
|
||||
columns={props.displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
|
||||
userData={{
|
||||
installAllHostPatches: this._installAllHostPatches,
|
||||
missingPatches: this.state.missingPatches,
|
||||
pools: props.pools
|
||||
}}
|
||||
/>
|
||||
<SortedTable
|
||||
collection={hosts}
|
||||
columns={props.displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
|
||||
userData={{
|
||||
installAllHostPatches: this._installAllHostPatches,
|
||||
missingPatches: this.state.missingPatches,
|
||||
pools: props.pools
|
||||
}}
|
||||
/>
|
||||
) : <p>{_('patchNothing')}</p>
|
||||
}
|
||||
<Portal container={() => props.buttonsGroupContainer()}>
|
||||
|
||||
@@ -62,7 +62,13 @@ export class IntlProvider extends Component {
|
||||
|
||||
render () {
|
||||
const { lang, children } = this.props
|
||||
// Adding a key prop is a work-around suggested by react-intl documentation
|
||||
// to make sure changes to the locale trigger a re-render of the child components
|
||||
// https://github.com/yahoo/react-intl/wiki/Components#dynamic-language-selection
|
||||
//
|
||||
// FIXME: remove the key prop when React context propagation is fixed (https://github.com/facebook/react/issues/2517)
|
||||
return <IntlProvider_
|
||||
key={lang}
|
||||
locale={lang}
|
||||
messages={locales[lang]}
|
||||
>
|
||||
|
||||
1
src/common/intl/locales/.gitignore
vendored
1
src/common/intl/locales/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
/index.js
|
||||
0
src/common/intl/locales/.index-modules
Normal file
0
src/common/intl/locales/.index-modules
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3592
src/common/intl/locales/hu.js
Normal file
3592
src/common/intl/locales/hu.js
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,13 @@ var isString = require('lodash/isString')
|
||||
var messages = {
|
||||
statusConnecting: 'Connecting',
|
||||
statusDisconnected: 'Disconnected',
|
||||
statusLoading: 'Loading…',
|
||||
errorPageNotFound: 'Page not found',
|
||||
errorNoSuchItem: 'no such item',
|
||||
|
||||
editableLongClickPlaceholder: 'Long click to edit',
|
||||
editableClickPlaceholder: 'Click to edit',
|
||||
browseFiles: 'Browse files',
|
||||
|
||||
// ----- Modals -----
|
||||
alertOk: 'OK',
|
||||
@@ -32,6 +36,7 @@ var messages = {
|
||||
homeHostPage: 'Hosts',
|
||||
homePoolPage: 'Pools',
|
||||
homeTemplatePage: 'Templates',
|
||||
homeSrPage: 'Storages',
|
||||
dashboardPage: 'Dashboard',
|
||||
overviewDashboardPage: 'Overview',
|
||||
overviewVisualizationDashboardPage: 'Visualizations',
|
||||
@@ -49,7 +54,9 @@ var messages = {
|
||||
settingsPluginsPage: 'Plugins',
|
||||
settingsLogsPage: 'Logs',
|
||||
settingsIpsPage: 'IPs',
|
||||
settingsConfigPage: 'Config',
|
||||
aboutPage: 'About',
|
||||
aboutXoaPlan: 'About XO {xoaPlan}',
|
||||
newMenu: 'New',
|
||||
taskMenu: 'Tasks',
|
||||
taskPage: 'Tasks',
|
||||
@@ -57,10 +64,12 @@ var messages = {
|
||||
newSrPage: 'Storage',
|
||||
newServerPage: 'Server',
|
||||
newImport: 'Import',
|
||||
xosan: 'XOSAN',
|
||||
backupOverviewPage: 'Overview',
|
||||
backupNewPage: 'New',
|
||||
backupRemotesPage: 'Remotes',
|
||||
backupRestorePage: 'Restore',
|
||||
backupFileRestorePage: 'File restore',
|
||||
schedule: 'Schedule',
|
||||
newVmBackup: 'New VM backup',
|
||||
editVmBackup: 'Edit VM backup',
|
||||
@@ -75,15 +84,24 @@ 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!',
|
||||
homeWelcomeText: 'Add your XenServer hosts or pools',
|
||||
homeConnectServerText: 'Some XenServers have been registered but are not connected',
|
||||
homeHelp: 'Want some help?',
|
||||
homeAddServer: 'Add server',
|
||||
homeConnectServer: 'Connect servers',
|
||||
homeOnlineDoc: 'Online Doc',
|
||||
homeProSupport: 'Pro Support',
|
||||
homeNoVms: 'There are no VMs!',
|
||||
@@ -105,6 +123,7 @@ var messages = {
|
||||
homeAllHosts: 'Hosts',
|
||||
homeAllTags: 'Tags',
|
||||
homeNewVm: 'New VM',
|
||||
homeFilterNone: 'None',
|
||||
homeFilterRunningHosts: 'Running hosts',
|
||||
homeFilterDisabledHosts: 'Disabled hosts',
|
||||
homeFilterRunningVms: 'Running VMs',
|
||||
@@ -118,16 +137,24 @@ var messages = {
|
||||
homeSortByRAM: 'RAM',
|
||||
homeSortByvCPUs: 'vCPUs',
|
||||
homeSortByCpus: 'CPUs',
|
||||
homeSortByShared: 'Shared/Not shared',
|
||||
homeSortBySize: 'Size',
|
||||
homeSortByUsage: 'Usage',
|
||||
homeSortByType: 'Type',
|
||||
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:',
|
||||
homeResourceSet: 'Resource set: {resourceSet}',
|
||||
highAvailability: 'High Availability',
|
||||
srSharedType: 'Shared {type}',
|
||||
srNotSharedType: 'Not shared {type}',
|
||||
|
||||
// ----- Forms -----
|
||||
add: 'Add',
|
||||
selectAll: 'Select all',
|
||||
remove: 'Remove',
|
||||
preview: 'Preview',
|
||||
item: 'Item',
|
||||
@@ -153,7 +180,8 @@ var messages = {
|
||||
selectTags: 'Select tag(s)…',
|
||||
selectVdis: 'Select disk(s)…',
|
||||
selectTimezone: 'Select timezone…',
|
||||
selectIp: 'Select IP(s)...',
|
||||
selectIp: 'Select IP(s)…',
|
||||
selectIpPool: 'Select IP pool(s)…',
|
||||
fillRequiredInformations: 'Fill required informations.',
|
||||
fillOptionalInformations: 'Fill informations (optional)',
|
||||
selectTableReset: 'Reset',
|
||||
@@ -161,28 +189,34 @@ var messages = {
|
||||
// --- Dates/Scheduler ---
|
||||
|
||||
schedulingMonth: 'Month',
|
||||
schedulingEveryNMonth: 'Every N month',
|
||||
schedulingEachSelectedMonth: 'Each selected month',
|
||||
schedulingMonthDay: 'Day of the month',
|
||||
schedulingEachSelectedMonthDay: 'Each selected day',
|
||||
schedulingWeekDay: 'Day of the week',
|
||||
schedulingEachSelectedWeekDay: 'Each selected day',
|
||||
schedulingDay: 'Day',
|
||||
schedulingEveryNDay: 'Every N day',
|
||||
schedulingEachSelectedDay: 'Each selected day',
|
||||
schedulingSetWeekDayMode: 'Switch to week days',
|
||||
schedulingSetMonthDayMode: 'Switch to month days',
|
||||
schedulingHour: 'Hour',
|
||||
schedulingEveryNHour: 'Every N hour',
|
||||
schedulingEachSelectedHour: 'Each selected hour',
|
||||
schedulingEveryNHour: 'Every N hour',
|
||||
schedulingMinute: 'Minute',
|
||||
schedulingEveryNMinute: 'Every N minute',
|
||||
schedulingEachSelectedMinute: 'Each selected minute',
|
||||
schedulingEveryNMinute: 'Every N minute',
|
||||
selectTableAllMonth: 'Every month',
|
||||
selectTableAllDay: 'Every day',
|
||||
selectTableAllHour: 'Every hour',
|
||||
selectTableAllMinute: 'Every 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',
|
||||
jobModalTitle: 'Job {job}',
|
||||
jobId: 'ID',
|
||||
jobType: 'Type',
|
||||
jobName: 'Name',
|
||||
jobNamePlaceholder: 'Name of your job (forbidden: "_")',
|
||||
jobStart: 'Start',
|
||||
@@ -193,8 +227,10 @@ var messages = {
|
||||
jobTag: 'Tag',
|
||||
jobScheduling: 'Scheduling',
|
||||
jobState: 'State',
|
||||
jobStateEnabled: 'Enabled',
|
||||
jobStateDisabled: 'Disabled',
|
||||
jobTimezone: 'Timezone',
|
||||
jobServerTimezone: 'xo-server',
|
||||
jobServerTimezone: 'Server',
|
||||
runJob: 'Run job',
|
||||
runJobVerbose: 'One shot running started. See overview for logs.',
|
||||
jobStarted: 'Started',
|
||||
@@ -209,6 +245,14 @@ var messages = {
|
||||
noJobs: 'No jobs found.',
|
||||
noSchedules: 'No schedules found',
|
||||
jobActionPlaceHolder: 'Select a xo-server API command',
|
||||
jobTimeoutPlaceHolder: ' Job timeout (seconds)',
|
||||
jobSchedules: 'Schedules',
|
||||
jobScheduleNamePlaceHolder: 'Name of your schedule',
|
||||
jobScheduleJobPlaceHolder: 'Select a Job',
|
||||
jobOwnerPlaceholder: 'Job owner',
|
||||
jobUserNotFound: 'This job\'s creator no longer exists',
|
||||
backupUserNotFound: 'This backup\'s creator no longer exists',
|
||||
backupOwner: 'Backup owner',
|
||||
|
||||
// ------ New backup -----
|
||||
newBackupSelection: 'Select your backup type:',
|
||||
@@ -217,6 +261,19 @@ var messages = {
|
||||
smartBackup: 'Smart backup',
|
||||
localRemoteWarningTitle: 'Local remote selected',
|
||||
localRemoteWarningMessage: 'Warning: local remotes will use limited XOA disk space. Only for advanced users.',
|
||||
backupVersionWarning: 'Warning: this feature works only with XenServer 6.5 or newer.',
|
||||
editBackupVmsTitle: 'VMs',
|
||||
editBackupSmartStatusTitle: 'VMs statuses',
|
||||
editBackupSmartResidentOn: 'Resident on',
|
||||
editBackupSmartPools: 'Pools',
|
||||
editBackupSmartTags: 'Tags',
|
||||
editBackupSmartTagsTitle: 'VMs Tags',
|
||||
editBackupNot: 'Reverse',
|
||||
editBackupTagTitle: 'Tag',
|
||||
editBackupReportTitle: 'Report',
|
||||
editBackupReportEnable: 'Enable immediately after creation',
|
||||
editBackupDepthTitle: 'Depth',
|
||||
editBackupRemoteTitle: 'Remote',
|
||||
|
||||
// ------ New Remote -----
|
||||
remoteList: 'Remote stores for backup',
|
||||
@@ -232,12 +289,42 @@ var messages = {
|
||||
remoteTestError: 'Error',
|
||||
remoteTestStep: 'Test Step',
|
||||
remoteTestFile: 'Test file',
|
||||
remoteTestName: 'Test name',
|
||||
remoteTestNameFailure: 'Remote name already exists!',
|
||||
remoteTestSuccessMessage: 'The remote appears to work correctly',
|
||||
remoteConnectionFailed: 'Connection failed',
|
||||
|
||||
// ------ Remote -----
|
||||
remoteName: 'Name',
|
||||
remotePath: 'Path',
|
||||
remoteState: 'State',
|
||||
remoteDevice: 'Device',
|
||||
remoteShare: 'Share',
|
||||
remoteAction: 'Action',
|
||||
remoteAuth: 'Auth',
|
||||
remoteMounted: 'Mounted',
|
||||
remoteUnmounted: 'Unmounted',
|
||||
remoteConnectTip: 'Connect',
|
||||
remoteDisconnectTip: 'Disconnect',
|
||||
remoteConnected: 'Connected',
|
||||
remoteDisconnected: 'Disconnected',
|
||||
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',
|
||||
@@ -256,11 +343,21 @@ var messages = {
|
||||
newSrInUse: 'in use',
|
||||
newSrSize: 'Size',
|
||||
newSrCreate: 'Create',
|
||||
newSrNamePlaceHolder: 'Storage name',
|
||||
newSrDescPlaceHolder: 'Storage description',
|
||||
newSrAddressPlaceHolder: 'Address',
|
||||
newSrPortPlaceHolder: '[port]',
|
||||
newSrUsernamePlaceHolder: 'Username',
|
||||
newSrPasswordPlaceHolder: 'Password',
|
||||
newSrLvmDevicePlaceHolder: 'Device, e.g /dev/sda…',
|
||||
newSrLocalPathPlaceHolder: '/path/to/directory',
|
||||
|
||||
// ----- Acls, Users, Groups ------
|
||||
subjectName: 'Users/Groups',
|
||||
objectName: 'Object',
|
||||
aclNoneFound: 'No acls found',
|
||||
roleName: 'Role',
|
||||
aclCreate: 'Create',
|
||||
newGroupName: 'New Group Name',
|
||||
createGroup: 'Create Group',
|
||||
createGroupButton: 'Create',
|
||||
@@ -289,6 +386,7 @@ var messages = {
|
||||
selectPermission: 'Select Permission',
|
||||
|
||||
// ----- Plugins ------
|
||||
noPlugins: 'No plugins found',
|
||||
autoloadPlugin: 'Auto-load at server start',
|
||||
savePluginConfiguration: 'Save configuration',
|
||||
deletePluginConfiguration: 'Delete configuration',
|
||||
@@ -343,15 +441,22 @@ var messages = {
|
||||
// ----- SR actions -----
|
||||
srRescan: 'Rescan all disks',
|
||||
srReconnectAll: 'Connect to all hosts',
|
||||
srDisconnectAll: 'Disconnect to all hosts',
|
||||
srDisconnectAll: 'Disconnect from all hosts',
|
||||
srForget: 'Forget this SR',
|
||||
srsForget: 'Forget SRs',
|
||||
srRemoveButton: 'Remove this SR',
|
||||
srNoVdis: 'No VDIs in this storage',
|
||||
// ----- Pool general -----
|
||||
poolTitleRamUsage: 'Pool RAM usage:',
|
||||
poolRamUsage: '{used} used on {total}',
|
||||
poolMaster: 'Master:',
|
||||
displayAllHosts: 'Display all hosts of this pool',
|
||||
displayAllStorages: 'Display all storages of this pool',
|
||||
displayAllVMs: 'Display all VMs of this pool',
|
||||
// ----- Pool tabs -----
|
||||
hostsTabName: 'Hosts',
|
||||
vmsTabName: 'Vms',
|
||||
srsTabName: 'Srs',
|
||||
// ----- Pool advanced tab -----
|
||||
poolHaStatus: 'High Availability',
|
||||
poolHaEnabled: 'Enabled',
|
||||
@@ -363,6 +468,7 @@ var messages = {
|
||||
noHost: 'No hosts',
|
||||
memoryLeftTooltip: '{used}% used ({free} free)',
|
||||
// ----- Pool network tab -----
|
||||
pif: 'PIF',
|
||||
poolNetworkNameLabel: 'Name',
|
||||
poolNetworkDescription: 'Description',
|
||||
poolNetworkPif: 'PIFs',
|
||||
@@ -372,6 +478,11 @@ var messages = {
|
||||
poolNetworkPifDetached: 'Disconnected',
|
||||
showPifs: 'Show PIFs',
|
||||
hidePifs: 'Hide PIFs',
|
||||
showDetails: 'Show details',
|
||||
hideDetails: 'Hide details',
|
||||
// ----- Pool stats tab -----
|
||||
poolNoStats: 'No stats',
|
||||
poolAllHosts: 'All hosts',
|
||||
// ----- Pool actions ------
|
||||
addSrLabel: 'Add SR',
|
||||
addVmLabel: 'Add VM',
|
||||
@@ -386,7 +497,11 @@ var messages = {
|
||||
restartHostAgent: 'Restart toolstack',
|
||||
forceRebootHostLabel: 'Force reboot',
|
||||
rebootHostLabel: 'Reboot',
|
||||
rebootUpdateHostLabel: 'Reboot for applying updates',
|
||||
noHostsAvailableErrorTitle: 'Error while restarting host',
|
||||
noHostsAvailableErrorMessage: 'Some VMs cannot be migrated before restarting this host. Please try force reboot.',
|
||||
failHostBulkRestartTitle: 'Error while restarting hosts',
|
||||
failHostBulkRestartMessage: '{failedHosts}/{totalHosts} host{failedHosts, plural, one {} other {s}} could not be restarted.',
|
||||
rebootUpdateHostLabel: 'Reboot to apply updates',
|
||||
emergencyModeLabel: 'Emergency mode',
|
||||
// ----- Host tabs -----
|
||||
storageTabName: 'Storage',
|
||||
@@ -394,6 +509,7 @@ var messages = {
|
||||
// ----- host stat tab -----
|
||||
statLoad: 'Load average',
|
||||
// ----- host advanced tab -----
|
||||
memoryHostState: 'RAM Usage: {memoryUsed}',
|
||||
hardwareHostSettingsLabel: 'Hardware',
|
||||
hostAddress: 'Address',
|
||||
hostStatus: 'Status',
|
||||
@@ -413,6 +529,16 @@ var messages = {
|
||||
hostLicenseType: 'Type',
|
||||
hostLicenseSocket: 'Socket',
|
||||
hostLicenseExpiry: 'Expiry',
|
||||
supplementalPacks: 'Installed supplemental packs',
|
||||
supplementalPackNew: 'Install new supplemental pack',
|
||||
supplementalPackPoolNew: 'Install supplemental pack on every host',
|
||||
supplementalPackTitle: '{name} (by {author})',
|
||||
supplementalPackInstallStartedTitle: 'Installation started',
|
||||
supplementalPackInstallStartedMessage: 'Installing new supplemental pack...',
|
||||
supplementalPackInstallErrorTitle: 'Installation error',
|
||||
supplementalPackInstallErrorMessage: 'The installation of the supplemental pack failed.',
|
||||
supplementalPackInstallSuccessTitle: 'Installation success',
|
||||
supplementalPackInstallSuccessMessage: 'Supplemental pack successfully installed.',
|
||||
// ----- Host net tabs -----
|
||||
networkCreateButton: 'Add a network',
|
||||
networkCreateBondedButton: 'Add a bonded network',
|
||||
@@ -420,6 +546,7 @@ var messages = {
|
||||
pifNetworkLabel: 'Network',
|
||||
pifVlanLabel: 'VLAN',
|
||||
pifAddressLabel: 'Address',
|
||||
pifModeLabel: 'Mode',
|
||||
pifMacLabel: 'MAC',
|
||||
pifMtuLabel: 'MTU',
|
||||
pifStatusLabel: 'Status',
|
||||
@@ -428,10 +555,18 @@ var messages = {
|
||||
pifNoInterface: 'No physical interface detected',
|
||||
pifInUse: 'This interface is currently in use',
|
||||
defaultLockingMode: 'Default locking mode',
|
||||
pifConfigureIp: 'Configure IP address',
|
||||
configIpErrorTitle: 'Invalid parameters',
|
||||
configIpErrorMessage: 'IP address and netmask required',
|
||||
staticIp: 'Static IP address',
|
||||
netmask: 'Netmask',
|
||||
dns: 'DNS',
|
||||
gateway: 'Gateway',
|
||||
// ----- Host storage tabs -----
|
||||
addSrDeviceButton: 'Add a storage',
|
||||
srNameLabel: 'Name',
|
||||
srType: 'Type',
|
||||
pbdAction: 'Action',
|
||||
pbdStatus: 'Status',
|
||||
pbdStatusConnected: 'Connected',
|
||||
pbdStatusDisconnected: 'Disconnected',
|
||||
@@ -510,7 +645,7 @@ var messages = {
|
||||
copyToClipboardLabel: 'Copy',
|
||||
ctrlAltDelButtonLabel: 'Ctrl+Alt+Del',
|
||||
tipLabel: 'Tip:',
|
||||
tipConsoleLabel: 'non-US keyboard could have issues with console: switch your own layout to US.',
|
||||
tipConsoleLabel: 'Due to a XenServer issue, non-US keyboard layouts aren\'t well supported. Switch your own layout to US to workaround it.',
|
||||
hideHeaderTooltip: 'Hide infos',
|
||||
showHeaderTooltip: 'Show infos',
|
||||
|
||||
@@ -552,6 +687,14 @@ var messages = {
|
||||
vbdNoVbd: 'No disks',
|
||||
vbdConnect: 'Connect VBD',
|
||||
vbdDisconnect: 'Disconnect VBD',
|
||||
vdbBootable: 'Bootable',
|
||||
vdbReadonly: 'Readonly',
|
||||
vbdAction: 'Action',
|
||||
vdbCreate: 'Create',
|
||||
vdbNamePlaceHolder: 'Disk name',
|
||||
vdbSizePlaceHolder: 'Size',
|
||||
saveBootOption: 'Save',
|
||||
resetBootOption: 'Reset',
|
||||
|
||||
// ----- VM network tab -----
|
||||
vifCreateDeviceButton: 'New device',
|
||||
@@ -574,6 +717,8 @@ var messages = {
|
||||
vifLockedNetworkNoIps: 'Network locked and no IPs are allowed for this interface',
|
||||
vifUnLockedNetwork: 'Network not locked',
|
||||
vifUnknownNetwork: 'Unknown network',
|
||||
vifAction: 'Action',
|
||||
vifCreate: 'Create',
|
||||
|
||||
// ----- VM snapshot tab -----
|
||||
noSnapshots: 'No snapshots',
|
||||
@@ -586,6 +731,7 @@ var messages = {
|
||||
snapshotDate: 'Creation date',
|
||||
snapshotName: 'Name',
|
||||
snapshotAction: 'Action',
|
||||
snapshotQuiesce: 'Quiesced snapshot',
|
||||
|
||||
// ----- VM log tab -----
|
||||
logRemoveAll: 'Remove all logs',
|
||||
@@ -617,6 +763,8 @@ var messages = {
|
||||
osKernel: 'OS kernel',
|
||||
autoPowerOn: 'Auto power on',
|
||||
ha: 'HA',
|
||||
vmAffinityHost: 'Affinity host',
|
||||
noAffinityHost: 'None',
|
||||
originalTemplate: 'Original template',
|
||||
unknownOsName: 'Unknown',
|
||||
unknownOsKernel: 'Unknown',
|
||||
@@ -646,7 +794,7 @@ var messages = {
|
||||
poolPanel: 'Pool{pools, plural, one {} other {s}}',
|
||||
hostPanel: 'Host{hosts, plural, one {} other {s}}',
|
||||
vmPanel: 'VM{vms, plural, one {} other {s}}',
|
||||
memoryStatePanel: 'RAM Usage',
|
||||
memoryStatePanel: 'RAM Usage:',
|
||||
cpuStatePanel: 'CPUs Usage',
|
||||
vmStatePanel: 'VMs Power state',
|
||||
taskStatePanel: 'Pending tasks',
|
||||
@@ -665,6 +813,9 @@ var messages = {
|
||||
srUsageStatePanel: 'Storage Usage',
|
||||
srTopUsageStatePanel: 'Top 5 SR Usage (in %)',
|
||||
vmsStates: '{running} running ({halted} halted)',
|
||||
dashboardStatsButtonRemoveAll: 'Clear selection',
|
||||
dashboardStatsButtonAddAllHost: 'Add all hosts',
|
||||
dashboardStatsButtonAddAllVM: 'Add all VMs',
|
||||
|
||||
// --- Stats board --
|
||||
weekHeatmapData: '{value} {date, date, medium}',
|
||||
@@ -682,10 +833,10 @@ var messages = {
|
||||
comingSoon: 'Coming soon!',
|
||||
|
||||
// ----- Health -----
|
||||
orphanedVdis: 'Orphaned VDIs',
|
||||
orphanedVms: 'Orphaned VMs',
|
||||
orphanedVdis: 'Orphaned snapshot VDIs',
|
||||
orphanedVms: 'Orphaned VMs snapshot',
|
||||
noOrphanedObject: 'No orphans',
|
||||
removeAllOrphanedObject: 'Remove all orphaned VDIs',
|
||||
removeAllOrphanedObject: 'Remove all orphaned snapshot VDIs',
|
||||
vmNameLabel: 'Name',
|
||||
vmNameDescription: 'Description',
|
||||
vmContainer: 'Resident on',
|
||||
@@ -709,9 +860,13 @@ var messages = {
|
||||
newVmPerfPanel: 'Performances',
|
||||
newVmVcpusLabel: 'vCPUs',
|
||||
newVmRamLabel: 'RAM',
|
||||
newVmStaticMaxLabel: 'Static memory max',
|
||||
newVmDynamicMinLabel: 'Dynamic memory min',
|
||||
newVmDynamicMaxLabel: 'Dynamic memory max',
|
||||
newVmInstallSettingsPanel: 'Install settings',
|
||||
newVmIsoDvdLabel: 'ISO/DVD',
|
||||
newVmNetworkLabel: 'Network',
|
||||
newVmInstallNetworkPlaceHolder: 'e.g: http://httpredir.debian.org/debian',
|
||||
newVmPvArgsLabel: 'PV Args',
|
||||
newVmPxeLabel: 'PXE',
|
||||
newVmInterfacesPanel: 'Interfaces',
|
||||
@@ -719,7 +874,6 @@ var messages = {
|
||||
newVmAddInterface: 'Add interface',
|
||||
newVmDisksPanel: 'Disks',
|
||||
newVmSrLabel: 'SR',
|
||||
newVmBootableLabel: 'Bootable',
|
||||
newVmSizeLabel: 'Size',
|
||||
newVmAddDisk: 'Add disk',
|
||||
newVmSummaryPanel: 'Summary',
|
||||
@@ -745,14 +899,16 @@ var messages = {
|
||||
newVmFirstIndex: 'First index:',
|
||||
newVmNumberRecalculate: 'Recalculate VMs number',
|
||||
newVmNameRefresh: 'Refresh VMs name',
|
||||
newVmAffinityHost: 'Affinity host',
|
||||
newVmAdvancedPanel: 'Advanced',
|
||||
newVmShowAdvanced: 'Show advanced settings',
|
||||
newVmHideAdvanced: 'Hide advanced settings',
|
||||
newVmShare: 'Share this VM',
|
||||
|
||||
// ----- Self -----
|
||||
resourceSets: 'Resource sets',
|
||||
noResourceSets: 'No resource sets.',
|
||||
loadingResourceSets: 'Loading resource sets...',
|
||||
loadingResourceSets: 'Loading resource sets',
|
||||
resourceSetName: 'Resource set name',
|
||||
recomputeResourceSets: 'Recompute all limits',
|
||||
saveResourceSet: 'Save',
|
||||
@@ -773,6 +929,8 @@ var messages = {
|
||||
maxCpus: 'Maximum CPUs',
|
||||
maxRam: 'Maximum RAM (GiB)',
|
||||
maxDiskSpace: 'Maximum disk space',
|
||||
ipPool: 'IP pool',
|
||||
quantity: 'Quantity',
|
||||
noResourceSetLimits: 'No limits.',
|
||||
totalResource: 'Total:',
|
||||
remainingResource: 'Remaining:',
|
||||
@@ -807,26 +965,44 @@ var messages = {
|
||||
xsTasks: 'Currently, there are not any pending XenServer tasks',
|
||||
|
||||
// ---- Backup views ---
|
||||
backupSchedules: 'Schedules',
|
||||
getRemote: 'Get remote',
|
||||
listRemote: 'List Remote',
|
||||
simpleBackup: 'simple',
|
||||
delta: 'delta',
|
||||
restoreBackups: 'Restore Backups',
|
||||
noRemotes: 'No remotes',
|
||||
remoteEnabled: 'enabled',
|
||||
remoteError: 'error',
|
||||
restoreBackupsInfo: 'Click on a VM to display restore options',
|
||||
remoteEnabled: 'Enabled',
|
||||
remoteError: 'Error',
|
||||
noBackup: 'No backup available',
|
||||
backupVmNameColumn: 'VM Name',
|
||||
backupTagColumn: 'Backup Tag',
|
||||
backupTags: 'Tags',
|
||||
lastBackupColumn: 'Last Backup',
|
||||
availableBackupsColumn: 'Available Backups',
|
||||
restoreColumn: 'Restore',
|
||||
restoreTip: 'View restore options',
|
||||
backupRestoreErrorTitle: 'Missing parameters',
|
||||
backupRestoreErrorMessage: 'Choose a SR and a backup',
|
||||
displayBackup: 'Display backups',
|
||||
importBackupTitle: 'Import VM',
|
||||
importBackupMessage: 'Starting your backup import',
|
||||
vmsToBackup: 'VMs to backup',
|
||||
|
||||
// ----- Restore files view -----
|
||||
listRemoteBackups: 'List remote backups',
|
||||
restoreFiles: 'Restore backup files',
|
||||
restoreFilesError: 'Invalid options',
|
||||
restoreFilesFromBackup: 'Restore file from {name}',
|
||||
restoreFilesSelectBackup: 'Select a backup…',
|
||||
restoreFilesSelectDisk: 'Select a disk…',
|
||||
restoreFilesSelectPartition: 'Select a partition…',
|
||||
restoreFilesSelectFolderPath: 'Folder path',
|
||||
restoreFilesSelectFiles: 'Select a file…',
|
||||
restoreFileContentNotFound: 'Content not found',
|
||||
restoreFilesNoFilesSelected: 'No files selected',
|
||||
restoreFilesSelectedFiles: 'Selected files ({files}):',
|
||||
restoreFilesDiskError: 'Error while scanning disk',
|
||||
restoreFilesSelectAllFiles: 'Select all this folder\'s files',
|
||||
restoreFilesUnselectAll: 'Unselect all files',
|
||||
|
||||
// ----- 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}}?',
|
||||
@@ -883,7 +1059,7 @@ var messages = {
|
||||
importBackupModalTitle: 'Import a {name} Backup',
|
||||
importBackupModalStart: 'Start VM after restore',
|
||||
importBackupModalSelectBackup: 'Select your backup…',
|
||||
removeAllOrphanedModalWarning: 'Are you sure you want to remove all orphaned VDIs?',
|
||||
removeAllOrphanedModalWarning: 'Are you sure you want to remove all orphaned snapshot VDIs?',
|
||||
removeAllLogsModalTitle: 'Remove all logs',
|
||||
removeAllLogsModalWarning: 'Are you sure you want to remove all logs?',
|
||||
definitiveMessageModal: 'This operation is definitive.',
|
||||
@@ -897,13 +1073,27 @@ var messages = {
|
||||
trialReadyModalText: 'During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!',
|
||||
|
||||
// ----- Servers -----
|
||||
serverLabel: 'Label',
|
||||
serverHost: 'Host',
|
||||
serverUsername: 'Username',
|
||||
serverPassword: 'Password',
|
||||
serverAction: 'Action',
|
||||
serverReadOnly: 'Read Only',
|
||||
serverConnect: 'Connect server',
|
||||
serverDisconnect: 'Disconnect server',
|
||||
serverPlaceHolderUser: 'username',
|
||||
serverPlaceHolderPassword: 'password',
|
||||
serverPlaceHolderAddress: 'address[:port]',
|
||||
serverPlaceHolderLabel: 'label',
|
||||
serverConnect: 'Connect',
|
||||
serverError: 'Error',
|
||||
serverAddFailed: 'Adding server failed',
|
||||
serverStatus: 'Status',
|
||||
serverConnectionFailed: 'Connection failed',
|
||||
serverConnecting: 'Connecting...',
|
||||
serverConnected: 'Connected',
|
||||
serverDisconnected: 'Disconnected',
|
||||
serverAuthFailed: 'Authentication error',
|
||||
serverUnknownError: 'Unknown error',
|
||||
|
||||
// ----- Copy VM -----
|
||||
copyVm: 'Copy VM',
|
||||
@@ -947,11 +1137,11 @@ var messages = {
|
||||
|
||||
// ----- About View -----
|
||||
xenOrchestra: 'Xen Orchestra',
|
||||
xenOrchestraServer: 'server',
|
||||
xenOrchestraWeb: 'web client',
|
||||
xenOrchestraServer: 'Xen Orchestra server',
|
||||
xenOrchestraWeb: 'Xen Orchestra web client',
|
||||
noProSupport: 'No pro support provided!',
|
||||
noProductionUse: 'Use in production at your own risks',
|
||||
downloadXoa: 'You can download our turnkey appliance at',
|
||||
downloadXoaFromWebsite: 'You can download our turnkey appliance at {website}',
|
||||
bugTracker: 'Bug Tracker',
|
||||
bugTrackerText: 'Issues? Report it!',
|
||||
community: 'Community',
|
||||
@@ -963,9 +1153,9 @@ var messages = {
|
||||
documentation: 'Documentation',
|
||||
documentationText: 'Read our official doc',
|
||||
proSupportIncluded: 'Pro support included',
|
||||
xoAccount: 'Acces your XO Account',
|
||||
xoAccount: 'Access your XO Account',
|
||||
openTicket: 'Report a problem',
|
||||
openTicketText: 'Problem? Open a ticket !',
|
||||
openTicketText: 'Problem? Open a ticket!',
|
||||
|
||||
// ----- Upgrade Panel -----
|
||||
upgradeNeeded: 'Upgrade needed',
|
||||
@@ -981,11 +1171,17 @@ var messages = {
|
||||
trial: 'Trial',
|
||||
settings: 'Settings',
|
||||
proxySettings: 'Proxy settings',
|
||||
proxySettingsHostPlaceHolder: 'Host (myproxy.example.org)',
|
||||
proxySettingsPortPlaceHolder: 'Port (eg: 3128)',
|
||||
proxySettingsUsernamePlaceHolder: 'Username',
|
||||
proxySettingsPasswordPlaceHolder: 'Password',
|
||||
updateRegistrationEmailPlaceHolder: 'Your email account',
|
||||
updateRegistrationPasswordPlaceHolder: 'Your password',
|
||||
update: 'Update',
|
||||
refresh: 'Refresh',
|
||||
upgrade: 'Upgrade',
|
||||
noUpdaterCommunity: 'No updater available for Community Edition',
|
||||
noUpdaterSubscribe: 'Please consider subscribe and try it with all features for free during 15 days on',
|
||||
considerSubscribe: 'Please consider subscribe and try it with all features for free during 15 days on {link}.',
|
||||
noUpdaterWarning: 'Manual update could break your current installation due to dependencies issues, do it with caution',
|
||||
currentVersion: 'Current version:',
|
||||
register: 'Register',
|
||||
@@ -1048,7 +1244,7 @@ var messages = {
|
||||
others: 'Others',
|
||||
|
||||
// ----- Logs -----
|
||||
loadingLogs: 'Loading logs...',
|
||||
loadingLogs: 'Loading logs…',
|
||||
logUser: 'User',
|
||||
logMethod: 'Method',
|
||||
logParams: 'Params',
|
||||
@@ -1062,6 +1258,9 @@ var messages = {
|
||||
logDeleteAll: 'Delete all logs',
|
||||
logDeleteAllTitle: 'Delete all logs',
|
||||
logDeleteAllMessage: 'Are you sure you want to delete all the logs?',
|
||||
logIndicationToEnable: 'Click to enable',
|
||||
logIndicationToDisable: 'Click to disable',
|
||||
reportBug: 'Report a bug',
|
||||
|
||||
// ----- IPs ------
|
||||
ipPoolName: 'Name',
|
||||
@@ -1074,6 +1273,8 @@ var messages = {
|
||||
ipsDeleteAllMessage: 'Are you sure you want to delete all the IP pools?',
|
||||
ipsVifs: 'VIFs',
|
||||
ipsNotUsed: 'Not used',
|
||||
ipPoolUnknownVif: 'unknown VIF',
|
||||
ipPoolNameAlreadyExists: 'Name already exists',
|
||||
|
||||
// ----- Shortcuts -----
|
||||
shortcutModalTitle: 'Keyboard shortcuts',
|
||||
@@ -1081,6 +1282,7 @@ var messages = {
|
||||
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_GO_TO_SRS: 'Go to SRs list',
|
||||
shortcut_CREATE_VM: 'Create a new VM',
|
||||
shortcut_UNFOCUS: 'Unfocus field',
|
||||
shortcut_HELP: 'Show shortcuts key bindings',
|
||||
@@ -1089,7 +1291,75 @@ var messages = {
|
||||
shortcut_NAV_DOWN: 'Next item',
|
||||
shortcut_NAV_UP: 'Previous item',
|
||||
shortcut_SELECT: 'Select item',
|
||||
shortcut_JUMP_INTO: 'Open'
|
||||
shortcut_JUMP_INTO: 'Open',
|
||||
|
||||
// ----- Settings/ACLs -----
|
||||
settingsAclsButtonTooltipVM: 'VM',
|
||||
settingsAclsButtonTooltiphost: 'Hosts',
|
||||
settingsAclsButtonTooltippool: 'Pool',
|
||||
settingsAclsButtonTooltipSR: 'SR',
|
||||
settingsAclsButtonTooltipnetwork: 'Network',
|
||||
|
||||
// ----- Config -----
|
||||
noConfigFile: 'No config file selected',
|
||||
importTip: 'Try dropping a config file here, or click to select a config file to upload.',
|
||||
config: 'Config',
|
||||
importConfig: 'Import',
|
||||
importConfigSuccess: 'Config file successfully imported',
|
||||
importConfigError: 'Error while importing config file',
|
||||
exportConfig: 'Export',
|
||||
downloadConfig: 'Download current config',
|
||||
noConfigImportCommunity: 'No config import available for Community Edition',
|
||||
|
||||
// ----- SR -----
|
||||
srReconnectAllModalTitle: 'Reconnect all hosts',
|
||||
srReconnectAllModalMessage: 'This will reconnect this SR to all its hosts.',
|
||||
srsReconnectAllModalMessage: 'This will reconnect each selected SR to its host (local SR) or to every hosts of its pool (shared SR).',
|
||||
srDisconnectAllModalTitle: 'Disconnect all hosts',
|
||||
srDisconnectAllModalMessage: 'This will disconnect this SR from all its hosts.',
|
||||
srsDisconnectAllModalMessage: 'This will disconnect each selected SR from its host (local SR) or from every hosts of its pool (shared SR).',
|
||||
srForgetModalTitle: 'Forget SR',
|
||||
srsForgetModalTitle: 'Forget selected SRs',
|
||||
srForgetModalMessage: 'Are you sure you want to forget this SR? VDIs on this storage won\'t be removed.',
|
||||
srsForgetModalMessage: 'Are you sure you want to forget all the selected SRs? VDIs on these storages won\'t be removed.',
|
||||
srAllDisconnected: 'Disconnected',
|
||||
srSomeConnected: 'Partially connected',
|
||||
srAllConnected: 'Connected',
|
||||
|
||||
// ----- XOSAN -----
|
||||
xosanTitle: 'XOSAN',
|
||||
xosanSrTitle: 'Xen Orchestra SAN SR',
|
||||
xosanAvailableSrsTitle: 'Select local SRs (lvm)',
|
||||
xosanSuggestions: 'Suggestions',
|
||||
xosanName: 'Name',
|
||||
xosanHost: 'Host',
|
||||
xosanHosts: 'Hosts',
|
||||
xosanVolumeId: 'Volume ID',
|
||||
xosanSize: 'Size',
|
||||
xosanUsedSpace: 'Used space',
|
||||
xosanNeedPack: 'XOSAN pack needs to be installed on each host of the pool.',
|
||||
xosanInstallIt: 'Install it now!',
|
||||
xosanInstallPackTitle: 'Install XOSAN pack on {pool}',
|
||||
xosanSelect2Srs: 'Select at least 2 SRs',
|
||||
xosanLayout: 'Layout',
|
||||
xosanRedundancy: 'Redundancy',
|
||||
xosanCapacity: 'Capacity',
|
||||
xosanAvailableSpace: 'Available space',
|
||||
xosanDiskLossLegend: '* Can fail without data loss',
|
||||
xosanCreate: 'Create',
|
||||
xosanInstalling: 'Installing XOSAN. Please wait...',
|
||||
xosanCommunity: 'No XOSAN available for Community Edition',
|
||||
// Pack download modal
|
||||
xosanInstallCloudPlugin: 'Install cloud plugin first',
|
||||
xosanLoadCloudPlugin: 'Load cloud plugin first',
|
||||
xosanLoading: 'Loading...',
|
||||
xosanNotAvailable: 'XOSAN is not available at the moment',
|
||||
xosanRegisterBeta: 'Register for the XOSAN beta',
|
||||
xosanSuccessfullyRegistered: 'You have successfully registered for the XOSAN beta. Please wait until your request has been approved.',
|
||||
xosanInstallPackOnHosts: 'Install XOSAN pack on these hosts:',
|
||||
xosanInstallPack: 'Install {pack} v{version}?',
|
||||
xosanNoPackFound: 'No compatible XOSAN pack found for your XenServer versions.',
|
||||
xosanPackRequirements: 'At least one of these version requirements must be satisfied by all the hosts in this pool:'
|
||||
|
||||
}
|
||||
forEach(messages, function (message, id) {
|
||||
|
||||
@@ -44,7 +44,17 @@ import {
|
||||
export default class IsoDevice extends Component {
|
||||
_getPredicate = createSelector(
|
||||
() => this.props.vm.$pool,
|
||||
poolId => sr => sr.$pool === poolId && sr.SR_type === 'iso'
|
||||
() => this.props.vm.$container,
|
||||
(vmPool, vmContainer) => sr => {
|
||||
const vmRunning = vmContainer !== vmPool
|
||||
const sameHost = vmContainer === sr.$container
|
||||
const samePool = vmPool === sr.$pool
|
||||
|
||||
return (
|
||||
samePool && (vmRunning ? sr.shared || sameHost : true) &&
|
||||
sr.SR_type === 'iso' || sr.SR_type === 'udev' && sr.size
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
_handleInsert = iso => {
|
||||
@@ -67,7 +77,6 @@ export default class IsoDevice extends Component {
|
||||
<SelectVdi
|
||||
srPredicate={this._getPredicate()}
|
||||
onChange={this._handleInsert}
|
||||
ref='selectIso'
|
||||
value={mountedIso}
|
||||
/>
|
||||
<span className='input-group-btn'>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Component } from 'react'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
onChange: propTypes.func,
|
||||
placeholder: propTypes.string,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
defaultValue: propTypes.any
|
||||
})
|
||||
export default class AbstractInput extends Component {
|
||||
set value (value) {
|
||||
this.refs.input.value = value === undefined ? '' : String(value)
|
||||
}
|
||||
|
||||
get value () {
|
||||
const { value } = this.refs.input
|
||||
return !value ? undefined : value
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
import map from 'lodash/map'
|
||||
import filter from 'lodash/filter'
|
||||
import React from 'react'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { filter, map } from 'lodash'
|
||||
|
||||
import _ from '../intl'
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
import { propsEqual } from '../utils'
|
||||
import { EMPTY_ARRAY } from '../utils'
|
||||
|
||||
import GenericInput from './generic-input'
|
||||
import {
|
||||
@@ -12,174 +13,110 @@ import {
|
||||
forceDisplayOptionalAttr
|
||||
} from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class ArrayItem extends Component {
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children } = this.props
|
||||
|
||||
return (
|
||||
<li className='list-group-item clearfix'>
|
||||
{cloneElement(children, {
|
||||
ref: 'input'
|
||||
})}
|
||||
<button disabled={children.props.disabled} className='btn btn-danger pull-xs-right' type='button' onClick={this.props.onDelete}>
|
||||
{_('remove')}
|
||||
</button>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
depth: propTypes.number,
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
defaultValue: propTypes.array
|
||||
uiSchema: propTypes.object
|
||||
})
|
||||
export default class ArrayInput extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
use: props.required || forceDisplayOptionalAttr(props),
|
||||
children: this._makeChildren(props)
|
||||
}
|
||||
this._nextChildKey = 0
|
||||
@uncontrollableInput()
|
||||
export default class ObjectInput extends Component {
|
||||
state = {
|
||||
use: this.props.required || forceDisplayOptionalAttr(this.props)
|
||||
}
|
||||
|
||||
get value () {
|
||||
if (this.state.use) {
|
||||
return map(this.refs, 'value')
|
||||
}
|
||||
_onAddItem = () => {
|
||||
const { props } = this
|
||||
props.onChange((props.value || EMPTY_ARRAY).concat(undefined))
|
||||
}
|
||||
|
||||
set value (value = []) {
|
||||
this.setState({
|
||||
children: this._makeChildren({ ...this.props, value })
|
||||
})
|
||||
_onChangeItem = (value, name) => {
|
||||
const key = Number(name)
|
||||
|
||||
const { props } = this
|
||||
const newValue = (props.value || EMPTY_ARRAY).slice()
|
||||
newValue[key] = value
|
||||
props.onChange(newValue)
|
||||
}
|
||||
|
||||
_handleOptionalChange = event => {
|
||||
this.setState({
|
||||
use: event.target.checked
|
||||
})
|
||||
}
|
||||
|
||||
_handleAdd = () => {
|
||||
const { children } = this.state
|
||||
this.setState({
|
||||
children: children.concat(this._makeChild(this.props))
|
||||
})
|
||||
}
|
||||
|
||||
_remove (key) {
|
||||
this.setState({
|
||||
children: filter(this.state.children, child => child.key !== key)
|
||||
})
|
||||
}
|
||||
|
||||
_makeChild (props) {
|
||||
const key = String(this._nextChildKey++)
|
||||
const {
|
||||
schema: {
|
||||
items
|
||||
}
|
||||
} = props
|
||||
|
||||
return (
|
||||
<ArrayItem key={key} onDelete={() => { this._remove(key) }}>
|
||||
<GenericInput
|
||||
depth={props.depth}
|
||||
disabled={props.disabled}
|
||||
label={items.title || _('item')}
|
||||
required
|
||||
schema={items}
|
||||
uiSchema={props.uiSchema.items}
|
||||
defaultValue={props.defaultValue}
|
||||
/>
|
||||
</ArrayItem>
|
||||
)
|
||||
}
|
||||
|
||||
_makeChildren ({ defaultValue, ...props }) {
|
||||
return map(defaultValue, defaultValue => {
|
||||
return (
|
||||
this._makeChild({
|
||||
...props,
|
||||
defaultValue
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
if (
|
||||
!propsEqual(
|
||||
this.props,
|
||||
props,
|
||||
[ 'depth', 'disabled', 'label', 'required', 'schema', 'uiSchema' ]
|
||||
)
|
||||
) {
|
||||
this.setState({
|
||||
children: this._makeChildren(props)
|
||||
})
|
||||
}
|
||||
_onRemoveItem = key => {
|
||||
const { props } = this
|
||||
props.onChange(filter(props.value, (_, i) => i !== key))
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
props,
|
||||
state
|
||||
props: {
|
||||
depth = 0,
|
||||
disabled,
|
||||
label,
|
||||
required,
|
||||
schema,
|
||||
uiSchema,
|
||||
value = EMPTY_ARRAY
|
||||
},
|
||||
state: { use }
|
||||
} = this
|
||||
const {
|
||||
disabled,
|
||||
schema
|
||||
} = props
|
||||
const { use } = state
|
||||
const depth = props.depth || 0
|
||||
|
||||
const childDepth = depth + 2
|
||||
const itemSchema = schema.items
|
||||
const itemUiSchema = uiSchema && uiSchema.items
|
||||
|
||||
const itemLabel = itemSchema.title || _('item')
|
||||
|
||||
return (
|
||||
<div style={{'paddingLeft': `${depth}em`}}>
|
||||
<legend>{props.label}</legend>
|
||||
<legend>{label}</legend>
|
||||
{descriptionRender(schema.description)}
|
||||
<hr />
|
||||
{!props.required &&
|
||||
<div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={use}
|
||||
disabled={disabled}
|
||||
onChange={this._handleOptionalChange}
|
||||
type='checkbox'
|
||||
/> {_('fillOptionalInformations')}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
{use &&
|
||||
<div className={'card-block'}>
|
||||
<ul style={{'paddingLeft': 0}} >
|
||||
{map(this.state.children, (child, index) =>
|
||||
cloneElement(child, { ref: index })
|
||||
)}
|
||||
</ul>
|
||||
<button disabled={disabled} className='btn btn-primary pull-xs-right m-t-1 m-r-1' type='button' onClick={this._handleAdd}>
|
||||
{_('add')}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
{!required && <div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={use}
|
||||
disabled={disabled}
|
||||
onChange={this.linkState('use')}
|
||||
type='checkbox'
|
||||
/> {_('fillOptionalInformations')}
|
||||
</label>
|
||||
</div>}
|
||||
{use && <div className='card-block'>
|
||||
<ul style={{'paddingLeft': 0}} >
|
||||
{map(value, (value, key) =>
|
||||
<li className='list-group-item clearfix' key={key}>
|
||||
<GenericInput
|
||||
depth={childDepth}
|
||||
disabled={disabled}
|
||||
label={itemLabel}
|
||||
name={key}
|
||||
onChange={this._onChangeItem}
|
||||
required
|
||||
schema={itemSchema}
|
||||
uiSchema={itemUiSchema}
|
||||
value={value}
|
||||
/>
|
||||
<button
|
||||
className='btn btn-danger pull-right'
|
||||
disabled={disabled}
|
||||
name={key}
|
||||
onClick={() => this._onRemoveItem(key)}
|
||||
type='button'
|
||||
>
|
||||
{_('remove')}
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
<button
|
||||
className='btn btn-primary pull-right mt-1 mr-1'
|
||||
disabled={disabled}
|
||||
onClick={this._onAddItem}
|
||||
type='button'
|
||||
>
|
||||
{_('add')}
|
||||
</button>
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
import React from 'react'
|
||||
import { Toggle } from 'form'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Component from '../base-component'
|
||||
import { Toggle } from '../form'
|
||||
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class BooleanInput extends AbstractInput {
|
||||
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
}
|
||||
|
||||
@uncontrollableInput()
|
||||
export default class BooleanInput extends Component {
|
||||
render () {
|
||||
const { props } = this
|
||||
const {
|
||||
disabled,
|
||||
onChange,
|
||||
value,
|
||||
...props
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<div className='checkbox form-control'>
|
||||
<Toggle
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
ref='input'
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
</PrimitiveInputWrapper>
|
||||
|
||||
@@ -1,33 +1,54 @@
|
||||
import React from 'react'
|
||||
import _ from 'intl'
|
||||
import map from 'lodash/map'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import { createSelector } from 'reselect'
|
||||
import { findIndex, map } from 'lodash'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class EnumInput extends AbstractInput {
|
||||
@uncontrollableInput()
|
||||
export default class EnumInput extends Component {
|
||||
_getSelectedIndex = createSelector(
|
||||
() => this.props.schema.enum,
|
||||
() => {
|
||||
const {
|
||||
schema,
|
||||
value = schema.default
|
||||
} = this.props
|
||||
return value
|
||||
},
|
||||
(enumValues, value) => {
|
||||
const index = findIndex(enumValues, current => current === value)
|
||||
return index === -1 ? '' : index
|
||||
}
|
||||
)
|
||||
|
||||
_onChange = event => {
|
||||
this.props.onChange(this.props.schema.enum[event.target.value])
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const {
|
||||
onChange,
|
||||
disabled,
|
||||
schema: { enum: enumValues, enumNames = enumValues },
|
||||
required
|
||||
} = props
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<PrimitiveInputWrapper {...this.props}>
|
||||
<select
|
||||
className='form-control'
|
||||
defaultValue={props.defaultValue || ''}
|
||||
disabled={props.disabled}
|
||||
onChange={onChange && (event => onChange(event.target.value))}
|
||||
ref='input'
|
||||
disabled={disabled}
|
||||
onChange={this._onChange}
|
||||
required={required}
|
||||
value={this._getSelectedIndex()}
|
||||
>
|
||||
{_('noSelectedValue', message => <option value=''>{message}</option>)}
|
||||
{map(props.schema.enum, (value, index) =>
|
||||
<option value={value} key={index}>{value}</option>
|
||||
{map(enumNames, (name, index) =>
|
||||
<option value={index} key={index}>{name}</option>
|
||||
)}
|
||||
</select>
|
||||
</PrimitiveInputWrapper>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { Component } from 'react'
|
||||
|
||||
import getEventValue from '../get-event-value'
|
||||
import propTypes from '../prop-types'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { EMPTY_OBJECT } from '../utils'
|
||||
|
||||
import ArrayInput from './array-input'
|
||||
@@ -30,35 +32,31 @@ const InputByType = {
|
||||
depth: propTypes.number,
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
onChange: propTypes.func,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
defaultValue: propTypes.any
|
||||
uiSchema: propTypes.object
|
||||
})
|
||||
@uncontrollableInput()
|
||||
export default class GenericInput extends Component {
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
_onChange = event => {
|
||||
const { name, onChange } = this.props
|
||||
onChange && onChange(getEventValue(event), name)
|
||||
}
|
||||
|
||||
render () {
|
||||
const {
|
||||
schema,
|
||||
defaultValue = schema.default,
|
||||
value = schema.default,
|
||||
uiSchema = EMPTY_OBJECT,
|
||||
...opts
|
||||
} = this.props
|
||||
|
||||
const props = {
|
||||
...opts,
|
||||
defaultValue,
|
||||
onChange: this._onChange,
|
||||
schema,
|
||||
uiSchema,
|
||||
ref: 'input'
|
||||
value
|
||||
}
|
||||
|
||||
// Enum, special case.
|
||||
|
||||
@@ -62,19 +62,19 @@ export const PrimitiveInputWrapper = ({ label, required = false, schema, childre
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const forceDisplayOptionalAttr = ({ schema, defaultValue }) => {
|
||||
if (!schema || !defaultValue) {
|
||||
export const forceDisplayOptionalAttr = ({ schema, value }) => {
|
||||
if (!schema || !value) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Array
|
||||
if (schema.items && Array.isArray(defaultValue)) {
|
||||
if (schema.items && Array.isArray(value)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Object
|
||||
for (const key in schema.properties) {
|
||||
if (defaultValue[key]) {
|
||||
if (value[key]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,42 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Combobox from '../combobox'
|
||||
import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class IntegerInput extends AbstractInput {
|
||||
get value () {
|
||||
const { value } = this.refs.input
|
||||
return !value ? undefined : +value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
// Getter/Setter are always inherited together.
|
||||
// `get value` is defined in the subclass, so `set value`
|
||||
// must be defined too.
|
||||
super.value = value
|
||||
@uncontrollableInput()
|
||||
export default class IntegerInput extends Component {
|
||||
_onChange = event => {
|
||||
const value = getEventValue(event)
|
||||
this.props.onChange(value ? +value : undefined)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const { schema } = props
|
||||
const { required, schema } = this.props
|
||||
const {
|
||||
disabled,
|
||||
onChange, // eslint-disable-line no-unused-vars
|
||||
placeholder = schema.default,
|
||||
value,
|
||||
...props
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
value={value === undefined ? '' : String(value)}
|
||||
disabled={disabled}
|
||||
max={schema.max}
|
||||
min={schema.min}
|
||||
onChange={this._onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
step={1}
|
||||
type='number'
|
||||
/>
|
||||
|
||||
@@ -1,38 +1,42 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Combobox from '../combobox'
|
||||
import Component from '../base-component'
|
||||
import getEventValue from '../get-event-value'
|
||||
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class NumberInput extends AbstractInput {
|
||||
get value () {
|
||||
const { value } = this.refs.input
|
||||
return !value ? undefined : +value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
// Getter/Setter are always inherited together.
|
||||
// `get value` is defined in the subclass, so `set value`
|
||||
// must be defined too.
|
||||
super.value = value
|
||||
@uncontrollableInput()
|
||||
export default class NumberInput extends Component {
|
||||
_onChange = event => {
|
||||
const value = getEventValue(event)
|
||||
this.props.onChange(value ? +value : undefined)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
const { schema } = props
|
||||
const { required, schema } = this.props
|
||||
const {
|
||||
disabled,
|
||||
onChange, // eslint-disable-line no-unused-vars
|
||||
placeholder = schema.default,
|
||||
value,
|
||||
...props
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
value={value === undefined ? '' : String(value)}
|
||||
disabled={disabled}
|
||||
max={schema.max}
|
||||
min={schema.min}
|
||||
onChange={this._onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
placeholder={placeholder}
|
||||
required={required}
|
||||
step='any'
|
||||
type='number'
|
||||
/>
|
||||
|
||||
@@ -1,163 +1,97 @@
|
||||
import _ from 'intl'
|
||||
import React, { Component, cloneElement } from 'react'
|
||||
import forEach from 'lodash/forEach'
|
||||
import includes from 'lodash/includes'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import { createSelector } from 'reselect'
|
||||
import { keyBy, map } from 'lodash'
|
||||
|
||||
import _ from '../intl'
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
import { propsEqual } from '../utils'
|
||||
import { EMPTY_OBJECT } from '../utils'
|
||||
|
||||
import GenericInput from './generic-input'
|
||||
|
||||
import {
|
||||
descriptionRender,
|
||||
forceDisplayOptionalAttr
|
||||
} from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
class ObjectItem extends Component {
|
||||
get value () {
|
||||
return this.refs.input.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props } = this
|
||||
|
||||
return (
|
||||
<div className='p-b-1'>
|
||||
{cloneElement(props.children, {
|
||||
ref: 'input'
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
depth: propTypes.number,
|
||||
disabled: propTypes.bool,
|
||||
label: propTypes.any.isRequired,
|
||||
required: propTypes.bool,
|
||||
schema: propTypes.object.isRequired,
|
||||
uiSchema: propTypes.object,
|
||||
defaultValue: propTypes.object
|
||||
uiSchema: propTypes.object
|
||||
})
|
||||
@uncontrollableInput()
|
||||
export default class ObjectInput extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
use: Boolean(props.required) || forceDisplayOptionalAttr(props),
|
||||
children: this._makeChildren(props)
|
||||
}
|
||||
state = {
|
||||
use: this.props.required || forceDisplayOptionalAttr(this.props)
|
||||
}
|
||||
|
||||
get value () {
|
||||
if (!this.state.use) {
|
||||
return
|
||||
}
|
||||
|
||||
const obj = {}
|
||||
|
||||
forEach(this.refs, (instance, key) => {
|
||||
obj[key] = instance.value
|
||||
})
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
set value (value = {}) {
|
||||
forEach(this.refs, (instance, id) => {
|
||||
instance.value = value[id]
|
||||
_onChildChange = (value, key) => {
|
||||
this.props.onChange({
|
||||
...this.props.value,
|
||||
[key]: value
|
||||
})
|
||||
}
|
||||
|
||||
_handleOptionalChange = event => {
|
||||
const { checked } = event.target
|
||||
|
||||
this.setState({
|
||||
use: checked
|
||||
})
|
||||
}
|
||||
|
||||
_makeChildren (props) {
|
||||
const {
|
||||
depth = 0,
|
||||
schema,
|
||||
uiSchema = {},
|
||||
defaultValue = {}
|
||||
} = props
|
||||
const obj = {}
|
||||
const { properties } = uiSchema
|
||||
|
||||
forEach(schema.properties, (childSchema, key) => {
|
||||
obj[key] = (
|
||||
<ObjectItem key={key}>
|
||||
<GenericInput
|
||||
depth={depth + 2}
|
||||
disabled={props.disabled}
|
||||
label={childSchema.title || key}
|
||||
required={includes(schema.required, key)}
|
||||
schema={childSchema}
|
||||
uiSchema={properties && properties[key]}
|
||||
defaultValue={defaultValue[key]}
|
||||
/>
|
||||
</ObjectItem>
|
||||
)
|
||||
})
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
if (
|
||||
!propsEqual(
|
||||
this.props,
|
||||
props,
|
||||
[ 'depth', 'disabled', 'label', 'required', 'schema', 'uiSchema' ]
|
||||
)
|
||||
) {
|
||||
this.setState({
|
||||
children: this._makeChildren(props)
|
||||
})
|
||||
}
|
||||
}
|
||||
_getRequiredProps = createSelector(
|
||||
() => this.props.schema.required,
|
||||
required => required
|
||||
? keyBy(required)
|
||||
: EMPTY_OBJECT
|
||||
)
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
const { use } = state
|
||||
const depth = props.depth || 0
|
||||
const {
|
||||
props: {
|
||||
depth = 0,
|
||||
disabled,
|
||||
label,
|
||||
required,
|
||||
schema,
|
||||
uiSchema,
|
||||
value = EMPTY_OBJECT
|
||||
},
|
||||
state: { use }
|
||||
} = this
|
||||
|
||||
const childDepth = depth + 2
|
||||
const properties = uiSchema && uiSchema.properties || EMPTY_OBJECT
|
||||
const requiredProps = this._getRequiredProps()
|
||||
|
||||
return (
|
||||
<div style={{'paddingLeft': `${depth}em`}}>
|
||||
<legend>{props.label}</legend>
|
||||
{descriptionRender(props.schema.description)}
|
||||
<legend>{label}</legend>
|
||||
{descriptionRender(schema.description)}
|
||||
<hr />
|
||||
{!props.required &&
|
||||
<div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={use}
|
||||
disabled={props.disabled}
|
||||
onChange={this._handleOptionalChange}
|
||||
type='checkbox'
|
||||
/> {_('fillOptionalInformations')}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
{use &&
|
||||
<div className='card-block'>
|
||||
{map(state.children, (child, index) =>
|
||||
cloneElement(child, { ref: index })
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
{!required && <div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
checked={use}
|
||||
disabled={disabled}
|
||||
onChange={this.linkState('use')}
|
||||
type='checkbox'
|
||||
/> {_('fillOptionalInformations')}
|
||||
</label>
|
||||
</div>}
|
||||
{use && <div className='card-block'>
|
||||
{map(schema.properties, (childSchema, key) =>
|
||||
<div className='pb-1' key={key}>
|
||||
<GenericInput
|
||||
depth={childDepth}
|
||||
disabled={disabled}
|
||||
label={childSchema.title || key}
|
||||
name={key}
|
||||
onChange={this._onChildChange}
|
||||
required={Boolean(requiredProps[key])}
|
||||
schema={childSchema}
|
||||
uiSchema={properties[key]}
|
||||
value={value[key]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import React from 'react'
|
||||
|
||||
import AbstractInput from './abstract-input'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Combobox from '../combobox'
|
||||
import Component from '../base-component'
|
||||
import propTypes from '../prop-types'
|
||||
|
||||
import { PrimitiveInputWrapper } from './helpers'
|
||||
|
||||
// ===================================================================
|
||||
@@ -10,22 +12,29 @@ import { PrimitiveInputWrapper } from './helpers'
|
||||
@propTypes({
|
||||
password: propTypes.bool
|
||||
})
|
||||
export default class StringInput extends AbstractInput {
|
||||
@uncontrollableInput()
|
||||
export default class StringInput extends Component {
|
||||
render () {
|
||||
const { props } = this
|
||||
const { schema } = props
|
||||
const { required, schema } = this.props
|
||||
const {
|
||||
disabled,
|
||||
onChange,
|
||||
password,
|
||||
placeholder = schema.default,
|
||||
value,
|
||||
...props
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<Combobox
|
||||
defaultValue={props.defaultValue}
|
||||
disabled={props.disabled}
|
||||
onChange={props.onChange}
|
||||
value={value || ''}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
options={schema.defaults}
|
||||
placeholder={props.placeholder || schema.default}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
type={props.password && 'password'}
|
||||
placeholder={placeholder || schema.default}
|
||||
required={required}
|
||||
type={password && 'password'}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -6,6 +6,10 @@ import React, { Component, cloneElement } from 'react'
|
||||
import { Button, Modal as ReactModal } from 'react-bootstrap-4/lib'
|
||||
|
||||
import propTypes from './prop-types'
|
||||
import {
|
||||
disable as disableShortcuts,
|
||||
enable as enableShortcuts
|
||||
} from './shortcuts'
|
||||
|
||||
let instance
|
||||
|
||||
@@ -15,7 +19,7 @@ const modal = (content, onClose) => {
|
||||
} else if (instance.state.showModal) {
|
||||
throw new Error('Other modal still open.')
|
||||
}
|
||||
instance.setState({ content, onClose, showModal: true })
|
||||
instance.setState({ content, onClose, showModal: true }, disableShortcuts)
|
||||
}
|
||||
|
||||
export const alert = (title, body) => {
|
||||
@@ -134,18 +138,22 @@ export default class Modal extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.state = { showModal: false }
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
if (instance) {
|
||||
throw new Error('Modal is a singleton!')
|
||||
}
|
||||
instance = this
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.setState({ showModal: false })
|
||||
componentWillUnmount () {
|
||||
instance = undefined
|
||||
}
|
||||
|
||||
close () {
|
||||
this.setState({ showModal: false })
|
||||
this.setState({ showModal: false }, enableShortcuts)
|
||||
}
|
||||
|
||||
_onHide = () => {
|
||||
|
||||
@@ -8,15 +8,17 @@ export let info
|
||||
export let success
|
||||
|
||||
export class Notification extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
componentDidMount () {
|
||||
if (instance) {
|
||||
throw new Error('Notification is a singleton!')
|
||||
}
|
||||
instance = this
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
instance = undefined
|
||||
}
|
||||
|
||||
// This special component never have to rerender!
|
||||
shouldComponentUpdate () {
|
||||
return false
|
||||
|
||||
7
src/common/react-novnc.js
vendored
7
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 () {
|
||||
@@ -120,6 +123,8 @@ export default class NoVnc extends Component {
|
||||
|
||||
rfb.get_keyboard().grab()
|
||||
rfb.get_mouse().grab()
|
||||
|
||||
disableShortcuts()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +133,8 @@ export default class NoVnc extends Component {
|
||||
if (rfb) {
|
||||
rfb.get_keyboard().ungrab()
|
||||
rfb.get_mouse().ungrab()
|
||||
|
||||
enableShortcuts()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'intl'
|
||||
import React from 'react'
|
||||
|
||||
import Icon from './icon'
|
||||
@@ -164,7 +165,7 @@ const xoItemToRender = {
|
||||
// PIF.
|
||||
PIF: pif => (
|
||||
<span>
|
||||
<Icon icon='network' /> {pif.device}
|
||||
<Icon icon='network' /> {pif.device} ({pif.deviceName})
|
||||
</span>
|
||||
),
|
||||
|
||||
@@ -217,7 +218,7 @@ const GenericXoItem = connectStore(() => {
|
||||
})
|
||||
})(({ xoItem, ...props }) => xoItem
|
||||
? renderXoItem(xoItem, props)
|
||||
: <span className='text-muted'>no such item</span>
|
||||
: <span className='text-muted'>{_('errorNoSuchItem')}</span>
|
||||
)
|
||||
|
||||
export const renderXoItemFromId = (id, props) => <GenericXoItem {...props} id={id} />
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import includes from 'lodash/includes'
|
||||
import join from 'lodash/join'
|
||||
import classNames from 'classnames'
|
||||
import Icon from 'icon'
|
||||
import later from 'later'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import sortedIndex from 'lodash/sortedIndex'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Toggle } from 'form'
|
||||
import { FormattedDate, FormattedTime } from 'react-intl'
|
||||
import {
|
||||
Tab,
|
||||
Tabs
|
||||
} from 'react-bootstrap-4/lib'
|
||||
forEach,
|
||||
includes,
|
||||
isArray,
|
||||
map,
|
||||
sortedIndex
|
||||
} from 'lodash'
|
||||
|
||||
import _ from './intl'
|
||||
import Component from './base-component'
|
||||
@@ -20,13 +23,22 @@ import { Range } from './form'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// By default later use UTC but we use this line for futures versions.
|
||||
// By default, later uses UTC but we use this line for future versions.
|
||||
later.date.UTC()
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const NAV_EACH_SELECTED = 1
|
||||
const NAV_EVERY_N = 2
|
||||
const CLICKABLE = { cursor: 'pointer' }
|
||||
const PREVIEW_SLIDER_STYLE = { width: '400px' }
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const UNITS = [ 'minute', 'hour', 'monthDay', 'month', 'weekDay' ]
|
||||
|
||||
const MINUTES_RANGE = [2, 30]
|
||||
const HOURS_RANGE = [2, 12]
|
||||
const MONTH_DAYS_RANGE = [2, 15]
|
||||
const MONTHS_RANGE = [2, 6]
|
||||
|
||||
const MIN_PREVIEWS = 5
|
||||
const MAX_PREVIEWS = 20
|
||||
@@ -130,24 +142,20 @@ const getDayName = (dayNum) =>
|
||||
cronPattern: propTypes.string.isRequired
|
||||
})
|
||||
export class SchedulePreview extends Component {
|
||||
_handleChange = value => {
|
||||
this.setState({
|
||||
value
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const { cronPattern } = this.props
|
||||
const { value } = this.state
|
||||
|
||||
const cronSched = later.parse.cron(cronPattern)
|
||||
const dates = later.schedule(cronSched).next(this.state.value || MIN_PREVIEWS)
|
||||
const dates = later.schedule(cronSched).next(value)
|
||||
|
||||
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 className='mb-1' style={PREVIEW_SLIDER_STYLE}>
|
||||
<Range min={MIN_PREVIEWS} max={MAX_PREVIEWS} onChange={this.linkState('value')} value={+value} />
|
||||
</div>
|
||||
<ul className='list-group'>
|
||||
{map(dates, (date, id) => (
|
||||
@@ -179,7 +187,7 @@ class ToggleTd extends Component {
|
||||
render () {
|
||||
const { props } = this
|
||||
return (
|
||||
<td style={{ cursor: 'pointer' }} className={props.value ? 'table-success' : ''} onClick={this._onClick}>
|
||||
<td style={CLICKABLE} className={props.value ? 'table-success' : ''} onClick={this._onClick}>
|
||||
{props.children}
|
||||
</td>
|
||||
)
|
||||
@@ -189,14 +197,15 @@ class ToggleTd extends Component {
|
||||
// ===================================================================
|
||||
|
||||
@propTypes({
|
||||
labelId: propTypes.string.isRequired,
|
||||
options: propTypes.array.isRequired,
|
||||
optionsRenderer: propTypes.func,
|
||||
optionRenderer: propTypes.func,
|
||||
onChange: propTypes.func.isRequired,
|
||||
value: propTypes.array.isRequired
|
||||
})
|
||||
class TableSelect extends Component {
|
||||
static defaultProps = {
|
||||
optionsRenderer: value => value
|
||||
optionRenderer: value => value
|
||||
}
|
||||
|
||||
_reset = () => {
|
||||
@@ -226,204 +235,247 @@ class TableSelect extends Component {
|
||||
|
||||
render () {
|
||||
const {
|
||||
labelId,
|
||||
options,
|
||||
optionsRenderer,
|
||||
optionRenderer,
|
||||
value
|
||||
} = this.props
|
||||
const { length } = options[0]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className='table table-bordered table-sm'>
|
||||
<tbody>
|
||||
{map(options, (line, i) => (
|
||||
<tr key={i}>
|
||||
{map(line, (tdOption, j) => {
|
||||
const tdId = length * i + j
|
||||
return (
|
||||
<ToggleTd
|
||||
children={optionsRenderer(tdOption)}
|
||||
tdId={tdId}
|
||||
key={tdId}
|
||||
onChange={this._handleChange}
|
||||
value={includes(value, tdId)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className='btn btn-secondary pull-xs-right' onClick={this._reset}>
|
||||
{_('selectTableReset')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
return <div>
|
||||
<table className='table table-bordered table-sm'>
|
||||
<tbody>
|
||||
{map(options, (line, i) => (
|
||||
<tr key={i}>
|
||||
{map(line, tdOption => (
|
||||
<ToggleTd
|
||||
children={optionRenderer(tdOption)}
|
||||
tdId={tdOption}
|
||||
key={tdOption}
|
||||
onChange={this._handleChange}
|
||||
value={includes(value, tdOption)}
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<button className='btn btn-secondary pull-right' onClick={this._reset}>
|
||||
{_(`selectTableAll${labelId}`)} {value && !value.length && <Icon icon='success' />}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// "2,7" => [2,7] "*/2" => 2 "*" => []
|
||||
const cronToValue = (cron, range) => {
|
||||
if (cron.indexOf('/') === 1) {
|
||||
return +cron.split('/')[1]
|
||||
}
|
||||
|
||||
if (cron === '*') {
|
||||
return []
|
||||
}
|
||||
|
||||
return map(cron.split(','), Number)
|
||||
}
|
||||
|
||||
// [2,7] => "2,7" 2 => "*/2" [] => "*"
|
||||
const valueToCron = value => {
|
||||
if (!isArray(value)) {
|
||||
return `*/${value}`
|
||||
}
|
||||
|
||||
if (!value.length) {
|
||||
return '*'
|
||||
}
|
||||
|
||||
return value.join(',')
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
optionsRenderer: propTypes.func,
|
||||
headerAddon: propTypes.node,
|
||||
optionRenderer: propTypes.func,
|
||||
onChange: propTypes.func.isRequired,
|
||||
range: propTypes.array,
|
||||
labelId: propTypes.string.isRequired,
|
||||
value: propTypes.any.isRequired,
|
||||
valueRenderer: propTypes.func
|
||||
value: propTypes.any.isRequired
|
||||
})
|
||||
class TimePicker extends Component {
|
||||
static defaultProps = {
|
||||
valueRenderer: e => +e
|
||||
}
|
||||
_update = cron => {
|
||||
const { tableValue, rangeValue } = this.state
|
||||
|
||||
constructor () {
|
||||
super()
|
||||
this.state = {
|
||||
activeKey: NAV_EACH_SELECTED,
|
||||
tableValue: []
|
||||
}
|
||||
}
|
||||
const newValue = cronToValue(cron)
|
||||
const periodic = !isArray(newValue)
|
||||
|
||||
_update (props) {
|
||||
const { value, valueRenderer } = props
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this._update(this.props)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
this._update(props)
|
||||
}
|
||||
|
||||
_selectTab = activeKey => {
|
||||
this.setState({
|
||||
activeKey
|
||||
}, () => {
|
||||
const { activeKey, tableValue } = this.state
|
||||
const { onChange } = this.props
|
||||
const { refs } = this
|
||||
|
||||
if (activeKey === NAV_EACH_SELECTED) {
|
||||
onChange(tableValue)
|
||||
} else {
|
||||
onChange(refs.range.value)
|
||||
}
|
||||
periodic,
|
||||
tableValue: periodic ? tableValue : newValue,
|
||||
rangeValue: periodic ? newValue : rangeValue
|
||||
})
|
||||
}
|
||||
|
||||
_handleTableValue = tableValue => {
|
||||
this.setState({
|
||||
tableValue
|
||||
}, () => this.props.onChange(tableValue))
|
||||
componentWillReceiveProps (props) {
|
||||
if (props.value !== this.props.value) {
|
||||
this._update(props.value)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._update(this.props.value)
|
||||
}
|
||||
|
||||
_onChange = value => {
|
||||
this.props.onChange(valueToCron(value))
|
||||
}
|
||||
|
||||
_tableTab = () => this._onChange(this.state.tableValue || [])
|
||||
_periodicTab = () => this._onChange(this.state.rangeValue || this.props.range[0])
|
||||
|
||||
render () {
|
||||
const {
|
||||
onChange,
|
||||
headerAddon,
|
||||
labelId,
|
||||
options,
|
||||
optionsRenderer,
|
||||
range,
|
||||
labelId
|
||||
optionRenderer,
|
||||
range
|
||||
} = this.props
|
||||
const { tableValue } = this.state
|
||||
|
||||
const tableSelect = (
|
||||
<TableSelect
|
||||
onChange={this._handleTableValue}
|
||||
options={options}
|
||||
optionsRenderer={optionsRenderer}
|
||||
value={tableValue}
|
||||
/>
|
||||
const {
|
||||
periodic,
|
||||
tableValue,
|
||||
rangeValue
|
||||
} = this.state
|
||||
|
||||
return <Card>
|
||||
<CardHeader>
|
||||
{_(`scheduling${labelId}`)}
|
||||
{headerAddon}
|
||||
</CardHeader>
|
||||
<CardBlock>
|
||||
{range && <ul className='nav nav-tabs mb-1'>
|
||||
<li className='nav-item'>
|
||||
<a onClick={this._tableTab} className={classNames('nav-link', !periodic && 'active')} style={CLICKABLE}>
|
||||
{_(`schedulingEachSelected${labelId}`)}
|
||||
</a>
|
||||
</li>
|
||||
<li className='nav-item'>
|
||||
<a onClick={this._periodicTab} className={classNames('nav-link', periodic && 'active')} style={CLICKABLE}>
|
||||
{_(`schedulingEveryN${labelId}`)}
|
||||
</a>
|
||||
</li>
|
||||
</ul>}
|
||||
{periodic
|
||||
? <Range ref='range' min={range[0]} max={range[1]} onChange={this._onChange} value={rangeValue} />
|
||||
: <TableSelect
|
||||
labelId={labelId}
|
||||
onChange={this._onChange}
|
||||
options={options}
|
||||
optionRenderer={optionRenderer}
|
||||
value={tableValue || []}
|
||||
/>
|
||||
}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
}
|
||||
}
|
||||
|
||||
const isWeekDayMode = ({ monthDayPattern, weekDayPattern }) => {
|
||||
if (monthDayPattern === '*' && weekDayPattern === '*') {
|
||||
return
|
||||
}
|
||||
|
||||
return weekDayPattern !== '*'
|
||||
}
|
||||
|
||||
@propTypes({
|
||||
monthDayPattern: propTypes.string.isRequired,
|
||||
weekDayPattern: propTypes.string.isRequired
|
||||
})
|
||||
class DayPicker extends Component {
|
||||
state = {
|
||||
weekDayMode: isWeekDayMode(this.props)
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
const weekDayMode = isWeekDayMode(props)
|
||||
|
||||
if (weekDayMode !== undefined) {
|
||||
this.setState({ weekDayMode })
|
||||
}
|
||||
}
|
||||
|
||||
_setWeekDayMode = weekDayMode => {
|
||||
this.props.onChange([ '*', '*' ])
|
||||
this.setState({ weekDayMode })
|
||||
}
|
||||
|
||||
_onChange = cron => {
|
||||
const isMonthDayPattern = !this.state.weekDayMode || includes(cron, '/')
|
||||
|
||||
this.props.onChange([
|
||||
isMonthDayPattern ? cron : '*',
|
||||
isMonthDayPattern ? '*' : cron
|
||||
])
|
||||
}
|
||||
|
||||
render () {
|
||||
const { monthDayPattern, weekDayPattern } = this.props
|
||||
const { weekDayMode } = this.state
|
||||
|
||||
const dayModeToggle = (
|
||||
<Tooltip content={_(weekDayMode ? 'schedulingSetMonthDayMode' : 'schedulingSetWeekDayMode')}>
|
||||
<span className='pull-right'><Toggle onChange={this._setWeekDayMode} iconSize={1} value={weekDayMode} /></span>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
return (
|
||||
<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>
|
||||
</Tabs>
|
||||
) : tableSelect
|
||||
}
|
||||
</CardBlock>
|
||||
</Card>
|
||||
)
|
||||
return <TimePicker
|
||||
headerAddon={dayModeToggle}
|
||||
key={weekDayMode ? 'week' : 'month'}
|
||||
labelId='Day'
|
||||
optionRenderer={weekDayMode ? getDayName : undefined}
|
||||
options={weekDayMode ? WEEK_DAYS : DAYS}
|
||||
onChange={this._onChange}
|
||||
range={MONTH_DAYS_RANGE}
|
||||
setWeekDayMode={this._setWeekDayMode}
|
||||
value={weekDayMode ? weekDayPattern : monthDayPattern}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const HOURS_RANGE = [2, 12]
|
||||
const MINUTES_RANGE = [2, 30]
|
||||
|
||||
const decrement = e => e - 1
|
||||
|
||||
@propTypes({
|
||||
cronPattern: propTypes.string.isRequired,
|
||||
onChange: propTypes.func,
|
||||
timezone: propTypes.string
|
||||
})
|
||||
export default class Scheduler extends Component {
|
||||
_update (type, value) {
|
||||
if (Array.isArray(value)) {
|
||||
if (!value.length) {
|
||||
value = '*'
|
||||
} else {
|
||||
value = join(
|
||||
(type === 'monthDay' || type === 'month')
|
||||
? map(value, n => n + 1)
|
||||
: value,
|
||||
','
|
||||
)
|
||||
}
|
||||
} else {
|
||||
value = `*/${value}`
|
||||
constructor (props) {
|
||||
super(props)
|
||||
|
||||
this._onCronChange = newCrons => {
|
||||
const cronPattern = this.props.cronPattern.split(' ')
|
||||
forEach(newCrons, (cron, unit) => {
|
||||
cronPattern[PICKTIME_TO_ID[unit]] = cron
|
||||
})
|
||||
|
||||
this.props.onChange({
|
||||
cronPattern: cronPattern.join(' '),
|
||||
timezone: this.props.timezone
|
||||
})
|
||||
}
|
||||
|
||||
const { props } = this
|
||||
const cronPattern = props.cronPattern.split(' ')
|
||||
cronPattern[PICKTIME_TO_ID[type]] = value
|
||||
|
||||
this.props.onChange({
|
||||
cronPattern: cronPattern.join(' '),
|
||||
timezone: props.timezone
|
||||
forEach(UNITS, unit => {
|
||||
this[`_${unit}Change`] = cron => this._onCronChange({ [unit]: cron })
|
||||
})
|
||||
this._dayChange = ([ monthDay, weekDay ]) => this._onCronChange({ monthDay, weekDay })
|
||||
}
|
||||
|
||||
_onHourChange = value => this._update('hour', value)
|
||||
_onMinuteChange = value => this._update('minute', value)
|
||||
_onMonthChange = value => this._update('month', value)
|
||||
_onMonthDayChange = value => this._update('monthDay', value)
|
||||
_onWeekDayChange = value => this._update('weekDay', value)
|
||||
|
||||
_onTimezoneChange = timezone => {
|
||||
const { props } = this
|
||||
props.onChange({
|
||||
cronPattern: props.cronPattern,
|
||||
this.props.onChange({
|
||||
cronPattern: this.props.cronPattern,
|
||||
timezone
|
||||
})
|
||||
}
|
||||
@@ -441,25 +493,16 @@ export default class Scheduler extends Component {
|
||||
<Col mediumSize={6}>
|
||||
<TimePicker
|
||||
labelId='Month'
|
||||
optionsRenderer={getMonthName}
|
||||
optionRenderer={getMonthName}
|
||||
options={MONTHS}
|
||||
onChange={this._onMonthChange}
|
||||
onChange={this._monthChange}
|
||||
range={MONTHS_RANGE}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['month']]}
|
||||
valueRenderer={decrement}
|
||||
/>
|
||||
<TimePicker
|
||||
labelId='MonthDay'
|
||||
options={DAYS}
|
||||
onChange={this._onMonthDayChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
|
||||
valueRenderer={decrement}
|
||||
/>
|
||||
<TimePicker
|
||||
labelId='WeekDay'
|
||||
optionsRenderer={getDayName}
|
||||
options={WEEK_DAYS}
|
||||
onChange={this._onWeekDayChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['weekDay']]}
|
||||
<DayPicker
|
||||
onChange={this._dayChange}
|
||||
monthDayPattern={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
|
||||
weekDayPattern={cronPatternArr[PICKTIME_TO_ID['weekDay']]}
|
||||
/>
|
||||
</Col>
|
||||
<Col mediumSize={6}>
|
||||
@@ -467,14 +510,14 @@ export default class Scheduler extends Component {
|
||||
labelId='Hour'
|
||||
options={HOURS}
|
||||
range={HOURS_RANGE}
|
||||
onChange={this._onHourChange}
|
||||
onChange={this._hourChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['hour']]}
|
||||
/>
|
||||
<TimePicker
|
||||
labelId='Minute'
|
||||
options={MINS}
|
||||
range={MINUTES_RANGE}
|
||||
onChange={this._onMinuteChange}
|
||||
onChange={this._minuteChange}
|
||||
value={cronPatternArr[PICKTIME_TO_ID['minute']]}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
32
src/common/select-files.js
Normal file
32
src/common/select-files.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import Icon from 'icon'
|
||||
import propTypes from 'prop-types'
|
||||
import React from 'react'
|
||||
import { omit } from 'lodash'
|
||||
|
||||
@propTypes({
|
||||
multi: propTypes.bool,
|
||||
label: propTypes.node,
|
||||
onChange: propTypes.func.isRequired
|
||||
})
|
||||
export default class SelectFiles extends Component {
|
||||
_onChange = e => {
|
||||
const { multi, onChange } = this.props
|
||||
const { files } = e.target
|
||||
|
||||
onChange(multi ? files : files[0])
|
||||
}
|
||||
|
||||
render () {
|
||||
return <label className='btn btn-secondary btn-file hidden'>
|
||||
<Icon icon='file' /> {this.props.label || _('browseFiles')}
|
||||
<input
|
||||
{...omit(this.props, [ 'hidden', 'label', 'onChange', 'multi' ])}
|
||||
hidden
|
||||
onChange={this._onChange}
|
||||
type='file'
|
||||
/>
|
||||
</label>
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,38 @@
|
||||
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 Icon from 'icon'
|
||||
import store from 'store'
|
||||
import Tooltip from 'tooltip'
|
||||
import { Button } from 'react-bootstrap-4/lib'
|
||||
import { parse as parseRemote } from 'xo-remote-parser'
|
||||
import {
|
||||
assign,
|
||||
filter,
|
||||
flatten,
|
||||
forEach,
|
||||
groupBy,
|
||||
includes,
|
||||
isArray,
|
||||
isEmpty,
|
||||
isInteger,
|
||||
isString,
|
||||
keyBy,
|
||||
keys,
|
||||
map,
|
||||
mapValues,
|
||||
pick,
|
||||
sortBy,
|
||||
toArray
|
||||
} from 'lodash'
|
||||
|
||||
import _ from './intl'
|
||||
import uncontrollableInput from 'uncontrollable-input'
|
||||
import Component from './base-component'
|
||||
import propTypes from './prop-types'
|
||||
import renderXoItem from './render-xo-item'
|
||||
import { Select } from './form'
|
||||
import {
|
||||
createCollectionWrapper,
|
||||
createFilter,
|
||||
createGetObjectsOfType,
|
||||
createGetTags,
|
||||
@@ -28,6 +40,7 @@ import {
|
||||
getObject
|
||||
} from './selectors'
|
||||
import {
|
||||
addSubscriptions,
|
||||
connectStore,
|
||||
mapPlus,
|
||||
resolveResourceSets
|
||||
@@ -45,6 +58,26 @@ import {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// react-select's line-height is 1.4
|
||||
// https://github.com/JedWatson/react-select/blob/916ab0e62fc7394be8e24f22251c399a68de8b1c/less/multi.less#L33
|
||||
// while bootstrap button's line-height is 1.25
|
||||
// https://github.com/twbs/bootstrap/blob/959c4e527c6ef69623928db638267ba1c370479d/scss/_variables.scss#L342
|
||||
const ADDON_BUTTON_STYLE = { lineHeight: '1.4' }
|
||||
|
||||
const getIds = value => value == null || isString(value) || isInteger(value)
|
||||
? value
|
||||
: isArray(value)
|
||||
? map(value, getIds)
|
||||
: value.id
|
||||
|
||||
const getOption = (object, container) => ({
|
||||
label: container
|
||||
? `${getLabel(object)} ${getLabel(container)}`
|
||||
: getLabel(object),
|
||||
value: object.id,
|
||||
xoItem: object
|
||||
})
|
||||
|
||||
const getLabel = object =>
|
||||
object.name_label ||
|
||||
object.name ||
|
||||
@@ -53,6 +86,10 @@ const getLabel = object =>
|
||||
object.value ||
|
||||
object.label
|
||||
|
||||
const options = props => ({
|
||||
defaultValue: props.multi ? [] : undefined
|
||||
})
|
||||
|
||||
// ===================================================================
|
||||
|
||||
/*
|
||||
@@ -84,12 +121,11 @@ const getLabel = object =>
|
||||
@propTypes({
|
||||
autoFocus: propTypes.bool,
|
||||
clearable: propTypes.bool,
|
||||
defaultValue: propTypes.any,
|
||||
disabled: propTypes.bool,
|
||||
hasSelectAll: propTypes.bool,
|
||||
multi: propTypes.bool,
|
||||
onChange: propTypes.func,
|
||||
placeholder: propTypes.any.isRequired,
|
||||
predicate: propTypes.func,
|
||||
required: propTypes.bool,
|
||||
value: propTypes.any,
|
||||
xoContainers: propTypes.array,
|
||||
@@ -99,202 +135,179 @@ const getLabel = object =>
|
||||
]).isRequired
|
||||
})
|
||||
export class GenericSelect extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
value: this._setValue(props.value || props.defaultValue, props)
|
||||
componentDidUpdate (prevProps) {
|
||||
const { onChange, xoObjects } = this.props
|
||||
|
||||
if (!onChange || prevProps.xoObjects === xoObjects) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_getValue (xoObjectsById = this.state.xoObjectsById, props = this.props) {
|
||||
const { value } = this.state
|
||||
const ids = this._getSelectValue()
|
||||
const objectsById = this._getObjectsById()
|
||||
|
||||
if (props.multi) {
|
||||
// Returns the values of the selected objects
|
||||
// if they are contained in xoObjectsById.
|
||||
return mapPlus(value, (value, push) => {
|
||||
const o = xoObjectsById[value.value !== undefined ? value.value : value]
|
||||
if (!isArray(ids)) {
|
||||
ids && !objectsById[ids] && onChange(undefined)
|
||||
} else {
|
||||
let shouldTriggerOnChange
|
||||
|
||||
if (o) {
|
||||
push(o)
|
||||
const newValue = isArray(ids) && mapPlus(ids, (id, push) => {
|
||||
const object = objectsById[id]
|
||||
|
||||
if (object) {
|
||||
push(object)
|
||||
} else {
|
||||
shouldTriggerOnChange = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return xoObjectsById[value.value || value] || ''
|
||||
}
|
||||
|
||||
// Supports id strings and objects.
|
||||
_setValue (value, props = this.props) {
|
||||
if (props.multi) {
|
||||
return map(value, object => object.id !== undefined ? object.id : object)
|
||||
}
|
||||
|
||||
return (value != null)
|
||||
? value.id !== undefined ? value.id : value
|
||||
: ''
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
const { props } = this
|
||||
|
||||
this.setState({
|
||||
...this._computeOptions(props)
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps (newProps) {
|
||||
const { props } = this
|
||||
const { value, xoContainers, xoObjects } = newProps
|
||||
|
||||
if (
|
||||
xoContainers !== props.xoContainers ||
|
||||
xoObjects !== props.xoObjects
|
||||
) {
|
||||
const {
|
||||
options,
|
||||
xoObjectsById
|
||||
} = this._computeOptions(newProps)
|
||||
|
||||
const value = this._getValue(xoObjectsById, newProps)
|
||||
|
||||
this.setState({
|
||||
options,
|
||||
value: this._setValue(value, newProps),
|
||||
xoObjectsById
|
||||
})
|
||||
}
|
||||
|
||||
if (value !== props.value) {
|
||||
this.setState({
|
||||
value: this._setValue(value, newProps)
|
||||
})
|
||||
if (shouldTriggerOnChange) {
|
||||
this.props.onChange(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_computeOptions ({ xoContainers, xoObjects }) {
|
||||
if (!xoContainers) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!Array.isArray(xoObjects)) {
|
||||
throw new Error('without xoContainers, xoObjects must be an array')
|
||||
_getObjectsById = createSelector(
|
||||
() => this.props.xoObjects,
|
||||
objects => keyBy(
|
||||
isArray(objects)
|
||||
? objects
|
||||
: flatten(toArray(objects)),
|
||||
'id'
|
||||
)
|
||||
)
|
||||
|
||||
_getOptions = createSelector(
|
||||
() => this.props.xoContainers,
|
||||
() => this.props.xoObjects,
|
||||
(containers, objects) => { // createCollectionWrapper with a depth?
|
||||
const { name } = this.constructor
|
||||
|
||||
if (!containers) {
|
||||
if (__DEV__ && !isArray(objects)) {
|
||||
throw new Error(`${name}: without xoContainers, xoObjects must be an array`)
|
||||
}
|
||||
|
||||
return map(objects, getOption)
|
||||
}
|
||||
|
||||
return {
|
||||
xoObjectsById: keyBy(xoObjects, 'id'),
|
||||
options: map(xoObjects, object => ({
|
||||
label: getLabel(object),
|
||||
value: object.id,
|
||||
xoItem: object
|
||||
}))
|
||||
if (__DEV__ && isArray(objects)) {
|
||||
throw new Error(`${name}: with xoContainers, xoObjects must be an object`)
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (Array.isArray(xoObjects)) {
|
||||
throw new Error('with xoContainers, xoObjects must be an object')
|
||||
}
|
||||
}
|
||||
const options = []
|
||||
forEach(containers, container => {
|
||||
options.push({
|
||||
disabled: true,
|
||||
xoItem: container
|
||||
})
|
||||
|
||||
const options = []
|
||||
const xoObjectsById = {}
|
||||
|
||||
forEach(xoContainers, container => {
|
||||
const containerObjects = keyBy(xoObjects[container.id], 'id')
|
||||
assign(xoObjectsById, containerObjects)
|
||||
|
||||
options.push({
|
||||
disabled: true,
|
||||
xoItem: container
|
||||
forEach(objects[container.id], object => {
|
||||
options.push(getOption(object, container))
|
||||
})
|
||||
})
|
||||
return options
|
||||
}
|
||||
)
|
||||
|
||||
options.push.apply(options, map(containerObjects, object => ({
|
||||
label: `${getLabel(object)} ${getLabel(container)}`,
|
||||
value: object.id,
|
||||
xoItem: object
|
||||
})))
|
||||
})
|
||||
_getSelectValue = createSelector(
|
||||
() => this.props.value,
|
||||
createCollectionWrapper(getIds)
|
||||
)
|
||||
|
||||
return { xoObjectsById, options }
|
||||
}
|
||||
_getNewSelectedObjects = createSelector(
|
||||
this._getObjectsById,
|
||||
value => value,
|
||||
(objectsById, value) => value == null
|
||||
? value
|
||||
: isArray(value)
|
||||
? map(value, value => objectsById[value.value])
|
||||
: objectsById[value.value]
|
||||
)
|
||||
|
||||
get value () {
|
||||
return this._getValue()
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.setState({
|
||||
value: this._setValue(value)
|
||||
})
|
||||
}
|
||||
|
||||
_handleChange = value => {
|
||||
_onChange = value => {
|
||||
const { onChange } = this.props
|
||||
if (onChange) {
|
||||
onChange(this._getNewSelectedObjects(value))
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
value: this._setValue(value)
|
||||
}, onChange && (() => onChange(this.value)))
|
||||
_selectAll = () => {
|
||||
this._onChange(
|
||||
filter(this._getOptions(), ({ disabled }) => !disabled)
|
||||
)
|
||||
}
|
||||
|
||||
// GroupBy: Display option with margin if not disabled and containers exists.
|
||||
_renderOption = option => (
|
||||
_renderOption = option =>
|
||||
<span
|
||||
className={classNames(
|
||||
!option.disabled && this.props.xoContainers && 'm-l-1'
|
||||
!option.disabled && this.props.xoContainers && 'ml-1'
|
||||
)}
|
||||
>
|
||||
{renderXoItem(option.xoItem)}
|
||||
</span>
|
||||
)
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
const {
|
||||
autoFocus,
|
||||
disabled,
|
||||
hasSelectAll,
|
||||
multi,
|
||||
placeholder,
|
||||
required,
|
||||
|
||||
return (
|
||||
<Select
|
||||
autofocus={props.autoFocus}
|
||||
clearable={props.clearable}
|
||||
disabled={props.disabled}
|
||||
multi={props.multi}
|
||||
onChange={this._handleChange}
|
||||
openOnFocus
|
||||
optionRenderer={this._renderOption}
|
||||
options={state.options}
|
||||
placeholder={props.placeholder}
|
||||
required={props.required}
|
||||
value={state.value}
|
||||
valueRenderer={this._renderOption}
|
||||
/>
|
||||
)
|
||||
clearable = Boolean(multi || !required)
|
||||
} = this.props
|
||||
|
||||
const select = <Select
|
||||
{...{
|
||||
autofocus: autoFocus,
|
||||
clearable,
|
||||
disabled,
|
||||
multi,
|
||||
placeholder,
|
||||
required
|
||||
}}
|
||||
|
||||
onChange={this._onChange}
|
||||
openOnFocus
|
||||
optionRenderer={this._renderOption}
|
||||
options={this._getOptions()}
|
||||
value={this._getSelectValue()}
|
||||
valueRenderer={this._renderOption}
|
||||
/>
|
||||
|
||||
if (!multi || !hasSelectAll) {
|
||||
return select
|
||||
}
|
||||
|
||||
// `hasSelectAll` should be provided by react-select after this pull request has been merged:
|
||||
// https://github.com/JedWatson/react-select/pull/748
|
||||
// TODO: remove once it has been merged upstream.
|
||||
return <div className='input-group'>
|
||||
{select}
|
||||
<span className='input-group-btn'>
|
||||
<Tooltip content={_('selectAll')}>
|
||||
<Button type='button' bsStyle='secondary' onClick={this._selectAll} style={ADDON_BUTTON_STYLE}>
|
||||
<Icon icon='add' />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const makeStoreSelect = (createSelectors, props) => connectStore(
|
||||
createSelectors,
|
||||
{ withRef: true }
|
||||
)(
|
||||
class extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
{...props}
|
||||
{...this.props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
const makeStoreSelect = (createSelectors, defaultProps) => uncontrollableInput(options)(
|
||||
connectStore(createSelectors)(
|
||||
props =>
|
||||
<GenericSelect
|
||||
{...defaultProps}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
|
||||
const makeSubscriptionSelect = (subscribe, props) => (
|
||||
const makeSubscriptionSelect = (subscribe, props) => uncontrollableInput(options)(
|
||||
class extends Component {
|
||||
constructor (props) {
|
||||
super(props)
|
||||
@@ -323,14 +336,6 @@ const makeSubscriptionSelect = (subscribe, props) => (
|
||||
)
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribe(::this.setState)
|
||||
}
|
||||
@@ -338,7 +343,6 @@ const makeSubscriptionSelect = (subscribe, props) => (
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
{...props}
|
||||
{...this.props}
|
||||
xoObjects={this._getFilteredXoObjects()}
|
||||
@@ -519,11 +523,11 @@ export const SelectTag = makeStoreSelect((_, props) => ({
|
||||
}), { placeholder: _('selectTags') })
|
||||
|
||||
export const SelectHighLevelObject = makeStoreSelect(() => {
|
||||
const getHosts = createGetObjectsOfType('host')
|
||||
const getNetworks = createGetObjectsOfType('network')
|
||||
const getPools = createGetObjectsOfType('pool')
|
||||
const getSrs = createGetObjectsOfType('SR')
|
||||
const getVms = createGetObjectsOfType('VM')
|
||||
const getHosts = createGetObjectsOfType('host').filter(getPredicate)
|
||||
const getNetworks = createGetObjectsOfType('network').filter(getPredicate)
|
||||
const getPools = createGetObjectsOfType('pool').filter(getPredicate)
|
||||
const getSrs = createGetObjectsOfType('SR').filter(getPredicate)
|
||||
const getVms = createGetObjectsOfType('VM').filter(getPredicate)
|
||||
|
||||
const getHighLevelObjects = createSelector(
|
||||
getHosts,
|
||||
@@ -655,14 +659,6 @@ export class SelectResourceSetsVmTemplate extends Component {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||
this.setState({
|
||||
resourceSets: resolveResourceSets(resourceSets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getTemplates = createSelector(
|
||||
() => this.props.resourceSet,
|
||||
({ objectsByType }) => {
|
||||
@@ -694,15 +690,6 @@ export class SelectResourceSetsSr extends Component {
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||
this.setState({
|
||||
resourceSets: resolveResourceSets(resourceSets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getSrs = createSelector(
|
||||
() => this.props.resourceSet,
|
||||
({ objectsByType }) => {
|
||||
@@ -735,14 +722,6 @@ export class SelectResourceSetsVdi extends Component {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||
this.setState({
|
||||
resourceSets: resolveResourceSets(resourceSets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getObject (id) {
|
||||
return getObject(store.getState(), id, true)
|
||||
}
|
||||
@@ -784,14 +763,6 @@ export class SelectResourceSetsNetwork extends Component {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this.componentWillUnmount = subscribeResourceSets(resourceSets => {
|
||||
this.setState({
|
||||
resourceSets: resolveResourceSets(resourceSets)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
_getNetworks = createSelector(
|
||||
() => this.props.resourceSet,
|
||||
({ objectsByType }) => {
|
||||
@@ -815,6 +786,78 @@ export class SelectResourceSetsNetwork extends Component {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// Pass a function to @addSubscriptions to ensure subscribeIpPools and subscribeResourceSets
|
||||
// are correctly imported before they are called
|
||||
@addSubscriptions(() => ({
|
||||
ipPools: subscribeIpPools,
|
||||
resourceSets: subscribeResourceSets
|
||||
}))
|
||||
@propTypes({
|
||||
containerPredicate: propTypes.func,
|
||||
predicate: propTypes.func,
|
||||
resourceSetId: propTypes.string.isRequired
|
||||
})
|
||||
export class SelectResourceSetIp extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.select.value = value
|
||||
}
|
||||
|
||||
_getResourceSetIpPools = createSelector(
|
||||
() => this.props.ipPools,
|
||||
() => this.props.resourceSets,
|
||||
() => this.props.resourceSetId,
|
||||
(allIpPools, allResourceSets, resourceSetId) => {
|
||||
const { ipPools } = allResourceSets[resourceSetId]
|
||||
return filter(allIpPools, ({ id }) => includes(ipPools, id))
|
||||
}
|
||||
)
|
||||
|
||||
_getIpPools = createSelector(
|
||||
() => this.props.ipPools,
|
||||
() => this.props.containerPredicate,
|
||||
(ipPools, predicate) => predicate
|
||||
? filter(ipPools, predicate)
|
||||
: ipPools
|
||||
)
|
||||
|
||||
_getIps = createSelector(
|
||||
this._getIpPools,
|
||||
() => this.props.predicate,
|
||||
() => this.props.ipPools,
|
||||
(ipPools, predicate, resolvedIpPools) => {
|
||||
return flatten(
|
||||
map(ipPools, ipPool => {
|
||||
const poolIps = map(ipPool.addresses, (address, ip) => ({
|
||||
...address,
|
||||
id: ip,
|
||||
label: ip,
|
||||
type: 'ipAddress',
|
||||
used: !isEmpty(address.vifs)
|
||||
}))
|
||||
return predicate ? filter(poolIps, predicate) : poolIps
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
render () {
|
||||
return (
|
||||
<GenericSelect
|
||||
ref='select'
|
||||
placeholder={_('selectIpPool')}
|
||||
{...this.props}
|
||||
xoObjects={this._getIps()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export class SelectSshKey extends Component {
|
||||
get value () {
|
||||
return this.refs.select.value
|
||||
@@ -872,3 +915,15 @@ export const SelectIp = makeSubscriptionSelect(subscriber => {
|
||||
|
||||
return unsubscribeIpPools
|
||||
}, { placeholder: _('selectIp') })
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const SelectIpPool = makeSubscriptionSelect(subscriber => {
|
||||
const unsubscribeIpPools = subscribeIpPools(ipPools => {
|
||||
subscriber({
|
||||
xoObjects: map(sortBy(ipPools, 'name'), ipPool => ({ ...ipPool, type: 'ipPool' }))
|
||||
})
|
||||
})
|
||||
|
||||
return unsubscribeIpPools
|
||||
}, { placeholder: _('selectIpPool') })
|
||||
|
||||
@@ -37,12 +37,16 @@ export {
|
||||
// Use case: in connect, to avoid rerendering a component where the
|
||||
// objects are still the same.
|
||||
const _createCollectionWrapper = selector => {
|
||||
let cache
|
||||
let cache, previous
|
||||
|
||||
return (...args) => {
|
||||
const value = selector(...args)
|
||||
if (!shallowEqual(value, cache)) {
|
||||
cache = value
|
||||
if (value !== previous) {
|
||||
previous = value
|
||||
|
||||
if (!shallowEqual(value, cache)) {
|
||||
cache = value
|
||||
}
|
||||
}
|
||||
return cache
|
||||
}
|
||||
@@ -142,10 +146,10 @@ export const createPicker = (object, props) =>
|
||||
// - predicate == null → no filtering
|
||||
// - predicate === false → everything is filtered out
|
||||
export const createFilter = (collection, predicate) =>
|
||||
_createCollectionWrapper(
|
||||
_create2(
|
||||
collection,
|
||||
predicate,
|
||||
_create2(
|
||||
collection,
|
||||
predicate,
|
||||
_createCollectionWrapper(
|
||||
(collection, predicate) => predicate === false
|
||||
? (isArrayLike(collection) ? EMPTY_ARRAY : EMPTY_OBJECT)
|
||||
: predicate
|
||||
@@ -168,17 +172,18 @@ export const createGroupBy = (collection, getter) =>
|
||||
groupBy
|
||||
)
|
||||
|
||||
export const createPager = (array, page, n = 25) => _createCollectionWrapper(
|
||||
export const createPager = (array, page, n = 25) =>
|
||||
_create2(
|
||||
array,
|
||||
page,
|
||||
n,
|
||||
(array, page, n) => {
|
||||
const start = (page - 1) * n
|
||||
return slice(array, start, start + n)
|
||||
}
|
||||
_createCollectionWrapper(
|
||||
(array, page, n) => {
|
||||
const start = (page - 1) * n
|
||||
return slice(array, start, start + n)
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
export const createSort = (
|
||||
collection,
|
||||
@@ -187,11 +192,11 @@ export const createSort = (
|
||||
) => _create2(collection, getter, order, orderBy)
|
||||
|
||||
export const createTop = (collection, iteratee, n) =>
|
||||
_createCollectionWrapper(
|
||||
_create2(
|
||||
collection,
|
||||
iteratee,
|
||||
n,
|
||||
_create2(
|
||||
collection,
|
||||
iteratee,
|
||||
n,
|
||||
_createCollectionWrapper(
|
||||
(objects, iteratee, n) => {
|
||||
let results = orderBy(objects, iteratee, 'desc')
|
||||
if (n < results.length) {
|
||||
@@ -217,6 +222,36 @@ export const getStatus = state => state.status
|
||||
|
||||
export const getUser = state => state.user
|
||||
|
||||
export const getCheckPermissions = invoke(() => {
|
||||
const getPredicate = create(
|
||||
state => state.permissions,
|
||||
state => state.objects,
|
||||
(permissions, objects) => {
|
||||
objects = objects.all
|
||||
const getObject = id => (objects[id] || EMPTY_OBJECT)
|
||||
|
||||
return (id, permission) => checkPermissions(permissions, getObject, id, permission)
|
||||
}
|
||||
)
|
||||
|
||||
const isTrue = () => true
|
||||
const isFalse = () => false
|
||||
|
||||
return state => {
|
||||
const user = getUser(state)
|
||||
|
||||
if (!user) {
|
||||
return isFalse
|
||||
}
|
||||
|
||||
if (user.permission === 'admin') {
|
||||
return isTrue
|
||||
}
|
||||
|
||||
return getPredicate(state)
|
||||
}
|
||||
})
|
||||
|
||||
const _getPermissionsPredicate = invoke(() => {
|
||||
const getPredicate = create(
|
||||
state => state.permissions,
|
||||
@@ -453,23 +488,24 @@ export const createDoesHostNeedRestart = hostSelector => {
|
||||
return (state, props) => restartPoolPatch(state, props) !== undefined
|
||||
}
|
||||
|
||||
export const createGetHostMetrics = hostSelector => _createCollectionWrapper(
|
||||
export const createGetHostMetrics = hostSelector =>
|
||||
create(
|
||||
hostSelector,
|
||||
hosts => {
|
||||
const metrics = {
|
||||
count: 0,
|
||||
cpus: 0,
|
||||
memoryTotal: 0,
|
||||
memoryUsage: 0
|
||||
_createCollectionWrapper(
|
||||
hosts => {
|
||||
const metrics = {
|
||||
count: 0,
|
||||
cpus: 0,
|
||||
memoryTotal: 0,
|
||||
memoryUsage: 0
|
||||
}
|
||||
forEach(hosts, host => {
|
||||
metrics.count++
|
||||
metrics.cpus += host.cpus.cores
|
||||
metrics.memoryTotal += host.memory.size
|
||||
metrics.memoryUsage += host.memory.usage
|
||||
})
|
||||
return metrics
|
||||
}
|
||||
forEach(hosts, host => {
|
||||
metrics.count++
|
||||
metrics.cpus += host.cpus.cores
|
||||
metrics.memoryTotal += host.memory.size
|
||||
metrics.memoryUsage += host.memory.usage
|
||||
})
|
||||
return metrics
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import kindOf from 'kindof'
|
||||
|
||||
// Tests that two collections (arrays or objects) have strictly equals
|
||||
// values (items or properties)
|
||||
const shallowEqual = (c1, c2) => {
|
||||
@@ -5,8 +7,8 @@ const shallowEqual = (c1, c2) => {
|
||||
return true
|
||||
}
|
||||
|
||||
const type = typeof c1
|
||||
if (type !== typeof c2) {
|
||||
const type = kindOf(c1)
|
||||
if (type !== kindOf(c2)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -25,6 +27,10 @@ const shallowEqual = (c1, c2) => {
|
||||
return true
|
||||
}
|
||||
|
||||
if (type !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
let n = 0
|
||||
for (const _ in c2) { // eslint-disable-line no-unused-vars
|
||||
++n
|
||||
|
||||
35
src/common/shortcuts.js
Normal file
35
src/common/shortcuts.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import Component from 'base-component'
|
||||
import forEach from 'lodash/forEach'
|
||||
import React from 'react'
|
||||
import remove from 'lodash/remove'
|
||||
import { Shortcuts as ReactShortcuts } from 'react-shortcuts'
|
||||
|
||||
let enabled = true
|
||||
const instances = []
|
||||
|
||||
const updateInstances = () => {
|
||||
forEach(instances, instance => instance.forceUpdate())
|
||||
}
|
||||
|
||||
export const enable = () => {
|
||||
enabled = true
|
||||
updateInstances()
|
||||
}
|
||||
|
||||
export const disable = () => {
|
||||
enabled = false
|
||||
updateInstances()
|
||||
}
|
||||
|
||||
export default class Shortcuts extends Component {
|
||||
componentDidMount () {
|
||||
instances.push(this)
|
||||
}
|
||||
componentWillUnmount () {
|
||||
remove(instances, this)
|
||||
}
|
||||
|
||||
render () {
|
||||
return enabled ? <ReactShortcuts {...this.props} /> : null
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
.clickableColumn {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clickableColumn:hover {
|
||||
color: #fff;
|
||||
background-color: #96b8d1;
|
||||
color: #fff;
|
||||
background-color: #96b8d1;
|
||||
}
|
||||
|
||||
.clickableRow {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import _ from 'intl'
|
||||
import ceil from 'lodash/ceil'
|
||||
import classNames from 'classnames'
|
||||
import debounce from 'lodash/debounce'
|
||||
import findIndex from 'lodash/findIndex'
|
||||
import isEmpty from 'lodash/isEmpty'
|
||||
@@ -92,7 +93,7 @@ class TableFilter extends Component {
|
||||
|
||||
@propTypes({
|
||||
columnId: propTypes.number.isRequired,
|
||||
name: propTypes.any.isRequired,
|
||||
name: propTypes.node,
|
||||
sort: propTypes.func,
|
||||
sortIcon: propTypes.string
|
||||
})
|
||||
@@ -103,25 +104,25 @@ class ColumnHead extends Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { name, sortIcon } = this.props
|
||||
const { name, sortIcon, textAlign } = this.props
|
||||
|
||||
if (!this.props.sort) {
|
||||
return <th>{name}</th>
|
||||
return <th className={textAlign && `text-xs-${textAlign}`}>{name}</th>
|
||||
}
|
||||
|
||||
let className = styles.clickableColumn
|
||||
|
||||
if (sortIcon === 'asc' || sortIcon === 'desc') {
|
||||
className += ' bg-info'
|
||||
}
|
||||
const isSelected = sortIcon === 'asc' || sortIcon === 'desc'
|
||||
|
||||
return (
|
||||
<th
|
||||
className={className}
|
||||
className={classNames(
|
||||
textAlign && `text-xs-${textAlign}`,
|
||||
styles.clickableColumn,
|
||||
isSelected && classNames('text-white', 'bg-info')
|
||||
)}
|
||||
onClick={this._sort}
|
||||
>
|
||||
{name}
|
||||
<span className='pull-xs-right'>
|
||||
<span className='pull-right'>
|
||||
<Icon icon={sortIcon} />
|
||||
</span>
|
||||
</th>
|
||||
@@ -141,7 +142,7 @@ const DEFAULT_ITEMS_PER_PAGE = 10
|
||||
]).isRequired,
|
||||
columns: propTypes.arrayOf(propTypes.shape({
|
||||
default: propTypes.bool,
|
||||
name: propTypes.node.isRequired,
|
||||
name: propTypes.node,
|
||||
itemRenderer: propTypes.func.isRequired,
|
||||
sortCriteria: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
@@ -153,6 +154,7 @@ const DEFAULT_ITEMS_PER_PAGE = 10
|
||||
filters: propTypes.object,
|
||||
itemsPerPage: propTypes.number,
|
||||
paginationContainer: propTypes.func,
|
||||
rowAction: propTypes.func,
|
||||
rowLink: propTypes.oneOfType([
|
||||
propTypes.func,
|
||||
propTypes.string
|
||||
@@ -263,6 +265,7 @@ export default class SortedTable extends Component {
|
||||
paginationContainer,
|
||||
filterContainer,
|
||||
filters,
|
||||
rowAction,
|
||||
rowLink,
|
||||
userData
|
||||
} = props
|
||||
@@ -300,7 +303,9 @@ export default class SortedTable extends Component {
|
||||
<tr>
|
||||
{map(props.columns, (column, key) => (
|
||||
<ColumnHead
|
||||
textAlign={column.textAlign}
|
||||
columnId={key}
|
||||
|
||||
key={key}
|
||||
name={column.name}
|
||||
sort={column.sortCriteria && this._sort}
|
||||
@@ -312,7 +317,7 @@ export default class SortedTable extends Component {
|
||||
<tbody>
|
||||
{map(this._getVisibleItems(), (item, i) => {
|
||||
const columns = map(props.columns, (column, key) => (
|
||||
<td key={key}>
|
||||
<td key={key} className={column.textAlign && `text-xs-${column.textAlign}`}>
|
||||
{column.itemRenderer(item, userData)}
|
||||
</td>
|
||||
))
|
||||
@@ -325,7 +330,13 @@ export default class SortedTable extends Component {
|
||||
tagName='tr'
|
||||
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
|
||||
>{columns}</BlockLink>
|
||||
: <tr key={id}>{columns}</tr>
|
||||
: <tr
|
||||
className={rowAction && styles.clickableRow}
|
||||
key={id}
|
||||
onClick={rowAction && (() => rowAction(item, userData))}
|
||||
>
|
||||
{columns}
|
||||
</tr>
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -335,19 +346,19 @@ export default class SortedTable extends Component {
|
||||
<Col mediumSize={8}>
|
||||
{paginationContainer
|
||||
? (
|
||||
// Rebuild container function to refresh Portal component.
|
||||
<Portal container={() => paginationContainer()}>
|
||||
{paginationInstance}
|
||||
</Portal>
|
||||
// Rebuild container function to refresh Portal component.
|
||||
<Portal container={() => paginationContainer()}>
|
||||
{paginationInstance}
|
||||
</Portal>
|
||||
) : paginationInstance
|
||||
}
|
||||
</Col>
|
||||
<Col mediumSize={4}>
|
||||
{filterContainer
|
||||
? (
|
||||
<Portal container={() => filterContainer()}>
|
||||
{filterInstance}
|
||||
</Portal>
|
||||
<Portal container={() => filterContainer()}>
|
||||
{filterInstance}
|
||||
</Portal>
|
||||
) : filterInstance
|
||||
}
|
||||
</Col>
|
||||
|
||||
38
src/common/state-button.js
Normal file
38
src/common/state-button.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
import ActionButton from './action-button'
|
||||
import propTypes from './prop-types'
|
||||
|
||||
const Button = styled(ActionButton)`
|
||||
background-color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateBg`]}
|
||||
border: 2px solid ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]}
|
||||
color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]}
|
||||
`
|
||||
|
||||
const StateButton = ({
|
||||
disabledHandler,
|
||||
disabledLabel,
|
||||
disabledTooltip,
|
||||
|
||||
enabledLabel,
|
||||
enabledTooltip,
|
||||
enabledHandler,
|
||||
|
||||
state,
|
||||
...props
|
||||
}) =>
|
||||
<Button
|
||||
handler={state ? enabledHandler : disabledHandler}
|
||||
tooltip={state ? enabledTooltip : disabledTooltip}
|
||||
{...props}
|
||||
icon={state ? 'running' : 'halted'}
|
||||
size='small'
|
||||
state={state}
|
||||
>
|
||||
{state ? enabledLabel : disabledLabel}
|
||||
</Button>
|
||||
|
||||
export default propTypes({
|
||||
state: propTypes.bool.isRequired
|
||||
})(StateButton)
|
||||
@@ -1,33 +1,22 @@
|
||||
import isFunction from 'lodash/isFunction'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const createAction = (() => {
|
||||
const { defineProperty } = Object
|
||||
const noop = function () {
|
||||
if (arguments.length) {
|
||||
throw new Error('this action expects no payload!')
|
||||
}
|
||||
}
|
||||
|
||||
return (type, payloadCreator = noop) => {
|
||||
const createActionObject = payload => {
|
||||
// Thunks
|
||||
if (isFunction(payload)) {
|
||||
return payload
|
||||
}
|
||||
return (type, payloadCreator) => defineProperty(
|
||||
payloadCreator
|
||||
? (...args) => ({
|
||||
type,
|
||||
payload: payloadCreator(...args)
|
||||
})
|
||||
: (action => function () {
|
||||
if (arguments.length) {
|
||||
throw new Error('this action expects no payload!')
|
||||
}
|
||||
|
||||
return payload === undefined
|
||||
? { type }
|
||||
: { type, payload }
|
||||
}
|
||||
|
||||
return defineProperty(
|
||||
(...args) => createActionObject(payloadCreator(...args)),
|
||||
'toString',
|
||||
{ value: () => type }
|
||||
)
|
||||
}
|
||||
return action
|
||||
})({ type }),
|
||||
'toString',
|
||||
{ value: () => type }
|
||||
)
|
||||
})()
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -102,16 +102,20 @@ export default {
|
||||
|
||||
for (const id in updates) {
|
||||
const object = updates[id]
|
||||
const previous = all[id]
|
||||
|
||||
if (object) {
|
||||
const { type } = object
|
||||
|
||||
all[id] = object
|
||||
get(object.type)[id] = object
|
||||
} else {
|
||||
const previous = all[id]
|
||||
if (previous) {
|
||||
delete all[id]
|
||||
get(type)[id] = object
|
||||
|
||||
if (previous && previous.type !== type) {
|
||||
delete get(previous.type)[id]
|
||||
}
|
||||
} else if (previous) {
|
||||
delete all[id]
|
||||
delete get(previous.type)[id]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ const TAG_STYLE = {
|
||||
padding: '0.3em',
|
||||
verticalAlign: 'middle'
|
||||
}
|
||||
const LINK_STYLE = {
|
||||
cursor: 'pointer'
|
||||
}
|
||||
const ADD_TAG_STYLE = {
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.8em',
|
||||
@@ -32,9 +35,10 @@ const REMOVE_TAG_STYLE = {
|
||||
|
||||
@propTypes({
|
||||
labels: propTypes.arrayOf(React.PropTypes.string).isRequired,
|
||||
onAdd: propTypes.func,
|
||||
onChange: propTypes.func,
|
||||
onDelete: propTypes.func,
|
||||
onAdd: propTypes.func
|
||||
onClick: propTypes.func,
|
||||
onDelete: propTypes.func
|
||||
})
|
||||
export default class Tags extends Component {
|
||||
componentWillMount () {
|
||||
@@ -85,6 +89,7 @@ export default class Tags extends Component {
|
||||
labels,
|
||||
onAdd,
|
||||
onChange,
|
||||
onClick,
|
||||
onDelete
|
||||
} = this.props
|
||||
|
||||
@@ -96,7 +101,7 @@ export default class Tags extends Component {
|
||||
{' '}
|
||||
<span>
|
||||
{map(labels.sort(), (label, index) =>
|
||||
<Tag label={label} onDelete={deleteTag} key={index} />
|
||||
<Tag label={label} onDelete={deleteTag} key={index} onClick={onClick} />
|
||||
)}
|
||||
</span>
|
||||
{(onAdd || onChange) && !this.state.editing
|
||||
@@ -118,9 +123,12 @@ export default class Tags extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export const Tag = ({ label, onDelete }) => (
|
||||
export const Tag = ({ type, label, onDelete, onClick }) => (
|
||||
<span style={TAG_STYLE}>
|
||||
{label}{' '}
|
||||
<span onClick={onClick && (() => onClick(label))} style={onClick && LINK_STYLE}>
|
||||
{label}
|
||||
</span>
|
||||
{' '}
|
||||
{onDelete
|
||||
? <span onClick={onDelete && (() => onDelete(label))} style={REMOVE_TAG_STYLE}>
|
||||
<Icon icon='remove-tag' />
|
||||
|
||||
0
src/common/themes/.index-modules
Normal file
0
src/common/themes/.index-modules
Normal file
6
src/common/themes/base.js
Normal file
6
src/common/themes/base.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
disabledStateBg: '#fff',
|
||||
disabledStateColor: '#c0392b',
|
||||
enabledStateBg: '#fff',
|
||||
enabledStateColor: '#27ae60'
|
||||
}
|
||||
@@ -9,89 +9,77 @@ import propTypes from './prop-types'
|
||||
import { getXoServerTimezone } from './xo'
|
||||
import { Select } from './form'
|
||||
|
||||
const XO_SERVER_TIMEZONE = 'xo-server'
|
||||
const SERVER_TIMEZONE_TAG = 'server'
|
||||
const LOCAL_TIMEZONE = moment.tz.guess()
|
||||
|
||||
@propTypes({
|
||||
defaultValue: propTypes.string,
|
||||
onChange: propTypes.func.isRequired,
|
||||
required: propTypes.bool,
|
||||
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()
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
getXoServerTimezone.then(serverTimezone => {
|
||||
this.setState({
|
||||
options: [{
|
||||
label: _('serverTimezoneOption', {
|
||||
value: serverTimezone
|
||||
}),
|
||||
value: XO_SERVER_TIMEZONE
|
||||
}].concat(this.state.options),
|
||||
serverTimezone
|
||||
timezone: this.props.value || this.props.defaultValue || SERVER_TIMEZONE_TAG,
|
||||
options: [
|
||||
...map(moment.tz.names(), value => ({ label: value, value })),
|
||||
{
|
||||
label: _('serverTimezoneOption', {
|
||||
value: serverTimezone
|
||||
}),
|
||||
value: SERVER_TIMEZONE_TAG
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
componentWillReceiveProps (props) {
|
||||
if (props.value !== this.props.value) {
|
||||
this.setState({ timezone: props.value || SERVER_TIMEZONE_TAG })
|
||||
}
|
||||
}
|
||||
|
||||
get value () {
|
||||
return this.state.timezone === SERVER_TIMEZONE_TAG ? null : this.state.timezone
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.setState({ timezone: value || SERVER_TIMEZONE_TAG })
|
||||
}
|
||||
|
||||
_onChange = option => {
|
||||
if (option && option.value === this.state.timezone) {
|
||||
return
|
||||
}
|
||||
|
||||
this.setState({
|
||||
timezone: option && option.value || SERVER_TIMEZONE_TAG
|
||||
}, () =>
|
||||
this.props.onChange(this.state.timezone === SERVER_TIMEZONE_TAG ? null : this.state.timezone)
|
||||
)
|
||||
}
|
||||
|
||||
_useLocalTime = () => {
|
||||
this._onChange({ value: LOCAL_TIMEZONE })
|
||||
}
|
||||
|
||||
render () {
|
||||
const { props, state } = this
|
||||
const { timezone, options } = this.state
|
||||
|
||||
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}
|
||||
className='mb-1'
|
||||
onChange={this._onChange}
|
||||
options={options}
|
||||
placeholder={_('selectTimezone')}
|
||||
ref='select'
|
||||
value={props.value || XO_SERVER_TIMEZONE}
|
||||
required={this.props.required}
|
||||
value={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}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.tooltipEnabled {
|
||||
background-color: #222;
|
||||
border-radius: 3px;
|
||||
border: 1px solid $fff;
|
||||
border: 1px solid #fff;
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
font-size: 13px;
|
||||
@@ -12,7 +12,7 @@
|
||||
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;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.tooltipDisabled {
|
||||
|
||||
@@ -17,11 +17,18 @@ export class TooltipViewer extends Component {
|
||||
constructor () {
|
||||
super()
|
||||
|
||||
this.state.place = 'top'
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
if (instance) {
|
||||
throw new Error('Tooltip viewer is a singleton!')
|
||||
}
|
||||
instance = this
|
||||
this.state.place = 'top'
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
instance = undefined
|
||||
}
|
||||
|
||||
render () {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import * as actions from 'store/actions'
|
||||
import escapeRegExp from 'lodash/escapeRegExp'
|
||||
import every from 'lodash/every'
|
||||
import forEach from 'lodash/forEach'
|
||||
@@ -16,12 +15,14 @@ import mapValues from 'lodash/mapValues'
|
||||
import React from 'react'
|
||||
import ReadableStream from 'readable-stream'
|
||||
import replace from 'lodash/replace'
|
||||
import store from 'store'
|
||||
import { connect } from 'react-redux'
|
||||
import { getObject } from 'selectors'
|
||||
|
||||
import _ from './intl'
|
||||
import * as actions from './store/actions'
|
||||
import BaseComponent from './base-component'
|
||||
import invoke from './invoke'
|
||||
import store from './store'
|
||||
import { getObject } from './selectors'
|
||||
|
||||
export const EMPTY_ARRAY = Object.freeze([ ])
|
||||
export const EMPTY_OBJECT = Object.freeze({ })
|
||||
@@ -50,6 +51,8 @@ export const propsEqual = (o1, o2, props) => {
|
||||
|
||||
// ===================================================================
|
||||
|
||||
// `subscriptions` can be a function if we want to ensure that the subscription
|
||||
// callbacks have been correctly initialized when there are circular dependencies
|
||||
export const addSubscriptions = subscriptions => Component => {
|
||||
class SubscriptionWrapper extends BaseComponent {
|
||||
constructor () {
|
||||
@@ -59,7 +62,7 @@ export const addSubscriptions = subscriptions => Component => {
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
this._unsubscribes = map(subscriptions, (subscribe, prop) =>
|
||||
this._unsubscribes = map(isFunction(subscriptions) ? subscriptions() : subscriptions, (subscribe, prop) =>
|
||||
subscribe(value => this.setState({ [prop]: value }))
|
||||
)
|
||||
}
|
||||
@@ -182,11 +185,12 @@ export const firstDefined = function () {
|
||||
const n = arguments.length
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const arg = arguments[i]
|
||||
if (arg != null) {
|
||||
if (arg !== undefined) {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
// Returns the current XOA Plan or the Plan name if number given
|
||||
@@ -283,7 +287,7 @@ export const normalizeXenToolsStatus = status => {
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
const _NotFound = () => <h1>Page not found</h1>
|
||||
const _NotFound = () => <h1>{_('errorPageNotFound')}</h1>
|
||||
|
||||
// Decorator to declare routes on a component.
|
||||
//
|
||||
@@ -366,12 +370,13 @@ export const resolveResourceSet = resourceSet => {
|
||||
return
|
||||
}
|
||||
|
||||
const { objects, ...attrs } = resourceSet
|
||||
const { objects, ipPools, ...attrs } = resourceSet
|
||||
const resolvedObjects = {}
|
||||
const resolvedSet = {
|
||||
...attrs,
|
||||
missingObjects: [],
|
||||
objectsByType: resolvedObjects
|
||||
objectsByType: resolvedObjects,
|
||||
ipPools
|
||||
}
|
||||
const state = store.getState()
|
||||
|
||||
@@ -457,3 +462,65 @@ export const htmlFileToStream = file => {
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export const resolveId = value =>
|
||||
(value != null && typeof value === 'object' && 'id' in value)
|
||||
? value.id
|
||||
: value
|
||||
|
||||
export const resolveIds = params => {
|
||||
for (const key in params) {
|
||||
const param = params[key]
|
||||
if (param != null && typeof param === 'object' && 'id' in param) {
|
||||
params[key] = param.id
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
|
||||
const OPs = {
|
||||
'<': a => a < 0,
|
||||
'<=': a => a <= 0,
|
||||
'===': a => a === 0,
|
||||
'>': a => a > 0,
|
||||
'>=': a => a >= 0
|
||||
}
|
||||
|
||||
const makeNiceCompare = compare => function () {
|
||||
const { length } = arguments
|
||||
if (length === 2) {
|
||||
return compare(arguments[0], arguments[1])
|
||||
}
|
||||
|
||||
let i = 1
|
||||
let v1 = arguments[0]
|
||||
let op, v2
|
||||
while (i < length) {
|
||||
op = arguments[i++]
|
||||
v2 = arguments[i++]
|
||||
if (!OPs[op](compare(v1, v2))) {
|
||||
return false
|
||||
}
|
||||
v1 = v2
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export const compareVersions = makeNiceCompare((v1, v2) => {
|
||||
v1 = v1.split('.')
|
||||
v2 = v2.split('.')
|
||||
|
||||
for (let i = 0; i < Math.max(v1.length, v2.length); i++) {
|
||||
const n1 = +v1[i] || 0
|
||||
const n2 = +v2[i] || 0
|
||||
|
||||
if (n1 < n2) return -1
|
||||
if (n1 > n2) return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import map from 'lodash/map'
|
||||
import AbstractInput from '../json-schema-input/abstract-input'
|
||||
import { PureComponent } from 'react'
|
||||
|
||||
import getEventValue from '../get-event-value'
|
||||
|
||||
// ===================================================================
|
||||
|
||||
export default class XoAbstractInput extends AbstractInput {
|
||||
get value () {
|
||||
const value = this.refs.input.value
|
||||
const getId = value => value != null && value.id || value
|
||||
|
||||
if (this.props.schema.type === 'array') {
|
||||
return map(value, object => object.id || object)
|
||||
}
|
||||
export default class XoAbstractInput extends PureComponent {
|
||||
_onChange = event => {
|
||||
const value = getEventValue(event)
|
||||
const { props } = this
|
||||
|
||||
return value.id || value
|
||||
}
|
||||
|
||||
set value (value) {
|
||||
this.refs.input.value = value
|
||||
return props.onChange(
|
||||
props.schema.type === 'array'
|
||||
? map(value, getId)
|
||||
: getId(value)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,12 @@ export default class HighLevelObjectInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectHighLevelObject
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -14,11 +14,12 @@ export default class HostInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectHost
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -14,11 +14,12 @@ export default class PoolInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectPool
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -14,11 +14,12 @@ export default class RemoteInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectRemote
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -14,11 +14,12 @@ export default class RoleInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectRole
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -14,11 +14,12 @@ export default class SrInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectSr
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -14,11 +14,12 @@ export default class SubjectInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectSubject
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -14,11 +14,12 @@ export default class TagInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectTag
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -14,11 +14,12 @@ export default class VmInput extends XoAbstractInput {
|
||||
<PrimitiveInputWrapper {...props}>
|
||||
<SelectVm
|
||||
disabled={props.disabled}
|
||||
hasSelectAll
|
||||
multi={props.multi}
|
||||
onChange={props.onChange}
|
||||
onChange={this._onChange}
|
||||
ref='input'
|
||||
required={props.required}
|
||||
defaultValue={props.defaultValue}
|
||||
value={props.value}
|
||||
/>
|
||||
</PrimitiveInputWrapper>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.dashedLine {
|
||||
stroke: black;
|
||||
stroke-dasharray: 4px 5px;
|
||||
stroke: black;
|
||||
stroke-dasharray: 4px 2px;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
import ChartistGraph from 'react-chartist'
|
||||
import ChartistLegend from 'chartist-plugin-legend'
|
||||
import ChartistTooltip from 'chartist-plugin-tooltip'
|
||||
import map from 'lodash/map'
|
||||
import React from 'react'
|
||||
import size from 'lodash/size'
|
||||
import values from 'lodash/values'
|
||||
import { injectIntl } from 'react-intl'
|
||||
import find from 'lodash/find'
|
||||
import { messages } from 'intl'
|
||||
import {
|
||||
find,
|
||||
flatten,
|
||||
floor,
|
||||
map,
|
||||
max,
|
||||
size,
|
||||
sum,
|
||||
values
|
||||
} from 'lodash'
|
||||
|
||||
import propTypes from '../prop-types'
|
||||
import { computeArraysSum } from '../xo-stats'
|
||||
@@ -53,7 +60,7 @@ const makeOptions = ({ intl, nValues, endTimestamp, interval, valueTransform })
|
||||
// ===================================================================
|
||||
|
||||
const makeLabelInterpolationFnc = (intl, nValues, endTimestamp, interval) => {
|
||||
const labelSpace = Math.floor(nValues / N_LABELS_X)
|
||||
const labelSpace = floor(nValues / N_LABELS_X)
|
||||
let format
|
||||
|
||||
if (interval === 3600) {
|
||||
@@ -150,7 +157,7 @@ export const CpuLineChart = injectIntl(propTypes({
|
||||
nValues: length,
|
||||
endTimestamp: data.endTimestamp,
|
||||
interval: data.interval,
|
||||
valueTransform: value => `${value}%`
|
||||
valueTransform: value => `${floor(value)}%`
|
||||
}),
|
||||
high: !addSumSeries ? 100 : stats.length * 100,
|
||||
...options
|
||||
@@ -159,6 +166,54 @@ export const CpuLineChart = injectIntl(propTypes({
|
||||
)
|
||||
}))
|
||||
|
||||
export const PoolCpuLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const firstHostData = data[0]
|
||||
const length = getStatsLength(firstHostData.stats.cpus)
|
||||
|
||||
if (!length) {
|
||||
return templateError
|
||||
}
|
||||
|
||||
const series = map(data, ({ host, stats }) => ({
|
||||
name: host,
|
||||
data: computeArraysSum(stats.cpus)
|
||||
}))
|
||||
|
||||
if (addSumSeries) {
|
||||
series.push({
|
||||
name: intl.formatMessage(messages.poolAllHosts),
|
||||
data: computeArraysSum(map(series, 'data')),
|
||||
className: styles.dashedLine
|
||||
})
|
||||
}
|
||||
|
||||
const nbCpusByHost = map(data, ({ stats }) => stats.cpus.length)
|
||||
|
||||
return (
|
||||
<ChartistGraph
|
||||
type='Line'
|
||||
data={{
|
||||
series
|
||||
}}
|
||||
options={{
|
||||
...makeOptions({
|
||||
intl,
|
||||
nValues: length,
|
||||
endTimestamp: firstHostData.endTimestamp,
|
||||
interval: firstHostData.interval,
|
||||
valueTransform: value => `${floor(value)}%`
|
||||
}),
|
||||
high: 100 * (addSumSeries ? sum(nbCpusByHost) : max(nbCpusByHost)),
|
||||
...options
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}))
|
||||
|
||||
export const MemoryLineChart = injectIntl(propTypes({
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
@@ -196,6 +251,57 @@ export const MemoryLineChart = injectIntl(propTypes({
|
||||
)
|
||||
}))
|
||||
|
||||
export const PoolMemoryLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const firstHostData = data[0]
|
||||
const {
|
||||
memory,
|
||||
memoryUsed
|
||||
} = firstHostData.stats
|
||||
|
||||
if (!memory || !memoryUsed) {
|
||||
return templateError
|
||||
}
|
||||
|
||||
const series = map(data, ({ host, stats }) => ({
|
||||
name: host,
|
||||
data: stats.memoryUsed
|
||||
}))
|
||||
|
||||
if (addSumSeries) {
|
||||
series.push({
|
||||
name: intl.formatMessage(messages.poolAllHosts),
|
||||
data: computeArraysSum(map(data, 'stats.memoryUsed')),
|
||||
className: styles.dashedLine
|
||||
})
|
||||
}
|
||||
|
||||
const currentMemoryByHost = map(data, ({ stats }) => stats.memory[stats.memory.length - 1])
|
||||
|
||||
return (
|
||||
<ChartistGraph
|
||||
type='Line'
|
||||
data={{
|
||||
series
|
||||
}}
|
||||
options={{
|
||||
...makeOptions({
|
||||
intl,
|
||||
nValues: firstHostData.stats.memoryUsed.length,
|
||||
endTimestamp: firstHostData.endTimestamp,
|
||||
interval: firstHostData.interval,
|
||||
valueTransform: formatSize
|
||||
}),
|
||||
high: addSumSeries ? sum(currentMemoryByHost) : max(currentMemoryByHost),
|
||||
...options
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}))
|
||||
|
||||
export const XvdLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
@@ -292,6 +398,51 @@ export const PifLineChart = injectIntl(propTypes({
|
||||
)
|
||||
}))
|
||||
|
||||
const ios = ['rx', 'tx']
|
||||
export const PoolPifLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const firstHostData = data[0]
|
||||
const length = firstHostData.stats && getStatsLength(firstHostData.stats.pifs.rx)
|
||||
|
||||
if (!length) {
|
||||
return templateError
|
||||
}
|
||||
|
||||
const series = addSumSeries
|
||||
? map(ios, io => ({
|
||||
name: `${intl.formatMessage(messages.poolAllHosts)} (${io})`,
|
||||
data: computeArraysSum(map(data, ({ stats }) => computeArraysSum(stats.pifs[io])))
|
||||
}))
|
||||
: flatten(map(data, ({ stats, host }) =>
|
||||
map(ios, io => ({
|
||||
name: `${host} (${io})`,
|
||||
data: computeArraysSum(stats.pifs[io])
|
||||
}))
|
||||
))
|
||||
|
||||
return (
|
||||
<ChartistGraph
|
||||
type='Line'
|
||||
data={{
|
||||
series
|
||||
}}
|
||||
options={{
|
||||
...makeOptions({
|
||||
intl,
|
||||
nValues: length,
|
||||
endTimestamp: firstHostData.endTimestamp,
|
||||
interval: firstHostData.interval,
|
||||
valueTransform: formatSize
|
||||
}),
|
||||
...options
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}))
|
||||
|
||||
export const LoadLineChart = injectIntl(propTypes({
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
@@ -325,3 +476,48 @@ export const LoadLineChart = injectIntl(propTypes({
|
||||
/>
|
||||
)
|
||||
}))
|
||||
|
||||
export const PoolLoadLineChart = injectIntl(propTypes({
|
||||
addSumSeries: propTypes.bool,
|
||||
data: propTypes.object.isRequired,
|
||||
options: propTypes.object
|
||||
})(({ addSumSeries, data, options = {}, intl }) => {
|
||||
const firstHostData = data[0]
|
||||
const length = firstHostData.stats && firstHostData.stats.load.length
|
||||
|
||||
if (!length) {
|
||||
return templateError
|
||||
}
|
||||
|
||||
const series = map(data, ({ host, stats }) => ({
|
||||
name: host,
|
||||
data: stats.load
|
||||
}))
|
||||
|
||||
if (addSumSeries) {
|
||||
series.push({
|
||||
name: intl.formatMessage(messages.poolAllHosts),
|
||||
data: computeArraysSum(map(data, 'stats.load')),
|
||||
className: styles.dashedLine
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<ChartistGraph
|
||||
type='Line'
|
||||
data={{
|
||||
series
|
||||
}}
|
||||
options={{
|
||||
...makeOptions({
|
||||
intl,
|
||||
nValues: length,
|
||||
endTimestamp: firstHostData.endTimestamp,
|
||||
interval: firstHostData.interval,
|
||||
valueTransform: value => `${value.toPrecision(3)}`
|
||||
}),
|
||||
...options
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}))
|
||||
|
||||
@@ -14,6 +14,7 @@ const STYLE = {}
|
||||
|
||||
const WIDTH = 120
|
||||
const HEIGHT = 20
|
||||
const STROKE_WIDTH = 0.5
|
||||
|
||||
// ===================================================================
|
||||
|
||||
@@ -26,7 +27,7 @@ const templateError =
|
||||
|
||||
export const CpuSparkLines = propTypes({
|
||||
data: propTypes.object.isRequired
|
||||
})(({ data }) => {
|
||||
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
|
||||
const { cpus } = data.stats
|
||||
|
||||
if (!cpus) {
|
||||
@@ -34,15 +35,15 @@ export const CpuSparkLines = propTypes({
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={computeArraysAvg(cpus)} max={100} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#366e98', fill: '#366e98', fillOpacity: 0.5 }} color='#2598d9' />
|
||||
<Sparklines style={STYLE} data={computeArraysAvg(cpus)} max={100} min={0} width={width} height={height}>
|
||||
<SparklinesLine style={{ strokeWidth, stroke: '#366e98', fill: '#366e98', fillOpacity: 0.5 }} color='#2598d9' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
|
||||
export const MemorySparkLines = propTypes({
|
||||
data: propTypes.object.isRequired
|
||||
})(({ data }) => {
|
||||
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
|
||||
const { memory, memoryUsed } = data.stats
|
||||
|
||||
if (!memory || !memoryUsed) {
|
||||
@@ -50,15 +51,15 @@ export const MemorySparkLines = propTypes({
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={memoryUsed} max={memory[memory.length - 1]} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#990822', fill: '#990822', fillOpacity: 0.5 }} color='#cc0066' />
|
||||
<Sparklines style={STYLE} data={memoryUsed} max={memory[memory.length - 1]} min={0} width={width} height={height}>
|
||||
<SparklinesLine style={{ strokeWidth, stroke: '#990822', fill: '#990822', fillOpacity: 0.5 }} color='#cc0066' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
|
||||
export const XvdSparkLines = propTypes({
|
||||
data: propTypes.object.isRequired
|
||||
})(({ data }) => {
|
||||
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
|
||||
const { xvds } = data.stats
|
||||
|
||||
if (!xvds) {
|
||||
@@ -66,15 +67,15 @@ export const XvdSparkLines = propTypes({
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={computeObjectsAvg(xvds)} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#089944', fill: '#089944', fillOpacity: 0.5 }} color='#33cc33' />
|
||||
<Sparklines style={STYLE} data={computeObjectsAvg(xvds)} min={0} width={width} height={height}>
|
||||
<SparklinesLine style={{ strokeWidth, stroke: '#089944', fill: '#089944', fillOpacity: 0.5 }} color='#33cc33' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
|
||||
export const VifSparkLines = propTypes({
|
||||
data: propTypes.object.isRequired
|
||||
})(({ data }) => {
|
||||
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
|
||||
const { vifs } = data.stats
|
||||
|
||||
if (!vifs) {
|
||||
@@ -82,15 +83,15 @@ export const VifSparkLines = propTypes({
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={computeObjectsAvg(vifs)} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
|
||||
<Sparklines style={STYLE} data={computeObjectsAvg(vifs)} min={0} width={width} height={height}>
|
||||
<SparklinesLine style={{ strokeWidth, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
|
||||
export const PifSparkLines = propTypes({
|
||||
data: propTypes.object.isRequired
|
||||
})(({ data }) => {
|
||||
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
|
||||
const { pifs } = data.stats
|
||||
|
||||
if (!pifs) {
|
||||
@@ -98,15 +99,15 @@ export const PifSparkLines = propTypes({
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={computeObjectsAvg(pifs)} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
|
||||
<Sparklines style={STYLE} data={computeObjectsAvg(pifs)} min={0} width={width} height={height}>
|
||||
<SparklinesLine style={{ strokeWidth, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
|
||||
export const LoadSparkLines = propTypes({
|
||||
data: propTypes.object.isRequired
|
||||
})(({ data }) => {
|
||||
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
|
||||
const { load } = data.stats
|
||||
|
||||
if (!load) {
|
||||
@@ -114,8 +115,8 @@ export const LoadSparkLines = propTypes({
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparklines style={STYLE} data={load} min={0} width={WIDTH} height={HEIGHT}>
|
||||
<SparklinesLine style={{ strokeWidth: 0.5, stroke: '#33cc33', fill: '#33cc33', fillOpacity: 0.5 }} color='#33cc33' />
|
||||
<Sparklines style={STYLE} data={load} min={0} width={width} height={height}>
|
||||
<SparklinesLine style={{ strokeWidth, stroke: '#33cc33', fill: '#33cc33', fillOpacity: 0.5 }} color='#33cc33' />
|
||||
</Sparklines>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -378,7 +378,7 @@ export default class XoWeekCharts extends Component {
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<p className='m-t-1'>
|
||||
<p className='mt-1'>
|
||||
{_('weeklyChartsScaleInfo')}
|
||||
{' '}
|
||||
<Toggle iconSize={1} icon='scale' className='btn btn-secondary' onChange={this._updateScale} />
|
||||
|
||||
@@ -1,24 +1,46 @@
|
||||
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'
|
||||
import { createCollectionWrapper, createGetObjectsOfType, createSelector } from 'selectors'
|
||||
import { forEach } from 'lodash'
|
||||
import { SelectHost } from 'select-objects'
|
||||
|
||||
@connectStore(() => ({
|
||||
hosts: createGetObjectsOfType('host')
|
||||
singleHosts: createSelector(
|
||||
(_, { pool }) => pool && pool.id,
|
||||
createGetObjectsOfType('host'),
|
||||
createCollectionWrapper((poolId, hosts) => {
|
||||
const visitedPools = {}
|
||||
const singleHosts = {}
|
||||
forEach(hosts, host => {
|
||||
const { $pool } = host
|
||||
if ($pool !== poolId) {
|
||||
const previousHost = visitedPools[$pool]
|
||||
if (previousHost) {
|
||||
delete singleHosts[previousHost]
|
||||
} else {
|
||||
const { id } = host
|
||||
singleHosts[id] = true
|
||||
visitedPools[$pool] = id
|
||||
}
|
||||
}
|
||||
})
|
||||
return singleHosts
|
||||
})
|
||||
)
|
||||
}), { 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)
|
||||
_getHostPredicate = createSelector(
|
||||
() => this.props.singleHosts,
|
||||
singleHosts => host => singleHosts[host.id]
|
||||
)
|
||||
|
||||
render () {
|
||||
return <div>
|
||||
@@ -27,7 +49,7 @@ export default class AddHostModal extends BaseComponent {
|
||||
<Col size={6}>
|
||||
<SelectHost
|
||||
onChange={this.linkState('host')}
|
||||
predicate={this._hostPredicate}
|
||||
predicate={this._getHostPredicate()}
|
||||
value={this.state.host}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
@@ -9,6 +9,8 @@ import { Toggle } from '../../form'
|
||||
import { injectIntl } from 'react-intl'
|
||||
|
||||
class CopyVmModalBody extends Component {
|
||||
state = { compress: false }
|
||||
|
||||
get value () {
|
||||
const { state } = this
|
||||
return {
|
||||
|
||||
@@ -15,14 +15,16 @@ import sortBy from 'lodash/sortBy'
|
||||
import throttle from 'lodash/throttle'
|
||||
import Xo from 'xo-lib'
|
||||
import { createBackoff } from 'jsonrpc-websocket-client'
|
||||
import { noHostsAvailable } from 'xo-common/api-errors'
|
||||
import { reflect } from 'promise-toolbox'
|
||||
import { resolve } from 'url'
|
||||
|
||||
import _ from '../intl'
|
||||
import invoke from '../invoke'
|
||||
import logError from '../log-error'
|
||||
import { confirm } from '../modal'
|
||||
import { alert, confirm } from '../modal'
|
||||
import { error, info, success } from '../notification'
|
||||
import { noop, rethrow, tap } from '../utils'
|
||||
import { noop, rethrow, tap, resolveId, resolveIds } from '../utils'
|
||||
import {
|
||||
connected,
|
||||
disconnected,
|
||||
@@ -67,6 +69,7 @@ const xo = invoke(() => {
|
||||
credentials: { token }
|
||||
})
|
||||
|
||||
xo.on('authenticationFailure', signOut)
|
||||
xo.on('scheduledAttempt', ({ delay }) => {
|
||||
console.warn('next attempt in %s ms', delay)
|
||||
})
|
||||
@@ -125,6 +128,13 @@ export const connectStore = store => {
|
||||
sendUpdates()
|
||||
})
|
||||
subscribePermissions(permissions => store.dispatch(updatePermissions(permissions)))
|
||||
|
||||
// work around to keep the user in Redux store up to date
|
||||
//
|
||||
// FIXME: store subscriptions data directly in Redux
|
||||
subscribeUsers(user => {
|
||||
store.dispatch(signedIn(xo.user))
|
||||
})
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
@@ -160,14 +170,13 @@ const createSubscription = cb => {
|
||||
if (!isEqual(result, cache)) {
|
||||
cache = result
|
||||
|
||||
/* FIXME: Edge case:
|
||||
* 1) MyComponent has a subscription with subscribers[1]
|
||||
* 2) subscribers[0] causes the MyComponent unmounting (and thus its unsubscription)
|
||||
* When subscribers[1] will be executed, it will no longer exist,
|
||||
* which will throw an error (Uncaught (in promise) TypeError: subscriber is not a function)
|
||||
*/
|
||||
forEach(subscribers, subscriber => {
|
||||
subscriber(result)
|
||||
// A subscriber might have disappeared during iteration.
|
||||
//
|
||||
// E.g.: if a subscriber triggers the subscription of another.
|
||||
if (subscriber) {
|
||||
subscriber(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, error => {
|
||||
@@ -214,9 +223,9 @@ export const subscribeAcls = createSubscription(() => _call('acl.get'))
|
||||
|
||||
export const subscribeJobs = createSubscription(() => _call('job.getAll'))
|
||||
|
||||
export const subscribeJobsLogs = createSubscription(() => _call('log.get', {namespace: 'jobs'}))
|
||||
export const subscribeJobsLogs = createSubscription(() => _call('log.get', { namespace: 'jobs' }))
|
||||
|
||||
export const subscribeApiLogs = createSubscription(() => _call('log.get', {namespace: 'api'}))
|
||||
export const subscribeApiLogs = createSubscription(() => _call('log.get', { namespace: 'api' }))
|
||||
|
||||
export const subscribePermissions = createSubscription(() => _call('acl.getCurrentPermissions'))
|
||||
|
||||
@@ -258,6 +267,19 @@ export const subscribeRoles = createSubscription(invoke(
|
||||
|
||||
export const subscribeIpPools = createSubscription(() => _call('ipPool.getAll'))
|
||||
|
||||
export const subscribeResourceCatalog = createSubscription(() => _call('cloud.getResourceCatalog'))
|
||||
|
||||
const xosanSubscriptions = {}
|
||||
export const subscribeIsInstallingXosan = (pool, cb) => {
|
||||
const poolId = resolveId(pool)
|
||||
|
||||
if (!xosanSubscriptions[poolId]) {
|
||||
xosanSubscriptions[poolId] = createSubscription(() => _call('xosan.checkSrIsBusy', { poolId }))
|
||||
}
|
||||
|
||||
return xosanSubscriptions[poolId](cb)
|
||||
}
|
||||
|
||||
// System ============================================================
|
||||
|
||||
export const apiMethods = _call('system.getMethodsInfo')
|
||||
@@ -266,33 +288,32 @@ export const serverVersion = _call('system.getServerVersion')
|
||||
|
||||
export const getXoServerTimezone = _call('system.getServerTimezone')
|
||||
|
||||
// ===================================================================
|
||||
// XO --------------------------------------------------------------------------
|
||||
|
||||
const resolveId = value =>
|
||||
(value != null && typeof value === 'object' && 'id' in value)
|
||||
? value.id
|
||||
: value
|
||||
|
||||
const resolveIds = params => {
|
||||
for (const key in params) {
|
||||
const param = params[key]
|
||||
if (param != null && typeof param === 'object' && 'id' in param) {
|
||||
params[key] = param.id
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
// Server ------------------------------------------------------------
|
||||
|
||||
export const addServer = (host, username, password) => (
|
||||
_call('server.add', { host, username, password })::tap(
|
||||
subscribeServers.forceRefresh
|
||||
export const importConfig = config => (
|
||||
_call('xo.importConfig').then(({ $sendTo: url }) =>
|
||||
request.post(url).send(config).then(response => {
|
||||
if (response.status !== 200) {
|
||||
throw new Error('config import failed')
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
export const editServer = (server, { host, username, password, readOnly }) => (
|
||||
_call('server.set', { id: resolveId(server), host, username, password, readOnly })::tap(
|
||||
export const exportConfig = () => (
|
||||
_call('xo.exportConfig').then(({ $getFrom: url }) => { window.location = `.${url}` })
|
||||
)
|
||||
|
||||
// Server ------------------------------------------------------------
|
||||
|
||||
export const addServer = (host, username, password, label) => (
|
||||
_call('server.add', { host, label, password, username })::tap(
|
||||
subscribeServers.forceRefresh
|
||||
)::rethrow(() => error(_('serverError'), _('serverAddFailed')))
|
||||
)
|
||||
|
||||
export const editServer = (server, props) => (
|
||||
_call('server.set', { ...props, id: resolveId(server) })::tap(
|
||||
subscribeServers.forceRefresh
|
||||
)
|
||||
)
|
||||
@@ -351,14 +372,14 @@ export const detachHost = host => (
|
||||
confirm({
|
||||
icon: 'host-eject',
|
||||
title: _('detachHostModalTitle'),
|
||||
body: _('detachHostModalMessage', {host: <strong>{host.name_label}</strong>})
|
||||
body: _('detachHostModalMessage', { host: <strong>{host.name_label}</strong> })
|
||||
}).then(
|
||||
() => _call('host.detach', { host: host.id })
|
||||
)
|
||||
)
|
||||
|
||||
export const setDefaultSr = sr => (
|
||||
_call('pool.setDefaultSr', {sr: resolveId(sr)})
|
||||
_call('pool.setDefaultSr', { sr: resolveId(sr) })
|
||||
)
|
||||
|
||||
// Host --------------------------------------------------------------
|
||||
@@ -376,18 +397,31 @@ export const restartHost = (host, force = false) => (
|
||||
title: _('restartHostModalTitle'),
|
||||
body: _('restartHostModalMessage')
|
||||
}).then(
|
||||
() => _call('host.restart', { id: resolveId(host), force }),
|
||||
() => _call('host.restart', { id: resolveId(host), force }).catch(error => {
|
||||
if (noHostsAvailable.is(error)) {
|
||||
alert(_('noHostsAvailableErrorTitle'), _('noHostsAvailableErrorMessage'))
|
||||
}
|
||||
}),
|
||||
noop
|
||||
)
|
||||
)
|
||||
|
||||
export const restartHosts = (hosts, force) => {
|
||||
export const restartHosts = (hosts, force = false) => {
|
||||
const nHosts = size(hosts)
|
||||
return confirm({
|
||||
title: _('restartHostsModalTitle', { nHosts }),
|
||||
body: _('restartHostsModalMessage', { nHosts })
|
||||
}).then(
|
||||
() => map(hosts, host => _call('host.restart', { id: resolveId(host), force })),
|
||||
() => Promise.all(
|
||||
map(hosts, host =>
|
||||
_call('host.restart', { id: resolveId(host), force })::reflect()
|
||||
)
|
||||
).then(results => {
|
||||
const nbErrors = filter(results, result => !result.isFulfilled()).length
|
||||
if (nbErrors) {
|
||||
return alert(_('failHostBulkRestartTitle'), _('failHostBulkRestartMessage', { failedHosts: nbErrors, totalHosts: results.length }))
|
||||
}
|
||||
}),
|
||||
noop
|
||||
)
|
||||
}
|
||||
@@ -471,6 +505,44 @@ export const installAllPatchesOnPool = pool => (
|
||||
_call('pool.installAllPatches', { pool: resolveId(pool) })
|
||||
)
|
||||
|
||||
export const installSupplementalPack = (host, file) => {
|
||||
info(_('supplementalPackInstallStartedTitle'), _('supplementalPackInstallStartedMessage'))
|
||||
|
||||
return _call('host.installSupplementalPack', { host: resolveId(host) }).then(({ $sendTo: url }) => (
|
||||
request.post(url)
|
||||
.send(file)
|
||||
.then(res => {
|
||||
if (res.status !== 200) {
|
||||
throw new Error('installing supplemental pack failed')
|
||||
}
|
||||
|
||||
success(_('supplementalPackInstallSuccessTitle'), _('supplementalPackInstallSuccessMessage'))
|
||||
}).catch(err => {
|
||||
error(_('supplementalPackInstallErrorTitle'), _('supplementalPackInstallErrorMessage'))
|
||||
throw err
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
export const installSupplementalPackOnAllHosts = (pool, file) => {
|
||||
info(_('supplementalPackInstallStartedTitle'), _('supplementalPackInstallStartedMessage'))
|
||||
|
||||
return _call('pool.installSupplementalPack', { pool: resolveId(pool) }).then(({ $sendTo: url }) => (
|
||||
request.post(url)
|
||||
.send(file)
|
||||
.then(res => {
|
||||
if (res.status !== 200) {
|
||||
throw new Error('installing supplemental pack failed')
|
||||
}
|
||||
|
||||
success(_('supplementalPackInstallSuccessTitle'), _('supplementalPackInstallSuccessMessage'))
|
||||
}).catch(err => {
|
||||
error(_('supplementalPackInstallErrorTitle'), _('supplementalPackInstallErrorMessage'))
|
||||
throw err
|
||||
})
|
||||
))
|
||||
}
|
||||
|
||||
// Containers --------------------------------------------------------
|
||||
|
||||
export const pauseContainer = (vm, container) => (
|
||||
@@ -725,7 +797,7 @@ export const createVm = args => (
|
||||
export const createVms = (args, nameLabels) => (
|
||||
confirm({
|
||||
title: _('newVmCreateVms'),
|
||||
body: _('newVmCreateVmsConfirm', {nbVms: nameLabels.length})
|
||||
body: _('newVmCreateVmsConfirm', { nbVms: nameLabels.length })
|
||||
}).then(
|
||||
() => Promise.all(map(nameLabels, name_label => // eslint-disable-line camelcase
|
||||
_call('vm.create', { ...args, name_label })
|
||||
@@ -758,12 +830,12 @@ export const deleteVms = vms => (
|
||||
)
|
||||
)
|
||||
|
||||
export const importBackup = ({remote, file, sr}) => (
|
||||
_call('vm.importBackup', resolveIds({remote, file, sr}))
|
||||
export const importBackup = ({ remote, file, sr }) => (
|
||||
_call('vm.importBackup', resolveIds({ remote, file, sr }))
|
||||
)
|
||||
|
||||
export const importDeltaBackup = ({remote, file, sr}) => (
|
||||
_call('vm.importDeltaBackup', resolveIds({remote, filePath: file, sr}))
|
||||
export const importDeltaBackup = ({ remote, file, sr }) => (
|
||||
_call('vm.importDeltaBackup', resolveIds({ remote, filePath: file, sr }))
|
||||
)
|
||||
|
||||
import RevertSnapshotModalBody from './revert-snapshot-modal'
|
||||
@@ -813,7 +885,7 @@ export const importVms = (vms, sr) => (
|
||||
export const exportVm = vm => {
|
||||
info(_('startVmExport'), vm.id)
|
||||
return _call('vm.export', { vm: resolveId(vm) })
|
||||
.then(({ $getFrom: url }) => window.open(`.${url}`))
|
||||
.then(({ $getFrom: url }) => { window.location = `.${url}` })
|
||||
}
|
||||
|
||||
export const insertCd = (vm, cd, force = false) => (
|
||||
@@ -835,7 +907,7 @@ export const setVmBootOrder = (vm, order) => (
|
||||
})
|
||||
)
|
||||
|
||||
export const attachDiskToVm = (vdi, vm, {bootable, mode, position}) => (
|
||||
export const attachDiskToVm = (vdi, vm, { bootable, mode, position }) => (
|
||||
_call('vm.attachDisk', {
|
||||
bootable,
|
||||
mode,
|
||||
@@ -871,6 +943,19 @@ export const deleteVdi = vdi => (
|
||||
)
|
||||
)
|
||||
|
||||
export const deleteOrphanedVdis = vdis => (
|
||||
confirm({
|
||||
title: _('removeAllOrphanedObject'),
|
||||
body: <div>
|
||||
<p>{_('removeAllOrphanedModalWarning')}</p>
|
||||
<p>{_('definitiveMessageModal')}</p>
|
||||
</div>
|
||||
}).then(
|
||||
() => Promise.all(map(resolveIds(vdis), id => _call('vdi.delete', { id }))),
|
||||
noop
|
||||
)
|
||||
)
|
||||
|
||||
export const migrateVdi = (vdi, sr) => (
|
||||
_call('vdi.migrate', { id: resolveId(vdi), sr_id: resolveId(sr) })
|
||||
)
|
||||
@@ -900,7 +985,7 @@ export const setBootableVbd = (vbd, bootable) => (
|
||||
// VIF ---------------------------------------------------------------
|
||||
|
||||
export const createVmInterface = (vm, network, mac) => (
|
||||
_call('vm.createInterface', resolveIds({vm, network, mac}))
|
||||
_call('vm.createInterface', resolveIds({ vm, network, mac }))
|
||||
)
|
||||
|
||||
export const connectVif = vif => (
|
||||
@@ -1004,6 +1089,15 @@ export const deletePif = pif => (
|
||||
)
|
||||
)
|
||||
|
||||
export const reconfigurePifIp = (pif, { mode, ip, netmask, gateway, dns }) =>
|
||||
_call('pif.reconfigureIp', { pif: resolveId(pif), mode, ip, netmask, gateway, dns })
|
||||
|
||||
export const getIpv4ConfigModes = () =>
|
||||
_call('pif.getIpv4ConfigurationModes')
|
||||
|
||||
export const editPif = (pif, { vlan }) =>
|
||||
_call('pif.editPif', { pif: resolveId(pif), vlan })
|
||||
|
||||
// SR ----------------------------------------------------------------
|
||||
|
||||
export const deleteSr = sr => (
|
||||
@@ -1021,40 +1115,66 @@ export const deleteSr = sr => (
|
||||
|
||||
export const forgetSr = sr => (
|
||||
confirm({
|
||||
title: 'Forget SR',
|
||||
body: <div>
|
||||
<p>Are you sure you want to forget this SR?</p>
|
||||
<p>VDIs on this storage wont be removed.</p>
|
||||
</div>
|
||||
title: _('srForgetModalTitle'),
|
||||
body: _('srForgetModalMessage')
|
||||
}).then(
|
||||
() => _call('sr.forget', { id: resolveId(sr) }),
|
||||
noop
|
||||
)
|
||||
)
|
||||
export const forgetSrs = srs => (
|
||||
confirm({
|
||||
title: _('srsForgetModalTitle'),
|
||||
body: _('srsForgetModalMessage')
|
||||
}).then(
|
||||
() => Promise.all(map(resolveIds(srs), id =>
|
||||
_call('sr.forget', { id })
|
||||
)),
|
||||
noop
|
||||
)
|
||||
)
|
||||
|
||||
export const reconnectAllHostsSr = sr => (
|
||||
confirm({
|
||||
title: 'Reconnect all hosts',
|
||||
body: <div>
|
||||
<p>This will reconnect this SR to all its hosts</p>
|
||||
</div>
|
||||
title: _('srReconnectAllModalTitle'),
|
||||
body: _('srReconnectAllModalMessage')
|
||||
}).then(
|
||||
() => _call('sr.connectAllPbds', { id: resolveId(sr) }),
|
||||
noop
|
||||
)
|
||||
)
|
||||
export const reconnectAllHostsSrs = srs => (
|
||||
confirm({
|
||||
title: _('srReconnectAllModalTitle'),
|
||||
body: _('srReconnectAllModalMessage')
|
||||
}).then(
|
||||
() => Promise.all(resolveIds(srs), id =>
|
||||
_call('sr.connectAllPbds', { id })
|
||||
),
|
||||
noop
|
||||
)
|
||||
)
|
||||
|
||||
export const disconnectAllHostsSr = sr => (
|
||||
confirm({
|
||||
title: 'Disconnect all hosts',
|
||||
body: <div>
|
||||
<p>This will disconnect this SR to all its hosts</p>
|
||||
</div>
|
||||
title: _('srDisconnectAllModalTitle'),
|
||||
body: _('srDisconnectAllModalMessage')
|
||||
}).then(
|
||||
() => _call('sr.disconnectAllPbds', { id: resolveId(sr) }),
|
||||
noop
|
||||
)
|
||||
)
|
||||
export const disconnectAllHostsSrs = srs => (
|
||||
confirm({
|
||||
title: _('srDisconnectAllModalTitle'),
|
||||
body: _('srsDisconnectAllModalMessage')
|
||||
}).then(
|
||||
() => Promise.all(resolveIds(srs), id =>
|
||||
_call('sr.disconnectAllPbds', { id })
|
||||
),
|
||||
noop
|
||||
)
|
||||
)
|
||||
|
||||
export const editSr = (sr, { nameDescription, nameLabel }) => (
|
||||
_call('sr.set', {
|
||||
@@ -1067,6 +1187,11 @@ export const editSr = (sr, { nameDescription, nameLabel }) => (
|
||||
export const rescanSr = sr => (
|
||||
_call('sr.scan', { id: resolveId(sr) })
|
||||
)
|
||||
export const rescanSrs = srs => (
|
||||
Promise.all(map(resolveIds(srs), id =>
|
||||
_call('sr.scan', { id })
|
||||
))
|
||||
)
|
||||
|
||||
// PBDs --------------------------------------------------------------
|
||||
|
||||
@@ -1108,7 +1233,36 @@ export const destroyTask = task => (
|
||||
_call('task.destroy', { id: resolveId(task) })
|
||||
)
|
||||
|
||||
// Backups -----------------------------------------------------------
|
||||
// Jobs -------------------------------------------------------------
|
||||
|
||||
export const createJob = job => (
|
||||
_call('job.create', { job })::tap(
|
||||
subscribeJobs.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const deleteJob = job => (
|
||||
_call('job.delete', { id: resolveId(job) })::tap(
|
||||
subscribeJobs.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const editJob = job => (
|
||||
_call('job.set', { job })::tap(
|
||||
subscribeJobs.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const getJob = id => (
|
||||
_call('job.get', { id })
|
||||
)
|
||||
|
||||
export const runJob = id => {
|
||||
info(_('runJob'), _('runJobVerbose'))
|
||||
return _call('job.runSequence', { idSequence: [id] })
|
||||
}
|
||||
|
||||
// Backup/Schedule ---------------------------------------------------------
|
||||
|
||||
export const createSchedule = (jobId, {
|
||||
cron,
|
||||
@@ -1121,43 +1275,6 @@ export const createSchedule = (jobId, {
|
||||
)
|
||||
)
|
||||
|
||||
export const createJob = job => (
|
||||
_call('job.create', { job })::tap(
|
||||
subscribeJobs.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const runJob = id => {
|
||||
info(_('runJob'), _('runJobVerbose'))
|
||||
return _call('job.runSequence', { idSequence: [id] })
|
||||
}
|
||||
|
||||
export const getJob = id => (
|
||||
_call('job.get', { id })
|
||||
)
|
||||
|
||||
export const setJob = job => (
|
||||
_call('job.set', { job })::tap(
|
||||
subscribeJobs.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const getSchedule = id => (
|
||||
_call('schedule.get', { id })
|
||||
)
|
||||
|
||||
export const enableSchedule = id => (
|
||||
_call('scheduler.enable', { id })::tap(
|
||||
subscribeScheduleTable.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const disableSchedule = id => (
|
||||
_call('scheduler.disable', { id })::tap(
|
||||
subscribeScheduleTable.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const deleteBackupSchedule = async schedule => {
|
||||
await confirm({
|
||||
title: _('deleteBackupSchedule'),
|
||||
@@ -1170,21 +1287,49 @@ export const deleteBackupSchedule = async schedule => {
|
||||
subscribeJobs.forceRefresh()
|
||||
}
|
||||
|
||||
export const deleteSchedule = schedule => (
|
||||
_call('schedule.delete', { id: resolveId(schedule) })::tap(
|
||||
subscribeSchedules.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const disableSchedule = id => (
|
||||
_call('scheduler.disable', { id })::tap(
|
||||
subscribeScheduleTable.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const editSchedule = ({ id, job: jobId, cron, enabled, name, timezone }) => (
|
||||
_call('schedule.set', { id, jobId, cron, enabled, name, timezone })::tap(
|
||||
subscribeSchedules.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const enableSchedule = id => (
|
||||
_call('scheduler.enable', { id })::tap(
|
||||
subscribeScheduleTable.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const getSchedule = id => (
|
||||
_call('schedule.get', { id })
|
||||
)
|
||||
|
||||
// Plugins -----------------------------------------------------------
|
||||
|
||||
export const loadPlugin = async id => (
|
||||
_call('plugin.load', { id })::tap(
|
||||
subscribePlugins.forceRefresh()
|
||||
subscribePlugins.forceRefresh
|
||||
)::rethrow(
|
||||
err => error(_('pluginError'), JSON.stringify(err.data) || _('unknownPluginError'))
|
||||
err => error(_('pluginError'), err && err.message || _('unknownPluginError'))
|
||||
)
|
||||
)
|
||||
|
||||
export const unloadPlugin = id => (
|
||||
_call('plugin.unload', { id })::tap(
|
||||
subscribePlugins.forceRefresh()
|
||||
subscribePlugins.forceRefresh
|
||||
)::rethrow(
|
||||
err => error(_('pluginError'), JSON.stringify(err.data) || _('unknownPluginError'))
|
||||
err => error(_('pluginError'), err && err.message || _('unknownPluginError'))
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1221,6 +1366,9 @@ export const purgePluginConfiguration = async id => {
|
||||
subscribePlugins.forceRefresh()
|
||||
}
|
||||
|
||||
export const testPlugin = async (id, data) =>
|
||||
_call('plugin.test', { id, data })
|
||||
|
||||
// Resource set ------------------------------------------------------
|
||||
|
||||
export const createResourceSet = (name, { subjects, objects, limits } = {}) => (
|
||||
@@ -1229,8 +1377,8 @@ export const createResourceSet = (name, { subjects, objects, limits } = {}) => (
|
||||
)
|
||||
)
|
||||
|
||||
export const editRessourceSet = (id, { name, subjects, objects, limits } = {}) => (
|
||||
_call('resourceSet.set', { id, name, subjects, objects, limits })::tap(
|
||||
export const editResourceSet = (id, { name, subjects, objects, limits, ipPools } = {}) => (
|
||||
_call('resourceSet.set', { id, name, subjects, objects, limits, ipPools })::tap(
|
||||
subscribeResourceSets.forceRefresh
|
||||
)
|
||||
)
|
||||
@@ -1252,67 +1400,89 @@ export const recomputeResourceSetsLimits = () => (
|
||||
// Remote ------------------------------------------------------------
|
||||
|
||||
export const getRemote = remote => (
|
||||
_call('remote.get', resolveIds({id: remote}))::rethrow(
|
||||
_call('remote.get', resolveIds({ id: remote }))::rethrow(
|
||||
err => error(_('getRemote'), err.message || String(err))
|
||||
)
|
||||
)
|
||||
|
||||
export const createRemote = (name, url) => (
|
||||
_call('remote.create', {name, url})::tap(
|
||||
_call('remote.create', { name, url })::tap(
|
||||
subscribeRemotes.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const deleteRemote = remote => (
|
||||
_call('remote.delete', {id: resolveId(remote)})::tap(
|
||||
_call('remote.delete', { id: resolveId(remote) })::tap(
|
||||
subscribeRemotes.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const enableRemote = remote => (
|
||||
_call('remote.set', {id: resolveId(remote), enabled: true})::tap(
|
||||
_call('remote.set', { id: resolveId(remote), enabled: true })::tap(
|
||||
subscribeRemotes.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const disableRemote = remote => (
|
||||
_call('remote.set', {id: resolveId(remote), enabled: false})::tap(
|
||||
_call('remote.set', { id: resolveId(remote), enabled: false })::tap(
|
||||
subscribeRemotes.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const editRemote = (remote, {name, url}) => (
|
||||
_call('remote.set', resolveIds({remote, name, url}))::tap(
|
||||
export const editRemote = (remote, { name, url }) => (
|
||||
_call('remote.set', resolveIds({ remote, name, url }))::tap(
|
||||
subscribeRemotes.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const listRemote = remote => (
|
||||
_call('remote.list', resolveIds({id: remote}))::tap(
|
||||
_call('remote.list', resolveIds({ id: remote }))::tap(
|
||||
subscribeRemotes.forceRefresh
|
||||
)::rethrow(
|
||||
err => error(_('listRemote'), err.message || String(err))
|
||||
)
|
||||
)
|
||||
|
||||
export const listRemoteBackups = remote => (
|
||||
_call('backup.list', resolveIds({ remote }))::rethrow(
|
||||
err => error(_('listRemote'), err.message || String(err))
|
||||
)
|
||||
)
|
||||
|
||||
export const testRemote = remote => (
|
||||
_call('remote.test', resolveIds({id: remote}))::rethrow(
|
||||
_call('remote.test', resolveIds({ id: remote }))::rethrow(
|
||||
err => error(_('testRemote'), err.message || String(err))
|
||||
)
|
||||
)
|
||||
|
||||
// File restore ----------------------------------------------------
|
||||
|
||||
export const scanDisk = (remote, disk) => (
|
||||
_call('backup.scanDisk', resolveIds({ remote, disk }))
|
||||
)
|
||||
|
||||
export const scanFiles = (remote, disk, path, partition) => (
|
||||
_call('backup.scanFiles', resolveIds({ remote, disk, path, partition }))
|
||||
)
|
||||
|
||||
export const fetchFiles = (remote, disk, partition, paths, format) => (
|
||||
_call('backup.fetchFiles', resolveIds({ remote, disk, partition, paths, format })).then(
|
||||
({ $getFrom: url }) => { window.location = `.${url}` }
|
||||
)
|
||||
)
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
export const probeSrNfs = (host, server) => (
|
||||
_call('sr.probeNfs', {host, server})
|
||||
_call('sr.probeNfs', { host, server })
|
||||
)
|
||||
|
||||
export const probeSrNfsExists = (host, server, serverPath) => (
|
||||
_call('sr.probeNfsExists', {host, server, serverPath})
|
||||
_call('sr.probeNfsExists', { host, server, serverPath })
|
||||
)
|
||||
|
||||
export const probeSrIscsiIqns = (host, target, port = undefined, chapUser = undefined, chapPassword) => {
|
||||
const params = {host, target}
|
||||
const params = { host, target }
|
||||
port && (params.port = port)
|
||||
chapUser && (params.chapUser = chapUser)
|
||||
chapPassword && (params.chapPassword = chapPassword)
|
||||
@@ -1320,14 +1490,14 @@ export const probeSrIscsiIqns = (host, target, port = undefined, chapUser = unde
|
||||
}
|
||||
|
||||
export const probeSrIscsiLuns = (host, target, targetIqn, chapUser = undefined, chapPassword) => {
|
||||
const params = {host, target, targetIqn}
|
||||
const params = { host, target, targetIqn }
|
||||
chapUser && (params.chapUser = chapUser)
|
||||
chapPassword && (params.chapPassword = chapPassword)
|
||||
return _call('sr.probeIscsiLuns', params)
|
||||
}
|
||||
|
||||
export const probeSrIscsiExists = (host, target, targetIqn, scsiId, port = undefined, chapUser = undefined, chapPassword) => {
|
||||
const params = {host, target, targetIqn, scsiId}
|
||||
const params = { host, target, targetIqn, scsiId }
|
||||
port && (params.port = port)
|
||||
chapUser && (params.chapUser = chapUser)
|
||||
chapPassword && (params.chapPassword = chapPassword)
|
||||
@@ -1335,21 +1505,21 @@ export const probeSrIscsiExists = (host, target, targetIqn, scsiId, port = undef
|
||||
}
|
||||
|
||||
export const reattachSr = (host, uuid, nameLabel, nameDescription, type) => (
|
||||
_call('sr.reattach', {host, uuid, nameLabel, nameDescription, type})
|
||||
_call('sr.reattach', { host, uuid, nameLabel, nameDescription, type })
|
||||
)
|
||||
|
||||
export const reattachSrIso = (host, uuid, nameLabel, nameDescription, type) => (
|
||||
_call('sr.reattachIso', {host, uuid, nameLabel, nameDescription, type})
|
||||
_call('sr.reattachIso', { host, uuid, nameLabel, nameDescription, type })
|
||||
)
|
||||
|
||||
export const createSrNfs = (host, nameLabel, nameDescription, server, serverPath, nfsVersion = undefined) => {
|
||||
const params = {host, nameLabel, nameDescription, server, serverPath}
|
||||
const params = { host, nameLabel, nameDescription, server, serverPath }
|
||||
nfsVersion && (params.nfsVersion = nfsVersion)
|
||||
return _call('sr.createNfs', params)
|
||||
}
|
||||
|
||||
export const createSrIscsi = (host, nameLabel, nameDescription, target, targetIqn, scsiId, port = undefined, chapUser = undefined, chapPassword = undefined) => {
|
||||
const params = {host, nameLabel, nameDescription, target, targetIqn, scsiId}
|
||||
const params = { host, nameLabel, nameDescription, target, targetIqn, scsiId }
|
||||
port && (params.port = port)
|
||||
chapUser && (params.chapUser = chapUser)
|
||||
chapPassword && (params.chapPassword = chapPassword)
|
||||
@@ -1357,20 +1527,20 @@ export const createSrIscsi = (host, nameLabel, nameDescription, target, targetIq
|
||||
}
|
||||
|
||||
export const createSrIso = (host, nameLabel, nameDescription, path, type, user = undefined, password = undefined) => {
|
||||
const params = {host, nameLabel, nameDescription, path, type}
|
||||
const params = { host, nameLabel, nameDescription, path, type }
|
||||
user && (params.user = user)
|
||||
password && (params.password = password)
|
||||
return _call('sr.createIso', params)
|
||||
}
|
||||
|
||||
export const createSrLvm = (host, nameLabel, nameDescription, device) => (
|
||||
_call('sr.createLvm', {host, nameLabel, nameDescription, device})
|
||||
_call('sr.createLvm', { host, nameLabel, nameDescription, device })
|
||||
)
|
||||
|
||||
// Job logs ----------------------------------------------------------
|
||||
|
||||
export const deleteJobsLog = id => (
|
||||
_call('log.delete', {namespace: 'jobs', id})::tap(
|
||||
_call('log.delete', { namespace: 'jobs', id })::tap(
|
||||
subscribeJobsLogs.forceRefresh
|
||||
)
|
||||
)
|
||||
@@ -1378,23 +1548,23 @@ export const deleteJobsLog = id => (
|
||||
// Logs
|
||||
|
||||
export const deleteApiLog = id => (
|
||||
_call('log.delete', {namespace: 'api', id})::tap(
|
||||
_call('log.delete', { namespace: 'api', id })::tap(
|
||||
subscribeApiLogs.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
// Acls, users, groups ----------------------------------------------------------
|
||||
|
||||
export const addAcl = ({subject, object, action}) => (
|
||||
_call('acl.add', resolveIds({subject, object, action}))::tap(
|
||||
export const addAcl = ({ subject, object, action }) => (
|
||||
_call('acl.add', resolveIds({ subject, object, action }))::tap(
|
||||
subscribeAcls.forceRefresh
|
||||
)::rethrow(
|
||||
err => error('Add ACL', err.message || String(err))
|
||||
)
|
||||
)
|
||||
|
||||
export const removeAcl = ({subject, object, action}) => (
|
||||
_call('acl.remove', resolveIds({subject, object, action}))::tap(
|
||||
export const removeAcl = ({ subject, object, action }) => (
|
||||
_call('acl.remove', resolveIds({ subject, object, action }))::tap(
|
||||
subscribeAcls.forceRefresh
|
||||
)::rethrow(
|
||||
err => error('Remove ACL', err.message || String(err))
|
||||
@@ -1409,14 +1579,14 @@ export const editAcl = (
|
||||
action: newAction = action
|
||||
}
|
||||
) => (
|
||||
_call('acl.remove', resolveIds({subject, object, action}))
|
||||
.then(() => _call('acl.add', resolveIds({subject: newSubject, object: newObject, action: newAction})))
|
||||
_call('acl.remove', resolveIds({ subject, object, action }))
|
||||
.then(() => _call('acl.add', resolveIds({ subject: newSubject, object: newObject, action: newAction })))
|
||||
::tap(subscribeAcls.forceRefresh)
|
||||
::rethrow(err => error('Edit ACL', err.message || String(err)))
|
||||
)
|
||||
|
||||
export const createGroup = name => (
|
||||
_call('group.create', {name})::tap(
|
||||
_call('group.create', { name })::tap(
|
||||
subscribeGroups.forceRefresh
|
||||
):: rethrow(
|
||||
err => error(_('createGroup'), err.message || String(err))
|
||||
@@ -1424,7 +1594,7 @@ export const createGroup = name => (
|
||||
)
|
||||
|
||||
export const setGroupName = (group, name) => (
|
||||
_call('group.set', resolveIds({group, name}))::tap(
|
||||
_call('group.set', resolveIds({ group, name }))::tap(
|
||||
subscribeGroups.forceRefresh
|
||||
)
|
||||
)
|
||||
@@ -1433,13 +1603,13 @@ export const deleteGroup = group => (
|
||||
confirm({
|
||||
title: _('deleteGroup'),
|
||||
body: <p>{_('deleteGroupConfirm')}</p>
|
||||
}).then(() => _call('group.delete', resolveIds({id: group})))
|
||||
}).then(() => _call('group.delete', resolveIds({ id: group })))
|
||||
::tap(subscribeGroups.forceRefresh)
|
||||
::rethrow(err => error(_('deleteGroup'), err.message || String(err)))
|
||||
)
|
||||
|
||||
export const removeUserFromGroup = (user, group) => (
|
||||
_call('group.removeUser', resolveIds({id: group, userId: user}))::tap(
|
||||
_call('group.removeUser', resolveIds({ id: group, userId: user }))::tap(
|
||||
subscribeGroups.forceRefresh
|
||||
)::rethrow(
|
||||
err => error(_('removeUserFromGroup'), err.message || String(err))
|
||||
@@ -1447,7 +1617,7 @@ export const removeUserFromGroup = (user, group) => (
|
||||
)
|
||||
|
||||
export const addUserToGroup = (user, group) => (
|
||||
_call('group.addUser', resolveIds({id: group, userId: user}))::tap(
|
||||
_call('group.addUser', resolveIds({ id: group, userId: user }))::tap(
|
||||
subscribeGroups.forceRefresh
|
||||
)::rethrow(
|
||||
err => error('Add User', err.message || String(err))
|
||||
@@ -1455,7 +1625,7 @@ export const addUserToGroup = (user, group) => (
|
||||
)
|
||||
|
||||
export const createUser = (email, password, permission) => (
|
||||
_call('user.create', {email, password, permission})::tap(
|
||||
_call('user.create', { email, password, permission })::tap(
|
||||
subscribeUsers.forceRefresh
|
||||
)::rethrow(
|
||||
err => error('Create user', err.message || String(err))
|
||||
@@ -1617,32 +1787,6 @@ export const setDefaultHomeFilter = (type, name) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Jobs ----------------------------------------------------------
|
||||
|
||||
export const deleteJob = job => (
|
||||
_call('job.delete', { id: resolveId(job) })::tap(
|
||||
subscribeJobs.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const deleteSchedule = schedule => (
|
||||
_call('schedule.delete', { id: resolveIds(schedule) })::tap(
|
||||
subscribeSchedules.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const updateJob = job => (
|
||||
_call('job.set', {job})::tap(
|
||||
subscribeJobs.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
export const updateSchedule = ({ id, job: jobId, cron, enabled, name, timezone }) => (
|
||||
_call('schedule.set', { id, jobId, cron, enabled, name, timezone })::tap(
|
||||
subscribeSchedules.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
// IP pools --------------------------------------------------------------------
|
||||
|
||||
export const createIpPool = ({ name, ips, networks }) => {
|
||||
@@ -1664,3 +1808,30 @@ export const setIpPool = (ipPool, { name, addresses, networks }) => (
|
||||
subscribeIpPools.forceRefresh
|
||||
)
|
||||
)
|
||||
|
||||
// XO SAN ----------------------------------------------------------------------
|
||||
|
||||
export const getVolumeInfo = (xosanSr) => _call('xosan.getVolumeInfo', { sr: xosanSr })
|
||||
|
||||
export const createXosanSR = ({ template, pif, vlan, srs, glusterType, redundancy }) => _call('xosan.createSR', {
|
||||
template,
|
||||
pif: pif.id,
|
||||
vlan: String(vlan),
|
||||
srs: resolveIds(srs),
|
||||
glusterType,
|
||||
redundancy: Number.parseInt(redundancy)
|
||||
})
|
||||
|
||||
export const computeXosanPossibleOptions = lvmSrs => _call('xosan.computeXosanPossibleOptions', { lvmSrs })
|
||||
|
||||
import InstallXosanPackModal from './install-xosan-pack-modal'
|
||||
export const downloadAndInstallXosanPack = pool =>
|
||||
confirm({
|
||||
title: _('xosanInstallPackTitle', { pool: pool.name_label }),
|
||||
icon: 'export',
|
||||
body: <InstallXosanPackModal pool={pool} />
|
||||
}).then(
|
||||
pack => _call('xosan.downloadAndInstallXosanPack', { id: pack.id, version: pack.version, pool: resolveId(pool) })
|
||||
)
|
||||
|
||||
export const registerXosan = namespace => _call('cloud.registerResource', { namespace: 'xosan' })
|
||||
|
||||
103
src/common/xo/install-xosan-pack-modal/index.js
Normal file
103
src/common/xo/install-xosan-pack-modal/index.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import _ from 'intl'
|
||||
import Component from 'base-component'
|
||||
import React from 'react'
|
||||
import { connectStore, compareVersions } from 'utils'
|
||||
import { subscribeResourceCatalog, subscribePlugins } from 'xo'
|
||||
import { createGetObjectsOfType, createSelector, createCollectionWrapper } from 'selectors'
|
||||
import { satisfies as versionSatisfies } from 'semver'
|
||||
import {
|
||||
every,
|
||||
filter,
|
||||
forEach,
|
||||
map
|
||||
} from 'lodash'
|
||||
|
||||
const findLatestPack = (packs, hostsVersions) => {
|
||||
const checkVersion = version =>
|
||||
every(hostsVersions, hostVersion => versionSatisfies(hostVersion, version))
|
||||
|
||||
let latestPack = { version: '0' }
|
||||
forEach(packs, pack => {
|
||||
const xsVersionRequirement = pack.requirements && pack.requirements.xenserver
|
||||
|
||||
if (
|
||||
pack.type === 'iso' &&
|
||||
compareVersions(pack.version, latestPack.version) > 0 &&
|
||||
(!xsVersionRequirement || checkVersion(xsVersionRequirement))
|
||||
) {
|
||||
latestPack = pack
|
||||
}
|
||||
})
|
||||
|
||||
if (latestPack.version === '0') {
|
||||
// No compatible pack was found
|
||||
return
|
||||
}
|
||||
|
||||
return latestPack
|
||||
}
|
||||
|
||||
@connectStore({
|
||||
hosts: createGetObjectsOfType('host').filter(
|
||||
(_, { pool }) => host => pool && host.$pool === pool.id && !host.supplementalPacks['vates:XOSAN']
|
||||
)
|
||||
}, { withRef: true })
|
||||
export default class InstallXosanPackModal extends Component {
|
||||
componentDidMount () {
|
||||
this._unsubscribePlugins = subscribePlugins(plugins => this.setState({ plugins }))
|
||||
this._unsubscribeResourceCatalog = subscribeResourceCatalog(catalog => this.setState({ catalog }))
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this._unsubscribePlugins()
|
||||
this._unsubscribeResourceCatalog()
|
||||
}
|
||||
|
||||
_getXosanLatestPack = createSelector(
|
||||
() => this.state.catalog && this.state.catalog.xosan,
|
||||
createSelector(
|
||||
() => this.props.hosts,
|
||||
createCollectionWrapper(hosts => map(hosts, 'version'))
|
||||
),
|
||||
findLatestPack
|
||||
)
|
||||
|
||||
_getXosanPacks = createSelector(
|
||||
() => this.state.catalog && this.state.catalog.xosan,
|
||||
packs => filter(packs, ({ type }) => type === 'iso')
|
||||
)
|
||||
|
||||
get value () {
|
||||
return this._getXosanLatestPack()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { hosts } = this.props
|
||||
const latestPack = this._getXosanLatestPack()
|
||||
|
||||
return <div>
|
||||
{latestPack
|
||||
? <div>
|
||||
{_('xosanInstallPackOnHosts')}
|
||||
<ul>
|
||||
{map(hosts, host => <li key={host.id}>{host.name_label}</li>)}
|
||||
</ul>
|
||||
<div className='mt-1'>
|
||||
{_('xosanInstallPack', { pack: latestPack.name, version: latestPack.version })}
|
||||
</div>
|
||||
</div>
|
||||
: <div>
|
||||
<p>{_('xosanNoPackFound')}</p>
|
||||
<p>
|
||||
{_('xosanPackRequirements')}
|
||||
<ul>
|
||||
{map(this._getXosanPacks(), ({ name, requirements }) => <li>
|
||||
{name}: <strong>{requirements && requirements.xenserver ? requirements.xenserver : '/'}</strong>
|
||||
</li>)}
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ export default class NewSshKeyModalBody extends BaseComponent {
|
||||
} = this.state
|
||||
|
||||
return <div>
|
||||
<div className='p-b-1'>
|
||||
<div className='pb-1'>
|
||||
<SingleLineRow>
|
||||
<Col size={4}>{_('title')}</Col>
|
||||
<Col size={8}>
|
||||
@@ -40,7 +40,7 @@ export default class NewSshKeyModalBody extends BaseComponent {
|
||||
</Col>
|
||||
</SingleLineRow>
|
||||
</div>
|
||||
<div className='p-b-1'>
|
||||
<div className='pb-1'>
|
||||
<SingleLineRow>
|
||||
<Col size={4}>{_('key')}</Col>
|
||||
<Col size={8}>
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
import forEach from 'lodash/forEach'
|
||||
import {
|
||||
forEach,
|
||||
includes,
|
||||
map
|
||||
} from 'lodash'
|
||||
|
||||
export const getDefaultNetworkForVif = (vif, destHost, pifs, networks) => {
|
||||
const originNetwork = networks[vif.$network]
|
||||
const originVlans = map(originNetwork.PIFs, pifId => pifs[pifId].vlan)
|
||||
|
||||
let destNetworkId = pifs[destHost.$PIFs[0]].$network
|
||||
|
||||
forEach(destHost.$PIFs, pifId => {
|
||||
const { $network, vlan } = pifs[pifId]
|
||||
|
||||
if (networks[$network].name_label === originNetwork.name_label) {
|
||||
destNetworkId = $network
|
||||
|
||||
export const getDefaultNetworkForVif = (vif, host, pifs, networks) => {
|
||||
const nameLabel = networks[vif.$network].name_label
|
||||
let defaultNetwork
|
||||
forEach(host.$PIFs, pifId => {
|
||||
const pif = pifs[pifId]
|
||||
if (pif.ip && networks[pif.$network].name_label === nameLabel) {
|
||||
defaultNetwork = pif.$network
|
||||
return false
|
||||
}
|
||||
|
||||
if (vlan !== -1 && includes(originVlans, vlan)) {
|
||||
destNetworkId = $network
|
||||
}
|
||||
})
|
||||
return defaultNetwork
|
||||
|
||||
return destNetworkId
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import assign from 'lodash/assign'
|
||||
import Client from 'jsonrpc-websocket-client'
|
||||
import Client, {AbortedConnection, ConnectionError} from 'jsonrpc-websocket-client'
|
||||
import eventToPromise from 'event-to-promise'
|
||||
import forEach from 'lodash/forEach'
|
||||
import makeError from 'make-error'
|
||||
@@ -52,7 +52,7 @@ function getCurrentUrl () {
|
||||
}
|
||||
|
||||
function adaptUrl (url, port = null) {
|
||||
const matches = /^http(s?):\/\/([^\/:]*(?::[^\/]*)?)(?:[^:]*)?$/.exec(url)
|
||||
const matches = /^http(s?):\/\/([^/:]*(?::[^/]*)?)(?:[^:]*)?$/.exec(url)
|
||||
if (!matches || !matches[2]) {
|
||||
throw new Error('current URL not recognized')
|
||||
}
|
||||
@@ -77,7 +77,7 @@ class XoaUpdater extends EventEmitter {
|
||||
|
||||
state (state) {
|
||||
this._state = state
|
||||
this.emit(state)
|
||||
this.emit(state, this._lowState && this._lowState.source)
|
||||
}
|
||||
|
||||
async update () {
|
||||
@@ -99,12 +99,21 @@ class XoaUpdater extends EventEmitter {
|
||||
}
|
||||
|
||||
_upgradeSuccessful () {
|
||||
this.emit('upgradeSuccessful')
|
||||
this.emit('upgradeSuccessful', this._lowState && this._lowState.source)
|
||||
}
|
||||
|
||||
async _open () {
|
||||
const openFailure = error => {
|
||||
this.log('error', error)
|
||||
switch (true) {
|
||||
case error instanceof AbortedConnection:
|
||||
this.log('error', 'AbortedConnection')
|
||||
break
|
||||
case error instanceof ConnectionError:
|
||||
this.log('error', 'ConnectionError')
|
||||
break
|
||||
default:
|
||||
this.log('error', error)
|
||||
}
|
||||
delete this._client
|
||||
this.state('disconnected')
|
||||
throw error
|
||||
@@ -208,7 +217,7 @@ class XoaUpdater extends EventEmitter {
|
||||
return c
|
||||
} else {
|
||||
return eventToPromise.multi(c, ['open'], ['closed', 'error'])
|
||||
.then(() => handleOpen(c), openFailure)
|
||||
.then(() => c)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,20 +9,23 @@ import { connectStore, getXoaPlan } from './utils'
|
||||
import { isAdmin } from 'selectors'
|
||||
|
||||
const Upgrade = propTypes({
|
||||
available: propTypes.number.isRequired,
|
||||
place: propTypes.string.isRequired
|
||||
available: propTypes.number,
|
||||
place: propTypes.string.isRequired,
|
||||
required: propTypes.number
|
||||
})(connectStore({
|
||||
isAdmin
|
||||
}))(({
|
||||
available,
|
||||
children,
|
||||
isAdmin,
|
||||
place
|
||||
}) => (
|
||||
<Card>
|
||||
place,
|
||||
required = available
|
||||
}) => process.env.XOA_PLAN < required
|
||||
? <Card>
|
||||
<CardHeader>{_('upgradeNeeded')}</CardHeader>
|
||||
{isAdmin
|
||||
? <CardBlock className='text-xs-center'>
|
||||
<p>{_('availableIn', {plan: getXoaPlan(available)})}</p>
|
||||
<p>{_('availableIn', {plan: getXoaPlan(required)})}</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')}
|
||||
@@ -37,6 +40,7 @@ const Upgrade = propTypes({
|
||||
</CardBlock>
|
||||
}
|
||||
</Card>
|
||||
))
|
||||
: children
|
||||
)
|
||||
|
||||
export { Upgrade as default }
|
||||
|
||||
BIN
src/favicon.ico
Normal file
BIN
src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -128,6 +128,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-map-marker;
|
||||
}
|
||||
&-file {
|
||||
@extend .fa;
|
||||
@extend .fa-file-o;
|
||||
}
|
||||
|
||||
&-shown {
|
||||
@extend .fa;
|
||||
@@ -195,7 +199,7 @@
|
||||
|
||||
&-cpu {
|
||||
@extend .fa;
|
||||
@extend .fa-dashboard;
|
||||
@extend .fa-microchip;
|
||||
}
|
||||
&-memory {
|
||||
@extend .fa;
|
||||
@@ -303,10 +307,6 @@
|
||||
@extend .fa;
|
||||
@extend .fa-camera;
|
||||
}
|
||||
&-export {
|
||||
@extend .fa;
|
||||
@extend .fa-download;
|
||||
}
|
||||
&-fast-clone {
|
||||
@extend .fa;
|
||||
@extend .fa-code-fork;
|
||||
@@ -375,6 +375,24 @@
|
||||
@extend .xo-status-busy;
|
||||
}
|
||||
|
||||
&-all-connected {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
@extend .xo-status-running;
|
||||
}
|
||||
|
||||
&-some-connected {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
@extend .xo-status-busy;
|
||||
}
|
||||
|
||||
&-all-disconnected {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
@extend .xo-status-halted;
|
||||
}
|
||||
|
||||
// Task
|
||||
&-task {
|
||||
&-cancel {
|
||||
@@ -569,6 +587,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-file-archive-o;
|
||||
}
|
||||
&-export {
|
||||
@extend .fa;
|
||||
@extend .fa-download;
|
||||
}
|
||||
&-schedule {
|
||||
@extend .fa;
|
||||
@extend .fa-clock-o;
|
||||
@@ -656,6 +678,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-upload;
|
||||
}
|
||||
&-file-restore {
|
||||
@extend .fa;
|
||||
@extend .fa-file-o;
|
||||
}
|
||||
}
|
||||
&-menu-jobs {
|
||||
@extend .fa;
|
||||
@@ -704,6 +730,10 @@
|
||||
@extend .fa;
|
||||
@extend .fa-list;
|
||||
}
|
||||
&-config {
|
||||
@extend .fa;
|
||||
@extend .fa-file-o;
|
||||
}
|
||||
}
|
||||
&-menu-about {
|
||||
@extend .fa;
|
||||
@@ -725,6 +755,10 @@
|
||||
@extend .fa-file-archive-o;
|
||||
}
|
||||
}
|
||||
&-menu-xosan {
|
||||
@extend .fa;
|
||||
@extend .fa-database;
|
||||
}
|
||||
// New VM
|
||||
&-new-vm {
|
||||
&-infos {
|
||||
|
||||
11
src/index.js
11
src/index.js
@@ -1,3 +1,5 @@
|
||||
import './patch-react'
|
||||
|
||||
import DevTools from 'store/dev-tools'
|
||||
import hashHistory from 'react-router/lib/hashHistory'
|
||||
import React from 'react'
|
||||
@@ -8,15 +10,6 @@ import { render } from 'react-dom'
|
||||
|
||||
import XoApp from './xo-app'
|
||||
|
||||
if (
|
||||
typeof window !== 'undefined' &&
|
||||
typeof window.addEventListener === 'function'
|
||||
) {
|
||||
window.addEventListener('unhandledRejection', reason => {
|
||||
console.error(reason)
|
||||
})
|
||||
}
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<div>
|
||||
|
||||
@@ -21,7 +21,6 @@ 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}
|
||||
|
||||
@@ -44,6 +43,11 @@ html.no-js(
|
||||
href = 'modules.css'
|
||||
)
|
||||
|
||||
link(
|
||||
rel = 'shortcut icon'
|
||||
href = 'favicon.ico'
|
||||
)
|
||||
|
||||
//- Styles required for a proper display while loading.
|
||||
style.
|
||||
html, body, #xo-app { height: 100% }
|
||||
@@ -6,6 +6,7 @@ const keymap = {
|
||||
GO_TO_HOSTS: 'g h',
|
||||
GO_TO_POOLS: 'g p',
|
||||
GO_TO_VMS: 'g v',
|
||||
GO_TO_SRS: 'g s',
|
||||
CREATE_VM: 'c v',
|
||||
UNFOCUS: 'esc',
|
||||
HELP: ['?', 'h']
|
||||
|
||||
42
src/patch-react.js
Normal file
42
src/patch-react.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import logError from 'log-error'
|
||||
import React from 'react'
|
||||
import { assign, isFunction } from 'lodash'
|
||||
|
||||
// Avoid global breakage if a component fails to render.
|
||||
//
|
||||
// Inspired by https://gist.github.com/Aldredcz/4d63b0a9049b00f54439f8780be7f0d8
|
||||
React.createElement = (createElement => {
|
||||
const errorComponent = <p className='text-danger' style={{
|
||||
fontWeight: 'bold'
|
||||
}}>an error has occured</p>
|
||||
|
||||
const wrapRender = render => function patchedRender () {
|
||||
try {
|
||||
return render.apply(this, arguments)
|
||||
} catch (error) {
|
||||
logError(error)
|
||||
|
||||
return errorComponent
|
||||
}
|
||||
}
|
||||
|
||||
return function (Component) {
|
||||
if (isFunction(Component)) {
|
||||
const patched = Component._patched
|
||||
if (patched) {
|
||||
arguments[0] = patched
|
||||
} else {
|
||||
const { prototype } = Component
|
||||
let render
|
||||
if (prototype && isFunction(render = prototype.render)) {
|
||||
prototype.render = wrapRender(render)
|
||||
Component._patched = Component // itself
|
||||
} else {
|
||||
arguments[0] = Component._patched = assign(wrapRender(Component), Component)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return createElement.apply(this, arguments)
|
||||
}
|
||||
})(React.createElement)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user