Compare commits

..

123 Commits

Author SHA1 Message Date
Julien Fontanet
6d1e2c47d3 5.7.10 2017-04-24 17:53:00 +02:00
Julien Fontanet
8b9b0346cb chore(backup/new): do not send unnecessary job props 2017-04-24 17:51:13 +02:00
Julien Fontanet
0d11817e3f 5.7.9 2017-04-24 15:50:35 +02:00
Julien Fontanet
a8cb209717 chore(backup/new): clarify timeout meaning 2017-04-24 15:49:43 +02:00
Olivier Lambert
cf45ffddf1 feat(sr/disks): provide filters for snapshots (#2103)
Fixes #2102
2017-04-24 15:26:29 +02:00
Pierre Donias
2e0ea51c30 fix(dashboard): replace unused labelId by tooltip (#2100)
Fixes #2090
2017-04-21 17:41:51 +02:00
Julien Fontanet
0f7f8c7330 chore(Combobox): use uncontrollableInput 2017-04-20 12:08:16 +02:00
Julien Fontanet
808f72409f fix(home): replace history when setting initial filter 2017-04-19 17:19:14 +02:00
Julien Fontanet
f8e2d29372 chore(Button): own implementation instead of react-bootstrap (#2089) 2017-04-18 10:12:02 +02:00
Julien Fontanet
22dec27c65 chore(package): update standard to v10 (#2067) 2017-04-13 17:34:49 +02:00
Julien Fontanet
89b3806a7a fix(vm/action-bar): pending status for copy 2017-04-13 17:34:20 +02:00
Julien Fontanet
b6bedf9253 chore(ButtonGroup): own implementation instead of react-bootstrap 2017-04-13 14:43:32 +02:00
Julien Fontanet
0d4983043b feat(vm/tab-snapshots): add pending status on new snasphot 2017-04-13 14:16:07 +02:00
Julien Fontanet
f9ff3fe168 fix(vm/tab-disks): fix StateButton prop 2017-04-13 14:16:07 +02:00
Julien Fontanet
4a25c5323f feat(servers): add pending status 2017-04-13 14:16:07 +02:00
Julien Fontanet
9b4e2d3bb8 feat(vm/action-bar): add pending status 2017-04-13 14:16:07 +02:00
Julien Fontanet
3915efcf92 feat(ActionButton): add pending prop 2017-04-13 14:16:07 +02:00
Julien Fontanet
4591ff8522 chore(ActionButton): do not reassign redirectOnSuccess 2017-04-13 14:16:07 +02:00
Julien Fontanet
e3491797f3 chore(ActionButton): do not use React.PropTypes directly 2017-04-13 14:16:07 +02:00
Julien Fontanet
6eee167675 chore(ActionButton): props documentation 2017-04-13 14:16:07 +02:00
Julien Fontanet
16b965b28a chore(ActionButton): use relative import for Icon 2017-04-13 14:16:07 +02:00
Julien Fontanet
5125410efd chore(ActionButton): do not use react-bootstrap 2017-04-13 14:16:07 +02:00
karolsok
1a4da2a8de feat(intl): improve pl translation (#2088) 2017-04-13 09:50:29 +02:00
Julien Fontanet
991fbaec86 5.7.8 2017-04-12 17:32:37 +02:00
Pierre Donias
fb399278b3 fix(new-vm): add network predicate in VIF item (#2087)
Fixes #2086
2017-04-12 17:32:00 +02:00
Julien Fontanet
b868092365 fix(vms/new): only display Shared checkbox in resource set
Fixes #2061
2017-04-12 17:07:00 +02:00
Pierre Donias
80fdc6849f fix(XOSAN): use new pack format to check if XOSAN pack is installed (#2085) 2017-04-12 16:53:08 +02:00
karolsok
25ffcb952b fix(user/lang selector): fix Polski spelling (#2083) 2017-04-12 12:33:28 +02:00
Julien Fontanet
083ac1e2d6 5.7.7 2017-04-11 17:02:51 +02:00
Julien Fontanet
5a4b553a60 fix(form/Toggle): onChange now emits the raw value
Fixes #2080
2017-04-11 16:54:13 +02:00
Julien Fontanet
b1135ef566 fix(backup/new): do not send timeout=null on creation 2017-04-11 16:38:17 +02:00
Julien Fontanet
1928d1e00f chore(backup/new): remove now unnecessary code 2017-04-11 16:33:30 +02:00
Julien Fontanet
a369f7f387 fix(backup/new): wrap tags in array
Fixes #176
2017-04-11 16:12:08 +02:00
Olivier Lambert
33d9801dfe feat(i18n): add Polish language, fixes #2079 2017-04-11 08:51:44 +02:00
Julien Fontanet
8c7a031cca chore: coding style 2017-04-10 17:32:38 +02:00
Julien Fontanet
9484d87e76 chore(backup/new): clearer timeout label 2017-04-10 17:24:53 +02:00
Julien Fontanet
4b6822d6e5 fix(backup/new): default owner is current user 2017-04-10 17:23:10 +02:00
Julien Fontanet
7241a0529b fix(utils/addSubscription): only use setState() when mounted 2017-04-10 17:05:50 +02:00
Julien Fontanet
66083b4e50 fix(backup/new): timeout should be in seconds
Fixes #2076
2017-04-10 16:47:04 +02:00
karolsok
f631b3cc64 feat(intl): pl translation (#2068) 2017-04-10 16:07:01 +02:00
Julien Fontanet
bb58d9b4d6 5.7.6 2017-04-07 16:16:06 +02:00
Julien Fontanet
93ebff1055 fix(backup/new): default to non smart backup 2017-04-07 16:12:51 +02:00
Julien Fontanet
08aec1c09a fix(backup/new): job creation was broken 2017-04-07 16:10:56 +02:00
Julien Fontanet
8ca98a56fe 5.7.5 2017-04-07 15:52:13 +02:00
Julien Fontanet
705f53e3e5 fix(scheduling): timezone selection 2017-04-07 15:51:49 +02:00
Julien Fontanet
adaf069d20 5.7.4 2017-04-07 15:27:46 +02:00
Julien Fontanet
d7be7d8660 fix(select-objects): do not treat empty string as a value (2) 2017-04-07 15:25:41 +02:00
Julien Fontanet
faddee86b6 fix(select-objects): do not treat empty string as a value 2017-04-07 15:23:27 +02:00
Julien Fontanet
c4fcc65d16 fix(backup/new): coding style 2017-04-07 15:17:51 +02:00
Julien Fontanet
890631d33b fix(select-objects): correctly handle incorrect values with non-multi 2017-04-07 15:17:32 +02:00
Julien Fontanet
8e8145bb48 chore(backup/new): controlled inputs (#2072) 2017-04-07 15:02:53 +02:00
Julien Fontanet
d73d6719a5 5.7.3 2017-04-06 19:30:33 +02:00
Pierre Donias
3419bee198 feat(pack,patch): support 7.1 packs/patches format (#2069)
Fixes #2058
2017-04-06 19:05:51 +02:00
Julien Fontanet
4368fad393 fix(react-novnc): do not error if canvas is not mounted 2017-04-06 17:28:29 +02:00
badrAZ
ab93fdbf10 feat: Display a warning when the CD drive is not completely installed (#2066)
Fixes #2064
2017-04-06 15:22:42 +02:00
Julien Fontanet
8fd7697a45 fix(vm/disks): attach/create disk for non-PV VM 2017-04-06 12:15:22 +02:00
badrAZ
1121a60912 feat(host/network): use StateButton (#2063)
Fixes #2060
2017-04-06 09:58:41 +02:00
Julien Fontanet
e7b4bd2fe4 5.7.2 2017-04-05 15:07:39 +02:00
Julien Fontanet
fcd8bdd1b3 chore(backup/new): simplify smart-backup condition 2017-04-05 14:52:52 +02:00
badrAZ
e6f140f575 fix(select-objects): display missing objects (#2059)
Fixes #2052
2017-04-05 14:52:21 +02:00
Julien Fontanet
bfe4c45fcf fix(xo/configurePlugin): do not swallow error 2017-04-03 10:31:21 +02:00
Julien Fontanet
f95370124b 5.7.1 2017-03-31 18:05:29 +02:00
Julien Fontanet
2564343816 fix(xo-json-schema-input/vm): controlled mode 2017-03-31 18:03:35 +02:00
Julien Fontanet
03734eb761 fix(logs): do not fail on non-string params 2017-03-31 18:03:35 +02:00
Julien Fontanet
29d63a9fdd 5.7.0 2017-03-31 16:36:40 +02:00
Julien Fontanet
ca94b236a8 feat(settings/plugins): easier edition 2017-03-31 16:35:14 +02:00
Julien Fontanet
fa1ec30ba5 chore(json-schema-input): controlled inputs (#2001) 2017-03-31 16:21:54 +02:00
Olivier Lambert
2b1423aebe fix(changelog): it seems we are in 2017. 2017-03-31 14:55:56 +02:00
Pierre Donias
373332141f fix(pool/packs): starter plan required to install packs (#2055) 2017-03-31 11:01:17 +02:00
Olivier Lambert
ecf2cf15b5 fix(changelog): typo in 5.7 release 2017-03-31 10:32:50 +02:00
Olivier Lambert
4ee0831d93 feat(changelog): updates for 5.7 2017-03-31 10:31:11 +02:00
Pierre Donias
7df2a88c13 feat(xosan/pack): check XS version requirement (#2054) 2017-03-31 10:18:43 +02:00
Olivier Lambert
3d52556c67 feat(changelog): updates for 5.6 2017-03-31 10:11:38 +02:00
badrAZ
437b160a3f feat(servers): add label property (#2051)
Fixes #1965
2017-03-29 16:23:51 +02:00
Pierre Donias
5c87b82e0c feat(new-vm,vm): select an affinity host (#2039)
See #1983
2017-03-29 14:07:55 +02:00
badrAZ
7f2bc79d5f feat(ActionButton): improve error reporting (#2050)
Fixes #2048
2017-03-29 12:03:19 +02:00
Pierre Donias
837a61acf3 fix(home): not visible items should never be selected (#2042)
Fixes #2027
Fixes #2035
2017-03-29 10:53:31 +02:00
badrAZ
5971eed72a feat(jobs): configure job timeout (#2043)
Fixes #1956
2017-03-29 10:39:29 +02:00
Pierre Donias
1b8224030b fix(ipPools): prevent creating 2 IP pools with the same name (#2041)
Fixes #1731
2017-03-24 12:26:52 +01:00
Pierre Donias
ed3ec3fa8b fix(vm/disks): do not show bootable flags for non PV VMs (#2040)
Fixes #1996
2017-03-24 11:49:46 +01:00
Pierre Donias
aa98ca49e5 feat(locales): Hungarian (hu) (#2038)
Fixes #2019
2017-03-24 10:36:03 +01:00
badrAZ
44d35c2351 feat: more uses of StateButton (#2034) 2017-03-23 17:46:26 +01:00
badrAZ
df8eb7a000 feat({backup,job}/overview): clearer state (#2023)
Fixes #1958
2017-03-23 09:42:23 +01:00
Julien Fontanet
ac061c8750 chore(backup/new): improve description of report 2017-03-22 12:13:35 +01:00
Julien Fontanet
656d3e55ac feat(backup/new): report on failure by default 2017-03-22 12:09:13 +01:00
Julien Fontanet
50641287f8 fix(XoApp): wait for signin before show pages 2017-03-17 14:48:33 +01:00
Julien Fontanet
0bc072aa65 feat(Home): add a None filter 2017-03-17 14:26:21 +01:00
Julien Fontanet
9d7d665520 chore(Home#_getDefaultFilter): cleaner code 2017-03-17 14:26:21 +01:00
Julien Fontanet
819ea94e7b fix(xo): keep user in store up to date 2017-03-17 14:26:21 +01:00
badrAZ
40753568df fix(settings/remotes): no duplicate names (#2021)
Fixes #1879
2017-03-17 14:11:15 +01:00
badrAZ
8793aed561 feat(home): improve inter-types linkage (#2015)
Fixes #2012
2017-03-16 10:11:52 +01:00
Julien Fontanet
377a50bc09 fix: minor warnings 2017-03-15 17:02:03 +01:00
Julien Fontanet
fe5a43fbdf chore: update yarn.lock 2017-03-15 16:09:17 +01:00
badrAZ
7f44220220 feat(new VM): share a VM (#2013) 2017-03-15 14:38:28 +01:00
greenkeeper[bot]
0df1610ca9 chore(package): update gulp-csso to version 3.0.0 (#2009)
https://greenkeeper.io/
2017-03-14 14:51:53 +01:00
Julien Fontanet
24c8b9e02d chore(auto-controlled-component): remove base-component dep 2017-03-14 11:43:47 +01:00
Pierre Donias
01b311f2ba fix(new-vm): remove bootable option (#2008)
Fixes #2007
2017-03-14 11:27:49 +01:00
Pierre Donias
a2bb3182f4 feat(backup/logs): show job tag in table (#2005)
Fixes #1982
2017-03-14 10:54:39 +01:00
Pierre Donias
c86e15a310 feat(xo/utils/getDefaultNetworkForVif): match network with same VLAN (#1997)
Fixes #1990
2017-03-13 18:03:14 +01:00
Julien Fontanet
862e5a95e7 fix(package): update babel-plugin-lodash 2017-03-09 17:51:47 +01:00
Julien Fontanet
73e2c7e849 chore(package): use babel-plugin-dev 2017-03-09 17:50:08 +01:00
Julien Fontanet
0b0937e233 chore(base-component): remove shallow-equal dep 2017-03-09 15:22:52 +01:00
Julien Fontanet
6bf114859f chore(base-component): remove invoke dep 2017-03-09 15:20:14 +01:00
Julien Fontanet
db6d67eeb7 feat(JsonSchemaInput/EnumInput): handle enumNames 2017-03-08 18:08:59 +01:00
Julien Fontanet
a345d89aac fix(home): changing type reset paging
Fixes #1993
2017-03-06 15:47:42 +01:00
Pierre Donias
e8f8ebb112 feat(XOSAN): select suggestion, SVG graph (#1991) 2017-03-06 12:01:38 +01:00
Julien Fontanet
1dad5b5c3a 5.6.3 2017-03-02 19:07:29 +01:00
Pierre Donias
5cc5ee4e87 fix(XOSAN): XS v7.0 required to install XOSAN (#1981) 2017-03-02 17:38:55 +01:00
Julien Fontanet
e8d2b32a14 5.6.2 2017-03-01 17:09:19 +01:00
Julien Fontanet
f492909e42 fix(linting): ignored files go into /.gitignore 2017-03-01 15:19:39 +01:00
Pierre Donias
7ea17750a1 fix(pool/patches): disable patching for free plan (#1972) 2017-03-01 10:13:09 +01:00
Julien Fontanet
663e1f1a4b fix(menu): XOSAN only displayed to admins
Fixes #1968
2017-02-28 17:54:21 +01:00
Julien Fontanet
079310c67e fix(store/reducer/object): missing part of previous fix 2017-02-28 17:01:36 +01:00
Julien Fontanet
5cf7f1f886 fix(store/reducer/object): handle type change
Fixes #1967
2017-02-28 16:14:07 +01:00
Julien Fontanet
9f64af859e chore(package): update react-select to v1.0.0-rc.3 2017-02-28 10:44:04 +01:00
Julien Fontanet
007aa776cb chore(package): update index-modules to v0.3.0 2017-02-28 10:33:12 +01:00
Julien Fontanet
66bc092edd chore(package): update husky to v0.13.1 2017-02-28 10:30:44 +01:00
Julien Fontanet
140a88ee12 chore(package): update jest to v19.0.2 2017-02-28 10:29:44 +01:00
Julien Fontanet
f42758938d fix(package): migrate ghooks→husky config 2017-02-27 11:41:31 +01:00
Julien Fontanet
e19fd81536 chore: update yarn.lock 2017-02-27 11:40:28 +01:00
Julien Fontanet
73835ded96 chore(store/actions/createAction): minor optimizations 2017-02-27 11:37:56 +01:00
Julien Fontanet
1ec1a8bd94 chore(package): update superagent to version 3.5.0 (#1962)
Closes #1947

https://greenkeeper.io/
2017-02-27 11:34:57 +01:00
greenkeeper[bot]
f0b6d57ba8 chore(package): update modular-css to version 4.1.1 (#1952)
https://greenkeeper.io/
2017-02-27 11:33:12 +01:00
125 changed files with 10140 additions and 2756 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
/dist/
/node_modules/
/src/common/intl/locales/index.js
/src/common/themes/index.js
npm-debug.log
npm-debug.log.*

View File

@@ -1,5 +1,86 @@
# ChangeLog
## **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.

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-web",
"version": "5.6.1",
"version": "5.7.10",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -34,6 +34,7 @@
"ansi_up": "^1.3.0",
"asap": "^2.0.4",
"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",
@@ -67,7 +68,7 @@
"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-plumber": "^1.1.0",
"gulp-pug": "^3.1.0",
@@ -77,10 +78,10 @@
"gulp-uglify": "^2.0.0",
"gulp-watch": "^4.3.5",
"human-format": "^0.7.0",
"husky": "^0.12.0",
"index-modules": "^0.2.1",
"husky": "^0.13.1",
"index-modules": "^0.3.0",
"is-ip": "^1.0.0",
"jest": "^18.0.0",
"jest": "^19.0.2",
"jsonrpc-websocket-client": "^0.1.1",
"kindof": "^2.0.0",
"later": "^1.2.0",
@@ -88,7 +89,7 @@
"loose-envify": "^1.1.0",
"make-error": "^1.2.1",
"marked": "^0.3.5",
"modular-css": "^3.0.0",
"modular-css": "^4.1.1",
"moment": "^2.13.0",
"moment-timezone": "^0.5.4",
"notifyjs": "^3.0.0",
@@ -113,7 +114,7 @@
"react-overlays": "^0.6.0",
"react-redux": "^5.0.0",
"react-router": "^3.0.0",
"react-select": "^1.0.0-beta13",
"react-select": "^1.0.0-rc.3",
"react-shortcuts": "^1.3.1",
"react-sparklines": "^1.5.0",
"react-virtualized": "^8.0.8",
@@ -124,9 +125,12 @@
"redux-devtools-log-monitor": "^1.0.5",
"redux-thunk": "^2.0.1",
"reselect": "^2.2.1",
"standard": "^8.4.0",
"superagent": "^2.0.0",
"semver": "^5.3.0",
"standard": "^10.0.0",
"styled-components": "^1.4.4",
"superagent": "^3.5.0",
"tar-stream": "^1.5.2",
"uncontrollable-input": "^0.0.1",
"vinyl": "^2.0.0",
"watchify": "^3.7.0",
"xml2js": "^0.4.17",
@@ -139,6 +143,7 @@
"benchmarks": "./tools/run-benchmarks.js 'src/**/*.bench.js'",
"build": "npm run build-indexes && NODE_ENV=production gulp build",
"build-indexes": "index-modules --auto src",
"commitmsg": "npm test",
"dev": "npm run build-indexes && NODE_ENV=development gulp build",
"dev-test": "jest --watch",
"lint": "standard",
@@ -168,6 +173,8 @@
}
},
"plugins": [
"dev",
"lodash",
"transform-decorators-legacy",
"transform-runtime"
],
@@ -177,17 +184,15 @@
"stage-0"
]
},
"config": {
"ghooks": {
"commit-msg": "npm test"
}
},
"jest": {
"snapshotSerializers": [
"enzyme-to-json/serializer"
]
},
"standard": {
"globals": [
"__DEV__"
],
"ignore": [
"dist"
],

View File

@@ -1,14 +1,19 @@
exports[`test Col 1`] = `
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Col 1`] = `
<div
className="col-xs-12" />
className="col-xs-12"
/>
`;
exports[`test Container 1`] = `
exports[`Container 1`] = `
<div
className="container-fluid" />
className="container-fluid"
/>
`;
exports[`test Row 1`] = `
exports[`Row 1`] = `
<div
className=" row" />
className=" row"
/>
`;

View File

@@ -1,14 +1,9 @@
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'
import {
noop
} from 'utils'
import { map, noop } from 'lodash'
import _ from './intl'
import ActionButton from './action-button'
import ButtonGroup from './button-group'
const ActionBar = ({ actions, param }) => (
<ButtonGroup>
@@ -17,18 +12,24 @@ const ActionBar = ({ actions, param }) => (
return
}
const { handler, handlerParam = param, label, icon, redirectOnSuccess } = button
return <Tooltip key={index} content={_(label)}>
<ActionButton
key={index}
btnStyle='secondary'
handler={handler || noop}
handlerParam={handlerParam}
icon={icon}
redirectOnSuccess={redirectOnSuccess}
size='large'
/>
</Tooltip>
const {
handler,
handlerParam = param,
icon,
label,
pending,
redirectOnSuccess
} = button
return <ActionButton
key={index}
handler={handler || noop}
handlerParam={handlerParam}
icon={icon}
pending={pending}
redirectOnSuccess={redirectOnSuccess}
size='large'
tooltip={_(label)}
/>
})}
</ButtonGroup>
)

View File

@@ -1,59 +1,82 @@
import Icon from 'icon'
import isFunction from 'lodash/isFunction'
import React from 'react'
import { Button } from 'react-bootstrap-4/lib'
import Button from './button'
import Component from './base-component'
import Icon from './icon'
import logError from './log-error'
import propTypes from './prop-types'
import Tooltip from './tooltip'
import { error as _error } from './notification'
@propTypes({
btnStyle: propTypes.string,
// React element to use as button content
children: propTypes.node,
// whether this button is disabled (default to false)
disabled: propTypes.bool,
// form identifier
//
// if provided, this button and its action are associated to this
// form for the submit event
form: propTypes.string,
// function to call when the action is triggered (via a clik on the
// button or submit on the form)
handler: propTypes.func.isRequired,
// optional value which will be passed as first param to the handler
handlerParam: propTypes.any,
// XO icon to use for this button
icon: propTypes.string.isRequired,
// whether the action of this action is already underway
pending: propTypes.bool,
// path to redirect to when the triggered action finish successfully
//
// if a function, it will be called with the result of the action to
// compute the path
redirectOnSuccess: propTypes.oneOfType([
propTypes.func,
propTypes.string
]),
size: propTypes.oneOf([
'large',
'small'
]),
// React element to use tooltip for the component
tooltip: propTypes.node
})
export default class ActionButton extends Component {
static contextTypes = {
router: React.PropTypes.object
router: propTypes.object
}
async _execute () {
if (this.state.working) {
if (this.props.pending || this.state.working) {
return
}
const {
children,
handler,
handlerParam
handlerParam,
tooltip
} = this.props
try {
this.setState({
error: null,
error: undefined,
working: true
})
const result = await handler(handlerParam)
let { redirectOnSuccess } = this.props
const { redirectOnSuccess } = this.props
if (redirectOnSuccess) {
if (isFunction(redirectOnSuccess)) {
redirectOnSuccess = redirectOnSuccess(result)
}
return this.context.router.push(redirectOnSuccess)
return this.context.router.push(
isFunction(redirectOnSuccess) ? redirectOnSuccess(result) : redirectOnSuccess
)
}
this.setState({
@@ -68,6 +91,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))
}
}
}
@@ -97,28 +121,30 @@ export default class ActionButton extends Component {
render () {
const {
props: {
btnStyle,
children,
className,
disabled,
form,
icon,
size: bsSize,
style,
tooltip
pending,
tooltip,
...props
},
state: { error, working }
} = this
const button = <Button
bsStyle={error ? 'warning' : btnStyle}
form={form}
onClick={!form && this._execute}
disabled={working || disabled}
type={form ? 'submit' : 'button'}
{...{ bsSize, className, style }}
>
<Icon icon={working ? 'loading' : icon} fixedWidth />
if (error !== undefined) {
props.btnStyle = 'warning'
}
if (pending || working) {
props.disabled = true
}
delete props.handler
delete props.handlerParam
if (props.form === undefined) {
props.onClick = this._execute
}
delete props.redirectOnSuccess
const button = <Button {...props}>
<Icon icon={pending || working ? 'loading' : icon} fixedWidth />
{children && ' '}
{children}
</Button>

View File

@@ -1,87 +0,0 @@
import React from 'react'
import { isFunction, omit } from 'lodash'
import Component from './base-component'
import getEventValue from './get-event-value'
const __DEV__ = process.env.NODE_ENV !== 'production'
// This decorator can be used on a controlled input component to make
// it able to automatically handled the uncontrolled mode.
export default options => ControlledInput => {
class AutoControlledInput extends Component {
constructor (props) {
super()
const opts = isFunction(options) ? options(props) : options
const controlled = this._controlled = 'value' in props
if (!controlled) {
this.state.value = props.defaultValue || opts && opts.defaultValue
this._onChange = event => {
let defaultPrevented = false
const { onChange } = this.props
if (onChange) {
onChange(event)
defaultPrevented = event && event.defaultPrevented
}
if (!defaultPrevented) {
this.setState({ value: getEventValue(event) })
}
}
} else if (__DEV__ && 'defaultValue' in props) {
throw new Error(`${this.constructor.name}: controlled component should not have a default value`)
}
}
get value () {
return this._controlled
? this.props.value
: this.state.value
}
set value (value) {
if (__DEV__ && this._controlled) {
throw new Error(`${this.constructor.name}: should not set value on controlled component`)
}
this.setState({ value })
}
render () {
if (this._controlled) {
return <ControlledInput {...this.props} />
}
return <ControlledInput
{...omit(this.props, 'defaultValue')}
onChange={this._onChange}
value={this.state.value}
/>
}
}
if (__DEV__) {
AutoControlledInput.prototype.componentWillReceiveProps = function (newProps) {
const { name } = this.constructor
const controlled = this._controlled
const newControlled = 'value' in newProps
if (!controlled) {
if (newControlled) {
throw new Error(`${name}: uncontrolled component should not become controlled`)
}
} else if (!newControlled) {
throw new Error(`${name}: controlled component should not become uncontrolled`)
}
if (newProps.defaultValue !== this.props.defaultValue) {
throw new Error(`${name}: default value should not change`)
}
}
}
return AutoControlledInput
}

View File

@@ -3,11 +3,9 @@ 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?
//
@@ -19,7 +17,7 @@ const cowSet = (object, path, value, depth) => {
return value
}
object = clone(object)
object = object != null ? clone(object) : {}
const prop = path[depth]
object[prop] = cowSet(object[prop], path, value, depth + 1)
return object
@@ -36,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)
@@ -46,11 +44,11 @@ export default class BaseComponent extends Component {
this._linkedState = null
if (VERBOSE) {
this.render = invoke(this.render, render => () => {
this.render = (render => () => {
console.log('render', this.constructor.name)
return render.call(this)
})
})(this.render)
}
}
@@ -112,13 +110,6 @@ export default class BaseComponent extends Component {
})
})
}
shouldComponentUpdate (newProps, newState) {
return !(
shallowEqual(this.props, newProps) &&
shallowEqual(this.state, newState)
)
}
}
if (VERBOSE) {

View File

@@ -0,0 +1,8 @@
import React from 'react'
const ButtonGroup = ({ children }) =>
<div className='btn-group' role='group'>
{children}
</div>
export { ButtonGroup as default }

56
src/common/button.js Normal file
View File

@@ -0,0 +1,56 @@
import classNames from 'classnames'
import React from 'react'
import propTypes from './prop-types'
const Button = ({
active,
block,
btnStyle = 'secondary',
children,
outline,
size,
...props
}) => {
props.className = classNames(
props.className,
'btn',
`btn${outline ? '-outline' : ''}-${btnStyle}`,
active !== undefined && 'active',
block && 'btn-block',
size === 'large' ? 'btn-lg' : size === 'small' ? 'btn-sm' : null
)
if (props.type === undefined && props.form === undefined) {
props.type = 'button'
}
return <button {...props}>{children}</button>
}
propTypes(Button)({
active: propTypes.bool,
block: propTypes.bool,
// Bootstrap button style
//
// See https://v4-alpha.getbootstrap.com/components/buttons/#examples
//
// The default value (secondary) is not listed here because it does
// not make sense to explicit it.
btnStyle: propTypes.oneOf([
'danger',
'info',
'link',
'primary',
'success',
'warning'
]),
outline: propTypes.bool,
size: propTypes.oneOf([
'large',
'small'
])
})
export { Button as default }

View File

@@ -1,5 +1,6 @@
import React from 'react'
import Button from './button'
import Component from './base-component'
import Icon from './icon'
import propTypes from './prop-types'
@@ -27,9 +28,9 @@ export default class Collapse extends Component {
return (
<div className={props.className}>
<button className='btn btn-lg btn-primary btn-block' onClick={this._onClick}>
<Button block btnStyle='primary' size='large' onClick={this._onClick}>
{props.buttonText} <Icon icon={`chevron-${isOpened ? 'up' : 'down'}`} />
</button>
</Button>
{isOpened && props.children}
</div>
)

64
src/common/combobox.js Normal file
View File

@@ -0,0 +1,64 @@
import React from 'react'
import uncontrollableInput from 'uncontrollable-input'
import { isEmpty, map } from 'lodash'
import {
DropdownButton,
MenuItem
} from 'react-bootstrap-4/lib'
import Component from './base-component'
import propTypes from './prop-types'
@uncontrollableInput({
defaultValue: ''
})
@propTypes({
disabled: propTypes.bool,
options: propTypes.oneOfType([
propTypes.arrayOf(propTypes.string),
propTypes.objectOf(propTypes.string)
]),
onChange: propTypes.func.isRequired,
value: propTypes.string.isRequired
})
export default class Combobox extends Component {
_handleChange = event => {
this.props.onChange(event.target.value)
}
_setText (value) {
this.props.onChange(value)
}
render () {
const { options, ...props } = this.props
props.className = 'form-control'
props.onChange = this._handleChange
const Input = <input {...props} />
if (isEmpty(options)) {
return Input
}
return (
<div className='input-group'>
<div className='input-group-btn'>
<DropdownButton
bsStyle='secondary'
disabled={props.disabled}
id='selectInput'
title=''
>
{map(options, option =>
<MenuItem key={option} onClick={() => this._setText(option)}>
{option}
</MenuItem>
)}
</DropdownButton>
</div>
{Input}
</div>
)
}
}

View File

@@ -1,3 +0,0 @@
.button {
border-radius: 0px;
};

View File

@@ -1,105 +0,0 @@
import map from 'lodash/map'
import React from 'react'
import size from 'lodash/size'
import Component from '../base-component'
import propTypes from '../prop-types'
import { ensureArray } from '../utils'
import {
DropdownButton,
MenuItem
} from 'react-bootstrap-4/lib'
import styles from './index.css'
@propTypes({
defaultValue: propTypes.any,
disabled: propTypes.bool,
max: propTypes.number,
min: propTypes.number,
options: propTypes.oneOfType([
propTypes.arrayOf(propTypes.string),
propTypes.number,
propTypes.objectOf(propTypes.string),
propTypes.string
]),
onChange: propTypes.func,
placeholder: propTypes.string,
required: propTypes.bool,
step: propTypes.any,
type: propTypes.string,
value: propTypes.any
})
export default class Combobox extends Component {
static defaultProps = {
type: 'text'
}
get value () {
return this.refs.input.value
}
set value (value) {
this.refs.input.value = value
}
_handleChange = event => {
const { onChange } = this.props
if (onChange) {
onChange(event.target.value)
}
}
_setText (value) {
this.refs.input.value = value
}
render () {
const { props } = this
const options = ensureArray(props.options)
const Input = (
<input
className='form-control'
defaultValue={props.defaultValue}
disabled={props.disabled}
max={props.max}
min={props.min}
options={options}
onChange={this._handleChange}
placeholder={props.placeholder}
ref='input'
required={props.required}
step={props.step}
type={props.type}
value={props.value}
/>
)
if (!size(options)) {
return Input
}
return (
<div className='input-group'>
<div className='input-group-btn'>
<DropdownButton
bsStyle='secondary'
className={styles.button}
disabled={props.disabled}
id='selectInput'
title=''
>
{map(options, option => (
<MenuItem key={option} onClick={() => this._setText(option)}>
{option}
</MenuItem>
))}
</DropdownButton>
</div>
{Input}
</div>
)
}
}

View File

@@ -294,10 +294,10 @@ export const getPropertyClausesStrings = function () {
export const removePropertyClause = function (name) {
let type
if (
!this ||
(type = this.type) === 'property' && this.name === name
) {
if (!this || (
(type = this.type) === 'property' &&
this.name === name
)) {
return
}
@@ -335,7 +335,7 @@ export const setPropertyClause = function (name, child) {
return _addAndClause(
this,
property,
node => node.type === 'property' && node.name === name,
node => node.type === 'property' && node.name === name
)
}

View File

@@ -1,11 +1,12 @@
import _ from 'intl'
import CopyToClipboard from 'react-copy-to-clipboard'
import classNames from 'classnames'
import Tooltip from 'tooltip'
import React, { createElement } from 'react'
import _ from '../intl'
import Button from '../button'
import Icon from '../icon'
import propTypes from '../prop-types'
import Tooltip from '../tooltip'
import styles from './index.css'
@@ -22,9 +23,9 @@ const Copiable = propTypes({
' ',
<Tooltip content={_('copyToClipboard')}>
<CopyToClipboard text={props.data || props.children}>
<button className={classNames('btn btn-sm btn-secondary', styles.button)}>
<Button className={styles.button} size='small'>
<Icon icon='clipboard' />
</button>
</Button>
</CopyToClipboard>
</Tooltip>
))

View File

@@ -394,7 +394,7 @@ const MAP_TYPE_SELECT = {
value: propTypes.oneOfType([
propTypes.string,
propTypes.object
]).isRequired
])
})
export class XoSelect extends Editable {
get value () {

View File

@@ -12,6 +12,7 @@ import {
MenuItem
} from 'react-bootstrap-4/lib'
import Button from '../button'
import Component from '../base-component'
import getEventValue from '../get-event-value'
import propTypes from '../prop-types'
@@ -70,9 +71,9 @@ export class Password extends Component {
return <div className='input-group'>
{enableGenerator && <span className='input-group-btn'>
<button type='button' className='btn btn-secondary' onClick={this._generate}>
<Button onClick={this._generate}>
<Icon icon='password' />
</button>
</Button>
</span>}
<input
{...props}
@@ -81,9 +82,9 @@ export class Password extends Component {
type={visible ? 'text' : 'password'}
/>
<span className='input-group-btn'>
<button type='button' className='btn btn-secondary' onClick={this._toggleVisibility}>
<Button onClick={this._toggleVisibility}>
<Icon icon={visible ? 'shown' : 'hidden'} />
</button>
</Button>
</span>
</div>
}

View File

@@ -1,4 +1,4 @@
import autoControlledInput from 'auto-controlled-input'
import uncontrollableInput from 'uncontrollable-input'
import Component from 'base-component'
import find from 'lodash/find'
import map from 'lodash/map'
@@ -20,7 +20,7 @@ import Select from './select'
required: propTypes.bool,
value: propTypes.any
})
@autoControlledInput()
@uncontrollableInput()
export default class SelectPlainObject extends Component {
componentDidMount () {
const { options, value } = this.props

View File

@@ -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,27 @@ 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)
}
_onChange = event => this.props.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'
type='checkbox'
/>
</label>

View File

@@ -1,4 +1,9 @@
const common = {
homeFilterNone: ''
}
export const VM = {
...common,
homeFilterPendingVms: 'current_operations:"" ',
homeFilterNonRunningVms: '!power_state:running ',
homeFilterHvmGuests: 'virtualizationMode:hvm ',
@@ -7,18 +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:'
}

View File

@@ -64,6 +64,15 @@ const POOLS_MISSING_PATCHES_COLUMNS = [{
sortCriteria: (host, { pools }) => pools[host.$pool].name_label
}].concat(MISSING_PATCHES_COLUMNS)
// Small component to homogenize Button usage in HostsPatchesTable
const ActionButton_ = ({ children, labelId, ...props }) =>
<ActionButton
{...props}
tooltip={_(labelId)}
>
{children}
</ActionButton>
// ===================================================================
class HostsPatchesTable extends Component {
@@ -150,15 +159,17 @@ class HostsPatchesTable extends Component {
const { props } = this
const Container = props.container || 'div'
const Button = props.useTabButton ? TabButton : ActionButton
const Button = this.props.useTabButton
? TabButton
: ActionButton_
const Buttons = (
<Container>
<Button
btnStyle='secondary'
handler={this._refreshMissingPatches}
icon='refresh'
labelId='refreshPatches'
labelId='checkForUpdates'
/>
<Button
btnStyle='primary'

View File

@@ -1 +0,0 @@
/index.js

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,8 @@ var messages = {
// ----- Filters -----
onError: 'On error',
successful: 'Successful',
filterNoSnapshots: 'Full disks only',
filterOnlySnapshots: 'Snapshots only',
// ----- Copiable component -----
copyToClipboard: 'Copy to clipboard',
@@ -123,6 +125,7 @@ var messages = {
homeAllHosts: 'Hosts',
homeAllTags: 'Tags',
homeNewVm: 'New VM',
homeFilterNone: 'None',
homeFilterRunningHosts: 'Running hosts',
homeFilterDisabledHosts: 'Disabled hosts',
homeFilterRunningVms: 'Running VMs',
@@ -213,7 +216,9 @@ var messages = {
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',
@@ -224,6 +229,8 @@ var messages = {
jobTag: 'Tag',
jobScheduling: 'Scheduling',
jobState: 'State',
jobStateEnabled: 'Enabled',
jobStateDisabled: 'Disabled',
jobTimezone: 'Timezone',
jobServerTimezone: 'Server',
runJob: 'Run job',
@@ -240,6 +247,7 @@ var messages = {
noJobs: 'No jobs found.',
noSchedules: 'No schedules found',
jobActionPlaceHolder: 'Select a xo-server API command',
jobTimeoutPlaceHolder: ' Timeout (number of seconds after which a VM is considered failed)',
jobSchedules: 'Schedules',
jobScheduleNamePlaceHolder: 'Name of your schedule',
jobScheduleJobPlaceHolder: 'Select a Job',
@@ -283,7 +291,10 @@ 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',
@@ -291,11 +302,14 @@ var messages = {
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 *',
@@ -443,6 +457,8 @@ var messages = {
displayAllVMs: 'Display all VMs of this pool',
// ----- Pool tabs -----
hostsTabName: 'Hosts',
vmsTabName: 'Vms',
srsTabName: 'Srs',
// ----- Pool advanced tab -----
poolHaStatus: 'High Availability',
poolHaEnabled: 'Enabled',
@@ -540,6 +556,7 @@ var messages = {
pifStatusDisconnected: 'Disconnected',
pifNoInterface: 'No physical interface detected',
pifInUse: 'This interface is currently in use',
pifAction: 'Action',
defaultLockingMode: 'Default locking mode',
pifConfigureIp: 'Configure IP address',
configIpErrorTitle: 'Invalid parameters',
@@ -552,6 +569,7 @@ var messages = {
addSrDeviceButton: 'Add a storage',
srNameLabel: 'Name',
srType: 'Type',
pbdAction: 'Action',
pbdStatus: 'Status',
pbdStatusConnected: 'Connected',
pbdStatusDisconnected: 'Disconnected',
@@ -580,6 +598,7 @@ var messages = {
// ----- Pool patch tabs -----
refreshPatches: 'Refresh patches',
installPoolPatches: 'Install pool patches',
checkForUpdates: 'Check for updates',
// ----- Pool storage tabs -----
defaultSr: 'Default SR',
setAsDefaultSr: 'Set as default SR',
@@ -674,9 +693,12 @@ var messages = {
vbdDisconnect: 'Disconnect VBD',
vdbBootable: 'Bootable',
vdbReadonly: 'Readonly',
vbdAction: 'Action',
vdbCreate: 'Create',
vdbNamePlaceHolder: 'Disk name',
vdbSizePlaceHolder: 'Size',
cdDriveNotInstalled: 'CD drive not completely installed',
cdDriveInstallation: 'Stop and start the VM to install the CD drive',
saveBootOption: 'Save',
resetBootOption: 'Reset',
@@ -701,6 +723,7 @@ 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 -----
@@ -746,6 +769,8 @@ var messages = {
osKernel: 'OS kernel',
autoPowerOn: 'Auto power on',
ha: 'HA',
vmAffinityHost: 'Affinity host',
noAffinityHost: 'None',
originalTemplate: 'Original template',
unknownOsName: 'Unknown',
unknownOsKernel: 'Unknown',
@@ -855,7 +880,6 @@ var messages = {
newVmAddInterface: 'Add interface',
newVmDisksPanel: 'Disks',
newVmSrLabel: 'SR',
newVmBootableLabel: 'Bootable',
newVmSizeLabel: 'Size',
newVmAddDisk: 'Add disk',
newVmSummaryPanel: 'Summary',
@@ -881,9 +905,11 @@ 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',
@@ -1053,6 +1079,7 @@ 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',
@@ -1062,6 +1089,7 @@ var messages = {
serverPlaceHolderUser: 'username',
serverPlaceHolderPassword: 'password',
serverPlaceHolderAddress: 'address[:port]',
serverPlaceHolderLabel: 'label',
serverConnect: 'Connect',
serverError: 'Error',
serverAddFailed: 'Adding server failed',
@@ -1191,6 +1219,10 @@ var messages = {
disconnectPifConfirm: 'Are you sure you want to disconnect this PIF?',
deletePif: 'Delete PIF',
deletePifConfirm: 'Are you sure you want to delete this PIF?',
pifConnected: 'Connected',
pifDisconnected: 'Disconnected',
pifPhysicallyConnected: 'Physically connected',
pifPhysicallyDisconnected: 'Physically disconnected',
// ----- User -----
username: 'Username',
@@ -1236,6 +1268,8 @@ 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 ------
@@ -1250,6 +1284,7 @@ var messages = {
ipsVifs: 'VIFs',
ipsNotUsed: 'Not used',
ipPoolUnknownVif: 'unknown VIF',
ipPoolNameAlreadyExists: 'Name already exists',
// ----- Shortcuts -----
shortcutModalTitle: 'Keyboard shortcuts',
@@ -1318,13 +1353,11 @@ var messages = {
xosanSelect2Srs: 'Select at least 2 SRs',
xosanLayout: 'Layout',
xosanRedundancy: 'Redundancy',
xosanRedundancyN: 'Redundancy {redundancy}',
xosanCapacity: 'Capacity',
xosanAvailableSpace: 'Available space',
xosanDiskLossLegend: '* Can fail without data loss',
xosanCreate: 'Create XOSAN',
xosanCreate: 'Create',
xosanInstalling: 'Installing XOSAN. Please wait...',
xosanBadVersion: 'You need XenServer 7 to install XOSAN',
xosanCommunity: 'No XOSAN available for Community Edition',
// Pack download modal
xosanInstallCloudPlugin: 'Install cloud plugin first',
@@ -1334,7 +1367,9 @@ var messages = {
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}?'
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) {

View File

@@ -28,7 +28,7 @@ function assertIpv4 (str, msg) {
if (!ipv4.test(str)) { throw new Error(msg) }
}
function *range (ip1, ip2) {
function * range (ip1, ip2) {
assertIpv4(ip1, 'argument "ip1" must be a valid IPv4 address')
assertIpv4(ip2, 'argument "ip2" must be a valid IPv4 address')

View File

@@ -1,8 +1,12 @@
import React from 'react'
import _ from 'intl'
import ActionButton from './action-button'
import Component from './base-component'
import Icon from 'icon'
import propTypes from './prop-types'
import Tooltip from 'tooltip'
import { alert } from 'modal'
import { connectStore } from './utils'
import { SelectVdi } from './select-objects'
import {
@@ -51,8 +55,9 @@ export default class IsoDevice extends Component {
const samePool = vmPool === sr.$pool
return (
samePool && (vmRunning ? sr.shared || sameHost : true) &&
sr.SR_type === 'iso' || sr.SR_type === 'udev' && sr.size
samePool &&
(vmRunning ? sr.shared || sameHost : true) &&
(sr.SR_type === 'iso' || (sr.SR_type === 'udev' && sr.size))
)
}
)
@@ -69,8 +74,10 @@ export default class IsoDevice extends Component {
_handleEject = () => ejectCd(this.props.vm)
_showWarning = () => alert(_('cdDriveNotInstalled'), _('cdDriveInstallation'))
render () {
const { mountedIso } = this.props
const {cdDrive, mountedIso} = this.props
return (
<div className='input-group'>
@@ -81,12 +88,24 @@ export default class IsoDevice extends Component {
/>
<span className='input-group-btn'>
<ActionButton
btnStyle='secondary'
disabled={!mountedIso}
handler={this._handleEject}
icon='vm-eject'
/>
</span>
{mountedIso && !cdDrive.device &&
<Tooltip content={_('cdDriveNotInstalled')}>
<a
className='text-warning btn btn-link'
onClick={this._showWarning}
>
<Icon
icon='alarm'
size='lg'
/>
</a>
</Tooltip>
}
</div>
)
}

View File

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

View File

@@ -1,10 +1,12 @@
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 Button from '../button'
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,175 +14,110 @@ import {
forceDisplayOptionalAttr
} from './helpers'
// ===================================================================
class ArrayItem extends Component {
get value () {
return this.refs.input.value
}
set value (value) {
this.setState({
use: true
}, () => {
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-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._nextChildKey = 0
this.state = {
use: props.required || forceDisplayOptionalAttr(props),
children: this._makeChildren(props)
}
@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, defaultValue) {
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={defaultValue}
/>
</ArrayItem>
)
}
_makeChildren (props) {
return map(props.defaultValue, defaultValue =>
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-right mt-1 mr-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
btnStyle='danger'
className='pull-right'
disabled={disabled}
name={key}
onClick={() => this._onRemoveItem(key)}
>
{_('remove')}
</Button>
</li>
)}
</ul>
<Button
btnStyle='primary'
className='pull-right mt-1 mr-1'
disabled={disabled}
onClick={this._onAddItem}
>
{_('add')}
</Button>
</div>}
</div>
)
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,40 +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}
value={value === undefined ? '' : String(value)}
disabled={disabled}
max={schema.max}
min={schema.min}
onChange={props.onChange}
onChange={this._onChange}
options={schema.defaults}
placeholder={props.placeholder || schema.default}
ref='input'
required={props.required}
placeholder={placeholder}
required={required}
step={1}
type='number'
/>

View File

@@ -1,40 +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}
value={value === undefined ? '' : String(value)}
disabled={disabled}
max={schema.max}
min={schema.min}
onChange={props.onChange}
onChange={this._onChange}
options={schema.defaults}
placeholder={props.placeholder || schema.default}
ref='input'
required={props.required}
placeholder={placeholder}
required={required}
step='any'
type='number'
/>

View File

@@ -1,167 +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='pb-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 = {}) {
this.setState({
use: true
}, () => {
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 != null && 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>
)
}

View File

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

View File

@@ -1,10 +1,11 @@
import _ from 'intl'
import Icon from 'icon'
import isArray from 'lodash/isArray'
import isString from 'lodash/isString'
import React, { Component, cloneElement } from 'react'
import { Button, Modal as ReactModal } from 'react-bootstrap-4/lib'
import { Modal as ReactModal } from 'react-bootstrap-4/lib'
import _ from './intl'
import Button from './button'
import Icon from './icon'
import propTypes from './prop-types'
import {
disable as disableShortcuts,
@@ -75,8 +76,6 @@ class Confirm extends Component {
instance.close()
}
_style = { marginRight: '0.5em' }
render () {
const { Body, Footer, Header, Title } = ReactModal
const { title, icon } = this.props
@@ -97,14 +96,14 @@ class Confirm extends Component {
</Body>
<Footer>
<Button
bsStyle='primary'
btnStyle='primary'
onClick={this._resolve}
style={this._style}
>
{_('confirmOk')}
</Button>
{' '}
<Button
bsStyle='secondary'
onClick={this._reject}
>
{_('confirmCancel')}

View File

@@ -77,6 +77,11 @@ export default class NoVnc extends Component {
_connect = () => {
this._clean()
const { canvas } = this.refs
if (!canvas) {
return
}
const url = parseRelativeUrl(this.props.url)
fixProtocol(url)

View File

@@ -182,6 +182,10 @@ const renderXoItem = (item, {
} = {}) => {
const { id, type, label } = item
if (item.removed) {
return <span key={id} className='text-danger'> <Icon icon='alarm' /> {id}</span>
}
if (!type) {
if (process.env.NODE_ENV !== 'production' && !label) {
throw new Error(`an item must have at least either a type or a label`)

View File

@@ -1,9 +1,6 @@
import classNames from 'classnames'
import Icon from 'icon'
import later from 'later'
import React from 'react'
import Tooltip from 'tooltip'
import { Toggle } from 'form'
import { FormattedDate, FormattedTime } from 'react-intl'
import {
forEach,
@@ -14,12 +11,15 @@ import {
} from 'lodash'
import _ from './intl'
import Button from './button'
import Component from './base-component'
import propTypes from './prop-types'
import TimezonePicker from './timezone-picker'
import Icon from './icon'
import Tooltip from './tooltip'
import { Card, CardHeader, CardBlock } from './card'
import { Col, Row } from './grid'
import { Range } from './form'
import { Range, Toggle } from './form'
// ===================================================================
@@ -259,9 +259,12 @@ class TableSelect extends Component {
))}
</tbody>
</table>
<button className='btn btn-secondary pull-right' onClick={this._reset}>
<Button
className='pull-right'
onClick={this._reset}
>
{_(`selectTableAll${labelId}`)} {value && !value.length && <Icon icon='success' />}
</button>
</Button>
</div>
}
}
@@ -447,23 +450,27 @@ class DayPicker extends Component {
// ===================================================================
@propTypes({
cronPattern: propTypes.string.isRequired,
cronPattern: propTypes.string,
onChange: propTypes.func,
timezone: propTypes.string
timezone: propTypes.string,
value: propTypes.shape({
cronPattern: propTypes.string.isRequired,
timezone: propTypes.string
})
})
export default class Scheduler extends Component {
constructor (props) {
super(props)
this._onCronChange = newCrons => {
const cronPattern = this.props.cronPattern.split(' ')
const cronPattern = this._getCronPattern().split(' ')
forEach(newCrons, (cron, unit) => {
cronPattern[PICKTIME_TO_ID[unit]] = cron
})
this.props.onChange({
cronPattern: cronPattern.join(' '),
timezone: this.props.timezone
timezone: this._getTimezone()
})
}
@@ -475,17 +482,24 @@ export default class Scheduler extends Component {
_onTimezoneChange = timezone => {
this.props.onChange({
cronPattern: this.props.cronPattern,
cronPattern: this._getCronPattern(),
timezone
})
}
_getCronPattern = () => {
const { value, cronPattern = value.cronPattern } = this.props
return cronPattern
}
_getTimezone = () => {
const { value, timezone = value && value.timezone } = this.props
return timezone
}
render () {
const {
cronPattern,
timezone
} = this.props
const cronPatternArr = cronPattern.split(' ')
const cronPatternArr = this._getCronPattern().split(' ')
const timezone = this._getTimezone()
return (
<div className='card-block'>

View File

@@ -1,9 +1,5 @@
import React from 'react'
import classNames from 'classnames'
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,
@@ -26,10 +22,14 @@ import {
} from 'lodash'
import _ from './intl'
import autoControlledInput from './auto-controlled-input'
import Button from './button'
import Component from './base-component'
import Icon from './icon'
import propTypes from './prop-types'
import renderXoItem from './render-xo-item'
import store from './store'
import Tooltip from './tooltip'
import uncontrollableInput from 'uncontrollable-input'
import { Select } from './form'
import {
createCollectionWrapper,
@@ -42,7 +42,6 @@ import {
import {
addSubscriptions,
connectStore,
mapPlus,
resolveResourceSets
} from './utils'
import {
@@ -135,37 +134,6 @@ const options = props => ({
]).isRequired
})
export class GenericSelect extends Component {
componentDidUpdate (prevProps) {
const { onChange, xoObjects } = this.props
if (!onChange || prevProps.xoObjects === xoObjects) {
return
}
const ids = this._getSelectValue()
const objectsById = this._getObjectsById()
if (!isArray(ids)) {
ids && !objectsById[ids] && onChange(undefined)
} else {
let shouldTriggerOnChange
const newValue = isArray(ids) && mapPlus(ids, (id, push) => {
const object = objectsById[id]
if (object) {
push(object)
} else {
shouldTriggerOnChange = true
}
})
if (shouldTriggerOnChange) {
this.props.onChange(newValue)
}
}
}
_getObjectsById = createSelector(
() => this.props.xoObjects,
objects => keyBy(
@@ -180,22 +148,19 @@ export class GenericSelect extends Component {
() => this.props.xoContainers,
() => this.props.xoObjects,
(containers, objects) => { // createCollectionWrapper with a depth?
const __DEV__ = process.env.NODE_ENV !== 'production'
const { name } = this.constructor
let options = []
if (!containers) {
if (__DEV__ && !isArray(objects)) {
throw new Error(`${name}: without xoContainers, xoObjects must be an array`)
}
return map(objects, getOption)
}
if (__DEV__ && isArray(objects)) {
options = map(objects, getOption)
} else if (__DEV__ && isArray(objects)) {
throw new Error(`${name}: with xoContainers, xoObjects must be an object`)
}
const options = []
forEach(containers, container => {
options.push({
disabled: true,
@@ -206,6 +171,30 @@ export class GenericSelect extends Component {
options.push(getOption(object, container))
})
})
const values = this._getSelectValue()
const objectsById = this._getObjectsById()
const addIfMissing = val => {
if (val && !objectsById[val]) {
options.push({
disabled: true,
id: val,
label: val,
value: val,
xoItem: {
id: val,
removed: true
}
})
}
}
if (isArray(values)) {
forEach(values, addIfMissing)
} else {
addIfMissing(values)
}
return options
}
)
@@ -289,7 +278,7 @@ export class GenericSelect extends Component {
{select}
<span className='input-group-btn'>
<Tooltip content={_('selectAll')}>
<Button type='button' bsStyle='secondary' onClick={this._selectAll} style={ADDON_BUTTON_STYLE}>
<Button onClick={this._selectAll} style={ADDON_BUTTON_STYLE}>
<Icon icon='add' />
</Button>
</Tooltip>
@@ -298,7 +287,7 @@ export class GenericSelect extends Component {
}
}
const makeStoreSelect = (createSelectors, defaultProps) => autoControlledInput(options)(
const makeStoreSelect = (createSelectors, defaultProps) => uncontrollableInput(options)(
connectStore(createSelectors)(
props =>
<GenericSelect
@@ -308,7 +297,7 @@ const makeStoreSelect = (createSelectors, defaultProps) => autoControlledInput(o
)
)
const makeSubscriptionSelect = (subscribe, props) => autoControlledInput(options)(
const makeSubscriptionSelect = (subscribe, props) => uncontrollableInput(options)(
class extends Component {
constructor (props) {
super(props)

View File

@@ -347,7 +347,7 @@ export const createSortForType = invoke(() => {
return (type, collection) => createSort(
collection,
autoSelector(type, getIteratees),
autoSelector(type, getOrders),
autoSelector(type, getOrders)
)
})

View File

@@ -12,6 +12,7 @@ import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabric
import DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
import { Portal } from 'react-overlays'
import Button from '../button'
import Component from '../base-component'
import Icon from '../icon'
import propTypes from '../prop-types'
@@ -80,9 +81,9 @@ class TableFilter extends Component {
className='form-control'
/>
<div className='input-group-btn'>
<button className='btn btn-secondary' onClick={this._cleanFilter}>
<Button onClick={this._cleanFilter}>
<Icon icon='clear-search' />
</button>
</Button>
</div>
</div>
)
@@ -93,7 +94,7 @@ class TableFilter extends Component {
@propTypes({
columnId: propTypes.number.isRequired,
name: propTypes.any.isRequired,
name: propTypes.node,
sort: propTypes.func,
sortIcon: propTypes.string
})
@@ -104,10 +105,10 @@ 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>
}
const isSelected = sortIcon === 'asc' || sortIcon === 'desc'
@@ -115,6 +116,7 @@ class ColumnHead extends Component {
return (
<th
className={classNames(
textAlign && `text-xs-${textAlign}`,
styles.clickableColumn,
isSelected && classNames('text-white', 'bg-info')
)}
@@ -141,13 +143,14 @@ 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,
propTypes.string
]),
sortOrder: propTypes.string
sortOrder: propTypes.string,
textAlign: propTypes.string
})).isRequired,
filterContainer: propTypes.func,
filters: propTypes.object,
@@ -302,7 +305,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}
@@ -314,7 +319,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>
))

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

View File

@@ -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 }
)
})()
// ===================================================================

View File

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

View File

View File

@@ -0,0 +1,6 @@
export default {
disabledStateBg: '#fff',
disabledStateColor: '#c0392b',
enabledStateBg: '#fff',
enabledStateColor: '#27ae60'
}

View File

@@ -56,7 +56,7 @@ export default class TimezonePicker extends Component {
}
this.setState({
timezone: option && option.value || SERVER_TIMEZONE_TAG
timezone: (option != null && option.value) || SERVER_TIMEZONE_TAG
}, () =>
this.props.onChange(this.state.timezone === SERVER_TIMEZONE_TAG ? null : this.state.timezone)
)
@@ -81,7 +81,6 @@ export default class TimezonePicker extends Component {
/>
<div className='pull-right'>
<ActionButton
btnStyle='secondary'
handler={this._useLocalTime}
icon='time'
>

View File

@@ -280,8 +280,8 @@ const getParent = (currentTarget) => {
currentParent = currentParent.parentElement
}
const parentTop = currentParent && currentParent.getBoundingClientRect().top || 0
const parentLeft = currentParent && currentParent.getBoundingClientRect().left || 0
const parentTop = currentParent && currentParent.getBoundingClientRect().top
const parentLeft = currentParent && currentParent.getBoundingClientRect().left
return {parentTop, parentLeft}
}

View File

@@ -15,6 +15,7 @@ import mapValues from 'lodash/mapValues'
import React from 'react'
import ReadableStream from 'readable-stream'
import replace from 'lodash/replace'
import startsWith from 'lodash/startsWith'
import { connect } from 'react-redux'
import _ from './intl'
@@ -63,13 +64,22 @@ export const addSubscriptions = subscriptions => Component => {
componentWillMount () {
this._unsubscribes = map(isFunction(subscriptions) ? subscriptions() : subscriptions, (subscribe, prop) =>
subscribe(value => this.setState({ [prop]: value }))
subscribe(value => this._setState({ [prop]: value }))
)
}
componentDidMount () {
this._setState = this.setState
}
componentWillUnmount () {
forEach(this._unsubscribes, unsubscribe => unsubscribe())
this._unsubscribes = null
delete this._setState
}
_setState (nextState) {
this.state = { ...this.state, nextState }
}
render () {
@@ -482,7 +492,35 @@ export const resolveIds = params => {
// ===================================================================
export const compareVersions = (v1, v2) => {
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('.')
@@ -495,4 +533,7 @@ export const compareVersions = (v1, v2) => {
}
return 0
}
})
export const isXosanPack = ({ name }) =>
startsWith(name, 'XOSAN')

View File

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

View File

@@ -16,10 +16,10 @@ export default class HighLevelObjectInput extends XoAbstractInput {
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>
)

View File

@@ -16,10 +16,10 @@ export default class HostInput extends XoAbstractInput {
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>
)

View File

@@ -16,10 +16,10 @@ export default class PoolInput extends XoAbstractInput {
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>
)

View File

@@ -16,10 +16,10 @@ export default class RemoteInput extends XoAbstractInput {
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>
)

View File

@@ -16,10 +16,10 @@ export default class RoleInput extends XoAbstractInput {
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>
)

View File

@@ -16,10 +16,10 @@ export default class SrInput extends XoAbstractInput {
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>
)

View File

@@ -16,10 +16,10 @@ export default class SubjectInput extends XoAbstractInput {
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>
)

View File

@@ -16,10 +16,10 @@ export default class TagInput extends XoAbstractInput {
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>
)

View File

@@ -16,10 +16,10 @@ export default class VmInput extends XoAbstractInput {
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>
)

View File

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

View File

@@ -128,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))
})
}
// -------------------------------------------------------------------
@@ -299,14 +306,14 @@ export const exportConfig = () => (
// Server ------------------------------------------------------------
export const addServer = (host, username, password) => (
_call('server.add', { host, username, password })::tap(
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, { host, username, password, readOnly }) => (
_call('server.set', { id: resolveId(server), host, username, password, readOnly })::tap(
export const editServer = (server, props) => (
_call('server.set', { ...props, id: resolveId(server) })::tap(
subscribeServers.forceRefresh
)
)
@@ -335,7 +342,7 @@ export const editPool = (pool, props) => (
_call('pool.set', { id: resolveId(pool), ...props })
)
import AddHostModalBody from './add-host-modal'
import AddHostModalBody from './add-host-modal' // eslint-disable-line import/first
export const addHostToPool = (pool, host) => {
if (host) {
return confirm({
@@ -636,7 +643,7 @@ export const cloneVm = ({ id, name_label: nameLabel }, fullCopy = false) => (
})
)
import CopyVmModalBody from './copy-vm-modal'
import CopyVmModalBody from './copy-vm-modal' // eslint-disable-line import/first
export const copyVm = (vm, sr, name, compress) => {
if (sr) {
return confirm({
@@ -660,7 +667,7 @@ export const copyVm = (vm, sr, name, compress) => {
}
}
import CopyVmsModalBody from './copy-vms-modal'
import CopyVmsModalBody from './copy-vms-modal' // eslint-disable-line import/first
export const copyVms = vms => {
const _vms = resolveIds(vms)
return confirm({
@@ -678,7 +685,7 @@ export const copyVms = vms => {
sr
} = params
Promise.all(map(_vms, (vm, index) =>
_call('vm.copy', { vm, sr, compress, name: names[index] }),
_call('vm.copy', { vm, sr, compress, name: names[index] })
))
},
noop
@@ -732,7 +739,7 @@ export const deleteSnapshot = vm => (
)
)
import MigrateVmModalBody from './migrate-vm-modal'
import MigrateVmModalBody from './migrate-vm-modal' // eslint-disable-line import/first
export const migrateVm = (vm, host) => (
confirm({
title: _('migrateVmModalTitle'),
@@ -748,7 +755,7 @@ export const migrateVm = (vm, host) => (
)
)
import MigrateVmsModalBody from './migrate-vms-modal'
import MigrateVmsModalBody from './migrate-vms-modal' // eslint-disable-line import/first
export const migrateVms = vms => (
confirm({
title: _('migrateVmModalTitle'),
@@ -831,7 +838,7 @@ export const importDeltaBackup = ({ remote, file, sr }) => (
_call('vm.importDeltaBackup', resolveIds({ remote, filePath: file, sr }))
)
import RevertSnapshotModalBody from './revert-snapshot-modal'
import RevertSnapshotModalBody from './revert-snapshot-modal' // eslint-disable-line import/first
export const revertSnapshot = vm => (
confirm({
title: _('revertVmModalTitle'),
@@ -904,7 +911,7 @@ export const attachDiskToVm = (vdi, vm, { bootable, mode, position }) => (
_call('vm.attachDisk', {
bootable,
mode,
position: position && String(position) || undefined,
position: (position && String(position)) || undefined,
vdi: resolveId(vdi),
vm: resolveId(vm)
})
@@ -1003,7 +1010,7 @@ export const editNetwork = (network, props) => (
_call('network.set', { ...props, id: resolveId(network) })
)
import CreateNetworkModalBody from './create-network-modal'
import CreateNetworkModalBody from './create-network-modal' // eslint-disable-line import/first
export const createNetwork = container => (
confirm({
icon: 'network',
@@ -1023,7 +1030,7 @@ export const createNetwork = container => (
export const getBondModes = () =>
_call('network.getBondModes')
import CreateBondedNetworkModalBody from './create-bonded-network-modal'
import CreateBondedNetworkModalBody from './create-bonded-network-modal' // eslint-disable-line import/first
export const createBondedNetwork = container => (
confirm({
icon: 'network',
@@ -1314,7 +1321,7 @@ export const loadPlugin = async id => (
_call('plugin.load', { id })::tap(
subscribePlugins.forceRefresh
)::rethrow(
err => error(_('pluginError'), err && err.message || _('unknownPluginError'))
err => error(_('pluginError'), (err && err.message) || _('unknownPluginError'))
)
)
@@ -1322,7 +1329,7 @@ export const unloadPlugin = id => (
_call('plugin.unload', { id })::tap(
subscribePlugins.forceRefresh
)::rethrow(
err => error(_('pluginError'), err && err.message || _('unknownPluginError'))
err => error(_('pluginError'), (err && err.message) || _('unknownPluginError'))
)
)
@@ -1338,7 +1345,7 @@ export const disablePluginAutoload = id => (
)
)
export const configurePlugin = (id, configuration) => {
export const configurePlugin = (id, configuration) =>
_call('plugin.configure', { id, configuration })::tap(
() => {
info(_('pluginConfigurationSuccess'), _('pluginConfigurationChanges'))
@@ -1347,7 +1354,6 @@ export const configurePlugin = (id, configuration) => {
)::rethrow(
err => error(_('pluginError'), JSON.stringify(err.data) || _('unknownPluginError'))
)
}
export const purgePluginConfiguration = async id => {
await confirm({
@@ -1661,10 +1667,10 @@ const _setUserPreferences = preferences => (
)
)
import NewSshKeyModalBody from './new-ssh-key-modal'
import NewSshKeyModalBody from './new-ssh-key-modal' // eslint-disable-line import/first
export const addSshKey = key => {
const { preferences } = xo.user
const otherKeys = preferences && preferences.sshKeys || []
const otherKeys = (preferences && preferences.sshKeys) || []
if (key) {
return _setUserPreferences({ sshKeys: [
...otherKeys,
@@ -1707,7 +1713,7 @@ export const deleteSshKey = key => (
// User filters --------------------------------------------------
import AddUserFilterModalBody from './add-user-filter-modal'
import AddUserFilterModalBody from './add-user-filter-modal' // eslint-disable-line import/first
export const addCustomFilter = (type, value) => {
const { user } = xo
return confirm({
@@ -1817,7 +1823,7 @@ export const createXosanSR = ({ template, pif, vlan, srs, glusterType, redundanc
export const computeXosanPossibleOptions = lvmSrs => _call('xosan.computeXosanPossibleOptions', { lvmSrs })
import InstallXosanPackModal from './install-xosan-pack-modal'
import InstallXosanPackModal from './install-xosan-pack-modal' // eslint-disable-line import/first
export const downloadAndInstallXosanPack = pool =>
confirm({
title: _('xosanInstallPackTitle', { pool: pool.name_label }),

View File

@@ -1,32 +1,53 @@
import _ from 'intl'
import Component from 'base-component'
import React from 'react'
import { connectStore, compareVersions } from 'utils'
import { connectStore, compareVersions, isXosanPack } from 'utils'
import { subscribeResourceCatalog, subscribePlugins } from 'xo'
import { createGetObjectsOfType, createSelector } from 'selectors'
import { createGetObjectsOfType, createSelector, createCollectionWrapper } from 'selectors'
import { satisfies as versionSatisfies } from 'semver'
import {
every,
filter,
forEach,
map
map,
some
} from 'lodash'
const findLatestPack = packs => {
let latestPack = packs[0]
const findLatestPack = (packs, hostsVersions) => {
const checkVersion = version =>
every(hostsVersions, hostVersion => versionSatisfies(hostVersion, version))
let latestPack = { version: '0' }
forEach(packs, pack => {
if (compareVersions(pack.version, latestPack.version) > 0) {
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({
@connectStore(() => ({
hosts: createGetObjectsOfType('host').filter(
(_, { pool }) => host => pool && host.$pool === pool.id && !host.supplementalPacks['vates:XOSAN']
createSelector(
(_, { pool }) => pool != null && pool.id,
poolId => poolId
? host => host.$pool === poolId && !some(host.supplementalPacks, isXosanPack)
: false
)
)
}, { withRef: true })
}), { withRef: true })
export default class InstallXosanPackModal extends Component {
componentDidMount () {
this._unsubscribePlugins = subscribePlugins(plugins => this.setState({ plugins }))
@@ -40,9 +61,16 @@ export default class InstallXosanPackModal extends Component {
_getXosanLatestPack = createSelector(
() => this.state.catalog && this.state.catalog.xosan,
xosanCatalog => findLatestPack(
filter(xosanCatalog, (value, key) => key !== '_token' && value.type === 'iso')
)
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 () {
@@ -54,13 +82,28 @@ export default class InstallXosanPackModal extends Component {
const latestPack = this._getXosanLatestPack()
return <div>
{_('xosanInstallPackOnHosts')}
<ul>
{map(hosts, host => <li key={host.id}>{host.name_label}</li>)}
</ul>
{latestPack && <div className='mt-1'>
{_('xosanInstallPack', { pack: latestPack.name, version: latestPack.version })}
</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>
}
}

View File

@@ -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 (networks[pif.$network].name_label === nameLabel) {
defaultNetwork = pif.$network
return false
}
if (vlan !== -1 && includes(originVlans, vlan)) {
destNetworkId = $network
}
})
return defaultNetwork
return destNetworkId
}

View File

@@ -240,7 +240,7 @@ class XoaUpdater extends EventEmitter {
this.registerState = 'error'
}
} finally {
this.emit('registerState', {state: this.registerState, email: this.token && this.token.registrationEmail || '', error: this.registerError})
this.emit('registerState', {state: this.registerState, email: (this.token && this.token.registrationEmail) || '', error: this.registerError})
}
}
@@ -262,7 +262,7 @@ class XoaUpdater extends EventEmitter {
this.registerState = 'error'
}
} finally {
this.emit('registerState', {state: this.registerState, email: this.token && this.token.registrationEmail || '', error: this.registerError})
this.emit('registerState', {state: this.registerState, email: (this.token && this.token.registrationEmail) || '', error: this.registerError})
if (this.registerState === 'registered') {
this.update()
}
@@ -351,7 +351,7 @@ class XoaUpdater extends EventEmitter {
}
log (level, message) {
message = message && message.message || String(message)
message = (message != null && message.message) || String(message)
const date = new Date()
this._log.unshift({
date: date.toLocaleString(),

View File

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

View File

@@ -268,7 +268,7 @@ export default class RestoreFileModalBody extends Component {
value={partition}
/>
]}
{(partition || disk && !scanDiskError && noPartitions) && [
{(partition || (disk && !scanDiskError && noPartitions)) && [
<br />,
<Container>
<Row>
@@ -280,7 +280,7 @@ export default class RestoreFileModalBody extends Component {
<Col size={2}>
<span className='pull-right'>
<Tooltip content={_('restoreFilesSelectAllFiles')}>
<ActionButton btnStyle='secondary' handler={this._selectAllFolderFiles} icon='add' size='small' />
<ActionButton handler={this._selectAllFolderFiles} icon='add' size='small' />
</Tooltip>
</span>
</Col>
@@ -322,12 +322,13 @@ export default class RestoreFileModalBody extends Component {
<Col className='pl-0 pb-1' size={10}>
<em>{_('restoreFilesSelectedFiles', { files: selectedFiles.length })}</em>
</Col>
<Col size={2}>
<span className='pull-right'>
<Tooltip content={_('restoreFilesUnselectAll')}>
<ActionButton btnStyle='secondary' handler={this._unselectAllFiles} icon='remove' size='small' />
</Tooltip>
</span>
<Col size={2} className='text-xs-right'>
<ActionButton
handler={this._unselectAllFiles}
icon='remove'
size='small'
tooltip={_('restoreFilesUnselectAll')}
/>
</Col>
</Row>
{map(selectedFiles, file =>
@@ -335,10 +336,8 @@ export default class RestoreFileModalBody extends Component {
<Col size={10}>
<pre>{file.path}</pre>
</Col>
<Col size={2}>
<span className='pull-right'>
<ActionButton btnStyle='secondary' handler={this._unselectFile} handlerParam={file} icon='remove' size='small' />
</span>
<Col size={2} className='text-xs-right'>
<ActionButton handler={this._unselectFile} handlerParam={file} icon='remove' size='small' />
</Col>
</Row>
)}

View File

@@ -1,32 +1,39 @@
import _ from 'intl'
import ActionButton from 'action-button'
import Button from 'button'
import Component from 'base-component'
import delay from 'lodash/delay'
import forEach from 'lodash/forEach'
import GenericInput from 'json-schema-input'
import getEventValue from 'get-event-value'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import moment from 'moment-timezone'
import React from 'react'
import Scheduler, { SchedulePreview } from 'scheduling'
import startsWith from 'lodash/startsWith'
import uncontrollableInput from 'uncontrollable-input'
import Upgrade from 'xoa-upgrade'
import Wizard, { Section } from 'wizard'
import { addSubscriptions } from 'utils'
import { confirm } from 'modal'
import { error } from 'notification'
import { generateUiSchema } from 'xo-json-schema-input'
import { SelectSubject } from 'select-objects'
import { connectStore, EMPTY_OBJECT } from 'utils'
import { Container, Row, Col } from 'grid'
import { createSelector } from 'reselect'
import { generateUiSchema } from 'xo-json-schema-input'
import { getUser } from 'selectors'
import { SelectSubject } from 'select-objects'
import {
forEach,
identity,
isArray,
map,
mapValues,
noop,
startsWith
} from 'lodash'
import {
createJob,
createSchedule,
getRemote,
editJob,
editSchedule,
subscribeCurrentUser
editSchedule
} from 'xo'
// ===================================================================
@@ -52,13 +59,13 @@ const NO_SMART_UI_SCHEMA = generateUiSchema(NO_SMART_SCHEMA)
const SMART_SCHEMA = {
type: 'object',
properties: {
status: {
power_state: {
default: 'All', // FIXME: can't translate
enum: [ 'All', 'Running', 'Halted' ], // FIXME: can't translate
title: _('editBackupSmartStatusTitle'),
description: 'The statuses of VMs to backup.' // FIXME: can't translate
},
poolsOptions: {
$pool: {
type: 'object',
title: _('editBackupSmartPools'),
properties: {
@@ -67,7 +74,7 @@ const SMART_SCHEMA = {
title: _('editBackupNot'),
description: 'Toggle on to backup VMs that are NOT resident on these pools'
},
pools: {
values: {
type: 'array',
items: {
type: 'string',
@@ -78,7 +85,7 @@ const SMART_SCHEMA = {
}
}
},
tagsOptions: {
tags: {
type: 'object',
title: _('editBackupSmartTags'),
properties: {
@@ -87,7 +94,7 @@ const SMART_SCHEMA = {
title: _('editBackupNot'),
description: 'Toggle on to backup VMs that do NOT contain these tags'
},
tags: {
values: {
type: 'array',
items: {
type: 'string',
@@ -99,7 +106,7 @@ const SMART_SCHEMA = {
}
}
},
required: [ 'status', 'poolsOptions', 'tagsOptions' ]
required: [ 'power_state', '$pool', 'tags' ]
}
const SMART_UI_SCHEMA = generateUiSchema(SMART_SCHEMA)
@@ -114,9 +121,14 @@ const COMMON_SCHEMA = {
description: 'Back-up tag.' // FIXME: can't translate
},
_reportWhen: {
default: 'failure',
enum: [ 'never', 'always', 'failure' ], // FIXME: can't translate
title: _('editBackupReportTitle'),
description: 'When to send reports.' // FIXME: can't translate
description: [
'When to send reports.',
'',
'Plugins *tranport-email* and *backup-reports* need to be configured.'
].join('\n')
},
enabled: {
type: 'boolean',
@@ -256,183 +268,177 @@ const BACKUP_METHOD_TO_INFO = {
// ===================================================================
const DEFAULT_CRON_PATTERN = '0 0 * * *'
@uncontrollableInput()
class TimeoutInput extends Component {
_onChange = event => {
const value = getEventValue(event).trim()
this.props.onChange(value === '' ? null : +value * 1e3)
}
function negatePattern (pattern, not = true) {
render () {
const { props } = this
const { value } = props
return <input
{...props}
onChange={this._onChange}
type='number'
value={value == null ? '' : String(value / 1e3)}
/>
}
}
// ===================================================================
const DEFAULT_CRON_PATTERN = '0 0 * * *'
const DEFAULT_TIMEZONE = moment.tz.guess()
// xo-web v5.7.1 introduced a bug where an extra level
// ({ id: { id: <id> } }) was introduced for the VM param.
//
// This code automatically unbox the ids.
const extractId = value => {
while (typeof value === 'object') {
value = value.id
}
return value
}
const destructPattern = (pattern, valueTransform = identity) => pattern && ({
not: !!pattern.__not,
values: valueTransform((pattern.__not || pattern).__or)
})
const constructPattern = ({ not, values } = EMPTY_OBJECT, valueTransform = identity) => {
if (values == null || !values.length) {
return
}
const pattern = { __or: valueTransform(values) }
return not
? { __not: pattern }
: pattern
}
@addSubscriptions({
currentUser: subscribeCurrentUser
@connectStore({
currentUser: getUser
})
export default class New extends Component {
constructor (props) {
super(props)
this.state.cronPattern = DEFAULT_CRON_PATTERN
}
componentWillReceiveProps (props) {
const { currentUser } = props
const { owner } = this.state
if (currentUser && !owner) {
this.setState({ owner: currentUser.id })
}
}
componentWillMount () {
const { job, schedule } = this.props
if (!job || !schedule) {
if (job || schedule) { // Having only one of them is unexpected incomplete information
error(_('backupEditNotFoundTitle'), _('backupEditNotFoundMessage'))
_getParams = createSelector(
() => this.props.job,
job => {
if (!job) {
return { main: {}, vms: { vms: [] } }
}
this.setState({
timezone: moment.tz.guess()
})
return
}
this.setState({
backupInfo: BACKUP_METHOD_TO_INFO[job.method],
cronPattern: schedule.cron,
owner: job.userId,
timezone: schedule.timezone || null
}, () => delay(this._populateForm, 250, job)) // Work around.
// Without the delay, some selects are not always ready to load a value
// Values are displayed, but html5 compliant browsers say the value is required and empty on submit
}
_populateForm = job => {
let values = job.paramsVector.items
const {
backupInput,
vmsInput
} = this.refs
const { items } = job.paramsVector
if (values.length === 1) {
// Older versions of XenOrchestra uses only values[0].
const array = values[0].values
const config = array[0]
const reportWhen = config._reportWhen
// legacy backup jobs
if (items.length === 1) {
const { ...main } = items[0].values[0]
backupInput.value = {
...config,
_reportWhen:
// Fix old reportWhen values...
(reportWhen === 'fail' && 'failure') ||
(reportWhen === 'alway' && 'always') ||
reportWhen
return {
main,
vms: { vms: map(items[0].values.slice(1), extractId) }
}
}
vmsInput.value = { vms: map(array, ({ id, vm }) => id || vm) }
} else {
if (values[1].type === 'map') {
// Smart backup.
const {
$pool: poolsOptions = {},
tags: tagsOptions = {},
power_state: status = 'All'
} = values[1].collection.pattern
backupInput.value = values[0].values[0]
// smart backup
if (items[1].type === 'map') {
const { pattern } = items[1].collection
const { $pool, tags } = pattern
this.setState({
smartBackupMode: true
}, () => {
vmsInput.value = {
poolsOptions: {
pools: poolsOptions.__not ? poolsOptions.__not.__or : poolsOptions.__or,
not: !!poolsOptions.__not
},
status,
tagsOptions: {
tags: map(tagsOptions.__not ? tagsOptions.__not.__or : tagsOptions.__or, tag => tag[0]),
not: !!tagsOptions.__not
}
return {
main: items[0].values[0],
vms: {
$pool: destructPattern($pool),
power_state: pattern.power_state,
tags: destructPattern(tags, tags => map(tags, tag => isArray(tag) ? tag[0] : tag))
}
})
} else {
// Normal backup.
backupInput.value = values[1].values[0]
vmsInput.value = { vms: values[0].values }
}
}
// normal backup
return {
main: items[1].values[0],
vms: { vms: map(items[0].values, extractId) }
}
}
}
)
_getMainParams = () => this.state.mainParams || this._getParams().main
_getVmsParam = () => this.state.vmsParam || this._getParams().vms
_getScheduling = createSelector(
() => this.props.schedule,
() => this.state.scheduling,
(schedule, scheduling) => {
if (scheduling !== undefined) {
return scheduling
}
const {
cron = DEFAULT_CRON_PATTERN,
timezone = DEFAULT_TIMEZONE
} = schedule || EMPTY_OBJECT
return {
cronPattern: cron,
timezone
}
}
)
_handleSubmit = async () => {
const { props, state } = this
const method = this._getValue('job', 'method')
const backupInfo = BACKUP_METHOD_TO_INFO[method]
const {
enabled,
...callArgs
} = this.refs.backupInput.value
const vmsInputValue = this.refs.vmsInput.value
const {
backupInfo,
smartBackupMode,
timezone,
owner
} = this.state
const { pools, not: notPools } = vmsInputValue.poolsOptions || {}
const { tags, not: notTags } = vmsInputValue.tagsOptions || {}
const formattedTags = map(tags, tag => [ tag ])
const paramsVector = !smartBackupMode
? {
type: 'crossProduct',
items: [{
type: 'set',
values: map(vmsInputValue.vms, vm => ({ id: vm }))
}, {
type: 'set',
values: [ callArgs ]
}]
} : {
type: 'crossProduct',
items: [{
type: 'set',
values: [ callArgs ]
}, {
type: 'map',
collection: {
type: 'fetchObjects',
pattern: {
$pool: isEmpty(pools)
? undefined
: negatePattern({ __or: pools }, notPools),
power_state: vmsInputValue.status === 'All' ? undefined : vmsInputValue.status,
tags: isEmpty(tags)
? undefined
: negatePattern({ __or: formattedTags }, notTags),
type: 'VM'
}
},
iteratee: {
type: 'extractProperties',
mapping: { id: 'id' }
}
}]
}
...mainParams
} = this._getMainParams()
const vms = this._getVmsParam()
const job = {
...state.job,
type: 'call',
key: backupInfo.jobKey,
method: backupInfo.method,
paramsVector,
userId: owner
paramsVector: {
type: 'crossProduct',
items: isArray(vms.vms)
? [{
type: 'set',
values: map(vms.vms, vm => ({ id: extractId(vm) }))
}, {
type: 'set',
values: [ mainParams ]
}]
: [{
type: 'set',
values: [ mainParams ]
}, {
type: 'map',
collection: {
type: 'fetchObjects',
pattern: {
$pool: constructPattern(vms.$pool),
power_state: vms.power_state === 'All' ? undefined : vms.power_state,
tags: constructPattern(vms.tags, tags => map(tags, tag => [ tag ])),
type: 'VM'
}
},
iteratee: {
type: 'extractProperties',
mapping: { id: 'id' }
}
}]
}
}
// Update backup schedule.
const { job: oldJob, schedule: oldSchedule } = this.props
if (oldJob && oldSchedule) {
job.id = oldJob.id
return editJob(job).then(() => editSchedule({
...oldSchedule,
cron: this.state.cronPattern,
timezone
}))
}
const scheduling = this._getScheduling()
let remoteId
if (job.type === 'call') {
@@ -465,57 +471,79 @@ export default class New extends Component {
}
}
// Update backup schedule.
const oldJob = props.job
if (oldJob) {
job.id = oldJob.id
await editJob(job)
return editSchedule({
id: props.schedule.id,
cron: scheduling.cronPattern,
timezone: scheduling.timezone
})
}
if (job.timeout === null) {
delete job.timeout // only needed for job edition
}
// Create backup schedule.
return createSchedule(await createJob(job), { cron: this.state.cronPattern, enabled, timezone })
return createSchedule(await createJob(job), {
cron: scheduling.cronPattern,
enabled,
timezone: scheduling.timezone
})
}
_handleReset = () => {
const { backupInput } = this.refs
if (backupInput) {
backupInput.value = undefined
}
this.setState({
cronPattern: DEFAULT_CRON_PATTERN
})
}
_updateCronPattern = value => {
this.setState(value)
}
_handleBackupSelection = event => {
const method = event.target.value
this.setState({
showVersionWarning: method === 'vm.rollingDeltaBackup' || method === 'vm.deltaCopy',
backupInfo: BACKUP_METHOD_TO_INFO[method]
})
this.setState(mapValues(this.state, noop))
}
_handleSmartBackupMode = event => {
this.setState({
smartBackupMode: event.target.value === 'smart'
})
this.setState(
event.target.value === 'smart'
? { vmsParam: {} }
: { vmsParam: { vms: [] } }
)
}
_subjectPredicate = ({ type, permission }) =>
type === 'user' && permission === 'admin'
render () {
const {
backupInfo,
cronPattern,
smartBackupMode,
timezone,
owner,
showVersionWarning
} = this.state
_getValue = (ns, key, defaultValue) => {
let tmp
return process.env.XOA_PLAN > 1
? (
<Wizard>
// look in the state
if (
(tmp = this.state[ns]) != null &&
(tmp = tmp[key]) !== undefined
) {
return tmp
}
// look in the props
if (
(tmp = this.props[ns]) != null &&
(tmp = tmp[key]) !== undefined
) {
return tmp
}
return defaultValue
}
render () {
const method = this._getValue('job', 'method', '')
const scheduling = this._getScheduling()
const vms = this._getVmsParam()
const backupInfo = BACKUP_METHOD_TO_INFO[method]
const smartBackupMode = !isArray(vms.vms)
return (
<Upgrade place='newBackup' required={2}>
<Wizard><form id='form-new-vm-backup'>
<Section icon='backup' title={this.props.job ? 'editVmBackup' : 'newVmBackup'}>
<Container>
<Row>
@@ -523,88 +551,96 @@ export default class New extends Component {
<fieldset className='form-group'>
<label>{_('backupOwner')}</label>
<SelectSubject
onChange={this.linkState('owner', 'id')}
onChange={this.linkState('job.userId', 'id')}
predicate={this._subjectPredicate}
required
value={owner || null}
value={this._getValue('job', 'userId', this.props.currentUser.id)}
/>
</fieldset>
<fieldset className='form-group'>
<label>{_('jobTimeoutPlaceHolder')}</label>
<TimeoutInput
className='form-control'
onChange={this.linkState('job.timeout')}
value={this._getValue('job', 'timeout')}
/>
</fieldset>
<fieldset className='form-group'>
<label htmlFor='selectBackup'>{_('newBackupSelection')}</label>
<select
className='form-control'
value={(backupInfo && backupInfo.method) || ''}
id='selectBackup'
onChange={this._handleBackupSelection}
onChange={this.linkState('job.method')}
required
value={method}
>
{_('noSelectedValue', message => <option value=''>{message}</option>)}
{map(BACKUP_METHOD_TO_INFO, (info, key) =>
_(info.label, message => <option key={key} value={key}>{message}</option>)
_(info.label, message => <option key={key} value={key}>{message}</option>)
)}
</select>
</fieldset>
{showVersionWarning && <div className='alert alert-warning' role='alert'>
{(method === 'vm.rollingDeltaBackup' || method === 'vm.deltaCopy') && <div className='alert alert-warning' role='alert'>
<Icon icon='error' /> {_('backupVersionWarning')}
</div>}
<form id='form-new-vm-backup'>
{backupInfo && <div>
<GenericInput
label={<span><Icon icon={backupInfo.icon} /> {_(backupInfo.label)}</span>}
ref='backupInput'
{backupInfo && <div>
<GenericInput
label={<span><Icon icon={backupInfo.icon} /> {_(backupInfo.label)}</span>}
required
schema={backupInfo.schema}
uiSchema={backupInfo.uiSchema}
onChange={this.linkState('mainParams')}
value={this._getMainParams()}
/>
<fieldset className='form-group'>
<label htmlFor='smartMode'>{_('smartBackupModeSelection')}</label>
<select
className='form-control'
id='smartMode'
onChange={this._handleSmartBackupMode}
required
schema={backupInfo.schema}
uiSchema={backupInfo.uiSchema}
/>
<fieldset className='form-group'>
<label htmlFor='smartMode'>{_('smartBackupModeSelection')}</label>
<select
className='form-control'
id='smartMode'
onChange={this._handleSmartBackupMode}
required
value={smartBackupMode ? 'smart' : 'normal'}
>
{_('normalBackup', message => <option value='normal'>{message}</option>)}
{_('smartBackup', message => <option value='smart'>{message}</option>)}
</select>
</fieldset>
{smartBackupMode
? (process.env.XOA_PLAN > 2
? <GenericInput
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
ref='vmsInput'
required
schema={SMART_SCHEMA}
uiSchema={SMART_UI_SCHEMA}
/>
: <Container><Upgrade place='newBackup' available={3} /></Container>
) : <GenericInput
value={smartBackupMode ? 'smart' : 'normal'}
>
{_('normalBackup', message => <option value='normal'>{message}</option>)}
{_('smartBackup', message => <option value='smart'>{message}</option>)}
</select>
</fieldset>
{smartBackupMode
? <Upgrade place='newBackup' required={3}>
<GenericInput
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
ref='vmsInput'
onChange={this.linkState('vmsParam')}
required
schema={NO_SMART_SCHEMA}
uiSchema={NO_SMART_UI_SCHEMA}
/>
}
</div>}
</form>
schema={SMART_SCHEMA}
uiSchema={SMART_UI_SCHEMA}
value={vms}
/>
</Upgrade>
: <GenericInput
label={<span><Icon icon='vm' /> {_('vmsToBackup')}</span>}
onChange={this.linkState('vmsParam')}
required
schema={NO_SMART_SCHEMA}
uiSchema={NO_SMART_UI_SCHEMA}
value={vms}
/>
}
</div>}
</Col>
</Row>
</Container>
</Section>
<Section icon='schedule' title='schedule'>
<Scheduler
cronPattern={cronPattern}
onChange={this._updateCronPattern}
timezone={timezone}
onChange={this.linkState('scheduling')}
value={scheduling}
/>
</Section>
<Section icon='preview' title='preview' summary>
<Container>
<Row>
<Col>
<SchedulePreview cronPattern={cronPattern} />
<SchedulePreview cronPattern={scheduling.cronPattern} />
{process.env.XOA_PLAN < 4 && backupInfo && process.env.XOA_PLAN < REQUIRED_XOA_PLAN[backupInfo.jobKey]
? <Upgrade place='newBackup' available={REQUIRED_XOA_PLAN[backupInfo.jobKey]} />
: (smartBackupMode && process.env.XOA_PLAN < 3
@@ -612,26 +648,27 @@ export default class New extends Component {
: <fieldset className='pull-right pt-1'>
<ActionButton
btnStyle='primary'
className='btn-lg mr-1'
className='mr-1'
disabled={!backupInfo}
form='form-new-vm-backup'
handler={this._handleSubmit}
icon='save'
redirectOnSuccess='/backup/overview'
size='large'
>
{_('saveBackupJob')}
</ActionButton>
<button type='button' className='btn btn-lg btn-secondary' onClick={this._handleReset}>
<Button onClick={this._handleReset} size='large'>
{_('selectTableReset')}
</button>
</Button>
</fieldset>)
}
</Col>
</Row>
</Container>
</Section>
</Wizard>
)
: <Container><Upgrade place='newBackup' available={2} /></Container>
</form></Wizard>
</Upgrade>
)
}
}

View File

@@ -1,6 +1,6 @@
import _ from 'intl'
import ActionRowButton from 'action-row-button'
import ActionToggle from 'action-toggle'
import ButtonGroup from 'button-group'
import Component from 'base-component'
import filter from 'lodash/filter'
import find from 'lodash/find'
@@ -13,9 +13,9 @@ import map from 'lodash/map'
import orderBy from 'lodash/orderBy'
import React from 'react'
import SortedTable from 'sorted-table'
import StateButton from 'state-button'
import Tooltip from 'tooltip'
import { addSubscriptions } from 'utils'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { createSelector } from 'selectors'
import {
Card,
@@ -45,13 +45,20 @@ const jobKeyToLabel = {
const JOB_COLUMNS = [
{
name: _('job'),
itemRenderer: ({ jobId, jobLabel }) => <span>{jobId.slice(4, 8)} ({jobLabel})</span>,
name: _('jobId'),
itemRenderer: ({ jobId }) => jobId.slice(4, 8),
sortCriteria: 'jobId'
},
{
name: _('jobType'),
itemRenderer: ({ jobLabel }) => jobLabel,
sortCriteria: 'jobLabel'
},
{
name: _('jobTag'),
itemRenderer: ({ scheduleTag }) => scheduleTag
itemRenderer: ({ scheduleTag }) => scheduleTag,
default: true,
sortCriteria: ({ scheduleTag }) => scheduleTag
},
{
name: _('jobScheduling'),
@@ -65,16 +72,23 @@ const JOB_COLUMNS = [
},
{
name: _('jobState'),
itemRenderer: ({ schedule, scheduleToggleValue }) => <ActionToggle
value={scheduleToggleValue}
handler={scheduleToggleValue ? disableSchedule : enableSchedule}
itemRenderer: ({ schedule, scheduleToggleValue }) => <StateButton
disabledLabel={_('jobStateDisabled')}
disabledHandler={enableSchedule}
disabledTooltip={_('logIndicationToEnable')}
enabledLabel={_('jobStateEnabled')}
enabledHandler={disableSchedule}
enabledTooltip={_('logIndicationToDisable')}
handlerParam={schedule.id}
size='small'
state={scheduleToggleValue}
/>,
sortCriteria: 'scheduleToggleValue'
},
{
itemRenderer: ({ schedule }, isScheduleUserMissing) => <fieldset className='pull-right'>
name: _('jobAction'),
itemRenderer: ({ schedule }, isScheduleUserMissing) => <fieldset>
{!isScheduleUserMissing[schedule.id] && <Tooltip content={_('backupUserNotFound')}><Icon className='mr-1' icon='error' /></Tooltip>}
<Link className='btn btn-sm btn-primary mr-1' to={`/backup/${schedule.id}/edit`}>
<Icon icon='edit' />
@@ -94,7 +108,8 @@ const JOB_COLUMNS = [
handlerParam={schedule.job}
/>
</ButtonGroup>
</fieldset>
</fieldset>,
textAlign: 'right'
}
]

View File

@@ -1,4 +1,5 @@
import _ from 'intl'
import ButtonGroup from 'button-group'
import ChartistGraph from 'react-chartist'
import Component from 'base-component'
import forEach from 'lodash/forEach'
@@ -10,7 +11,6 @@ import HostsPatchesTable from 'hosts-patches-table'
import React from 'react'
import size from 'lodash/size'
import Upgrade from 'xoa-upgrade'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { Card, CardBlock, CardHeader } from 'card'
import { Container, Row, Col } from 'grid'
import {

View File

@@ -305,32 +305,22 @@ class SelectMetric extends Component {
/>
</div>
<div className='btn-group mt-1' role='group'>
<button
className='btn btn-secondary'
onClick={this._resetSelection}
tooltip={_('dashboardStatsButtonRemoveAll')}
type='button'
>
<Icon icon='remove' />
</button>
<button
className='btn btn-secondary'
onClick={this._selectAllHosts}
tooltip={_('dashboardStatsButtonAddAllHost')}
type='button'
>
<Icon icon='host' />
</button>
<button
className='btn btn-secondary'
onClick={this._selectAllVms}
tooltip={_('dashboardStatsButtonAddAllVM')}
type='button'
>
<Icon icon='vm' />
</button>
<ActionButton
btnStyle='secondary'
handler={this._resetSelection}
icon='remove'
tooltip={_('dashboardStatsButtonRemoveAll')}
/>
<ActionButton
handler={this._selectAllHosts}
icon='host'
tooltip={_('dashboardStatsButtonAddAllHost')}
/>
<ActionButton
handler={this._selectAllVms}
icon='vm'
tooltip={_('dashboardStatsButtonAddAllVM')}
/>
<ActionButton
disabled={!objects.length}
handler={this._validSelection}
icon='success'

View File

@@ -26,7 +26,9 @@ import {
} from 'utils'
import {
createDoesHostNeedRestart,
createGetObject
createGetObject,
createGetObjectsOfType,
createSelector
} from 'selectors'
import {
CpuSparkLines,
@@ -79,7 +81,13 @@ class MiniStats extends Component {
@connectStore(({
container: createGetObject((_, props) => props.item.$pool),
needsRestart: createDoesHostNeedRestart((_, props) => props.item)
needsRestart: createDoesHostNeedRestart((_, props) => props.item),
nVms: createGetObjectsOfType('VM').count(
createSelector(
(_, props) => props.item.id,
hostId => obj => obj.$container === hostId
)
)
}))
export default class HostItem extends Component {
get _isRunning () {
@@ -97,7 +105,7 @@ export default class HostItem extends Component {
_onSelect = () => this.props.onSelect(this.props.item.id)
render () {
const { item: host, container, expandAll, selected } = this.props
const { item: host, container, expandAll, selected, nVms } = this.props
return <div className={styles.item}>
<BlockLink to={`/hosts/${host.id}`}>
<SingleLineRow>
@@ -129,6 +137,15 @@ export default class HostItem extends Component {
<Col mediumSize={3} className='hidden-lg-down'>
<EllipsisContainer>
<span className={styles.itemActionButons}>
<Tooltip content={<span>{nVms}x {_('vmsTabName')}</span>}>
{(nVms > 0)
? <Link to={`/home?s=$container:${host.id}&t=VM`}>
<Icon icon='vm' size='1' fixedWidth />
</Link>
: <Icon icon='vm' size='1' fixedWidth />
}
</Tooltip>
&nbsp;
{this._isRunning
? <span>
<Tooltip content={_('stopHostLabel')}>

View File

@@ -2,27 +2,36 @@ import * as ComplexMatcher from 'complex-matcher'
import * as homeFilters from 'home-filters'
import _ from 'intl'
import ActionButton from 'action-button'
import ceil from 'lodash/ceil'
import Button from 'button'
import CenterPanel from 'center-panel'
import Component from 'base-component'
import debounce from 'lodash/debounce'
import find from 'lodash/find'
import forEach from 'lodash/forEach'
import Icon from 'icon'
import invoke from 'invoke'
import keys from 'lodash/keys'
import includes from 'lodash/includes'
import isEmpty from 'lodash/isEmpty'
import isString from 'lodash/isString'
import Link from 'link'
import map from 'lodash/map'
import Page from '../page'
import React from 'react'
import Shortcuts from 'shortcuts'
import SingleLineRow from 'single-line-row'
import size from 'lodash/size'
import Tooltip from 'tooltip'
import { Card, CardHeader, CardBlock } from 'card'
import {
ceil,
debounce,
filter,
find,
forEach,
get,
identity,
includes,
isEmpty,
isString,
keys,
map,
pick,
pickBy,
size,
some
} from 'lodash'
import {
addCustomFilter,
copyVms,
@@ -67,7 +76,6 @@ import {
getUser
} from 'selectors'
import {
Button,
DropdownButton,
MenuItem,
OverlayTrigger,
@@ -219,6 +227,10 @@ export default class Home extends Component {
router: React.PropTypes.object
}
state = {
selectedItems: {}
}
get page () {
return this.state.page
}
@@ -231,13 +243,31 @@ export default class Home extends Component {
}
componentWillReceiveProps (props) {
this._initFilterAndSortBy(props)
if (this._getFilter() !== this._getFilter(props)) {
this._initFilterAndSortBy(props)
}
if (props.type !== this.props.type) {
this.setState({ highlighted: undefined })
this.setState({ activePage: undefined, highlighted: undefined })
}
}
componentDidUpdate () {
const { selectedItems } = this.state
// Unselect items that are no longer visible
if ((this._visibleItemsRecomputations || 0) < (this._visibleItemsRecomputations = this._getVisibleItems.recomputations())) {
const newSelectedItems = pick(selectedItems, map(this._getVisibleItems(), 'id'))
if (size(newSelectedItems) < this._getNumberOfSelectedItems()) {
this.setState({ selectedItems: newSelectedItems })
}
}
}
_getNumberOfItems = createCounter(() => this.props.items)
_getNumberOfSelectedItems = createCounter(
() => this.state.selectedItems,
[ identity ]
)
_getType () {
return this.props.type
@@ -249,33 +279,20 @@ export default class Home extends Component {
pathname,
query: { ...query, t: type, s: undefined }
})
this.setState({ highlighted: undefined })
}
// Filter and sort -----------------------------------------------------------
_getDefaultFilter (props = this.props) {
const { type, user } = props
const defaultFilter = OPTIONS[type].defaultFilter
// No user.
if (!user) {
return defaultFilter
}
const { defaultHomeFilters = {}, filters = {} } = user.preferences || {}
const filterName = defaultHomeFilters[type]
// No filter defined in preferences.
if (!filterName) {
return defaultFilter
}
// Filter defined.
let tmp
const { type } = props
const preferences = get(props, 'user.preferences')
const defaultFilterName = get(preferences, [ 'defaultHomeFilters', type ])
return firstDefined(
(tmp = homeFilters[type]) && tmp[filterName],
(tmp = filters[type]) && tmp[filterName],
defaultFilter
defaultFilterName && firstDefined(
get(homeFilters, [ type, defaultFilterName ]),
get(preferences, [ 'filters', type, defaultFilterName ])
),
OPTIONS[type].defaultFilter
)
}
@@ -296,7 +313,7 @@ export default class Home extends Component {
const defaultFilter = this._getDefaultFilter(props)
if (defaultFilter != null) {
this._setFilter(defaultFilter, props)
this._setFilter(defaultFilter, props, true)
}
return
}
@@ -342,13 +359,13 @@ export default class Home extends Component {
// Optionally can take the props to be able to use it in
// componentWillReceiveProps().
_setFilter (filter, props = this.props) {
_setFilter (filter, props = this.props, replace) {
if (!isString(filter)) {
filter = filter::ComplexMatcher.toString()
}
const { pathname, query } = props.location
this.context.router.push({
this.context.router[replace ? 'replace' : 'push']({
pathname,
query: { ...query, s: filter }
})
@@ -385,6 +402,11 @@ export default class Home extends Component {
_tick = isCriteria => <Icon icon={isCriteria ? 'success' : undefined} fixedWidth />
// High level filters --------------------------------------------------------
_typesDropdownItems = map(TYPES, (label, type) =>
<MenuItem key={type} onClick={() => this._setType(type)}>{label}</MenuItem>
)
_updateSelectedPools = pools => {
const filter = this._getParsedFilter()
@@ -424,42 +446,12 @@ export default class Home extends Component {
: filter::ComplexMatcher.removePropertyClause('tags')
)
}
// Checkboxes
_selectedItems = {}
_updateMasterCheckbox () {
const masterCheckbox = this.refs.masterCheckbox
if (!masterCheckbox) {
return
}
const noneChecked = isEmpty(this._selectedItems)
masterCheckbox.checked = !noneChecked
masterCheckbox.indeterminate = !noneChecked && size(this._selectedItems) !== this._getFilteredItems().length
this.setState({ displayActions: !noneChecked })
}
_selectItem = (id, checked) => {
const shouldBeChecked = checked === undefined ? !this._selectedItems[id] : checked
shouldBeChecked ? this._selectedItems[id] = true : delete this._selectedItems[id]
this.forceUpdate()
this._updateMasterCheckbox()
}
_selectAllItems = (checked) => {
const shouldBeChecked = checked === undefined ? !size(this._selectedItems) : checked
this._selectedItems = {}
forEach(this._getFilteredItems(), item => {
shouldBeChecked && (this._selectedItems[item.id] = true)
})
this.forceUpdate()
this._updateMasterCheckbox()
}
_addCustomFilter = () => {
return addCustomFilter(
this._getType(),
this._getFilter()
)
}
_getCustomFilters () {
const { preferences } = this.props.user || {}
@@ -471,6 +463,34 @@ export default class Home extends Component {
return customFilters[this._getType()]
}
// Checkboxes ----------------------------------------------------------------
_getIsAllSelected = createSelector(
() => this.state.selectedItems,
this._getVisibleItems,
(selectedItems, visibleItems) =>
size(visibleItems) > 0 && size(filter(selectedItems)) === size(visibleItems)
)
_getIsSomeSelected = createSelector(
() => this.state.selectedItems,
some
)
_toggleMaster = () => {
const selectedItems = {}
if (!this._getIsAllSelected()) {
forEach(this._getVisibleItems(), ({ id }) => {
selectedItems[id] = true
})
}
this.setState({ selectedItems })
}
_getSelectedItemsIds = createSelector(
() => this.state.selectedItems,
items => keys(pickBy(items))
)
// Shortcuts -----------------------------------------------------------------
_getShortcutsHandler = createSelector(
() => this._getVisibleItems(),
items => (command, event) => {
@@ -486,7 +506,13 @@ export default class Home extends Component {
this.setState({ highlighted: (this.state.highlighted + items.length - 1) % items.length || 0 })
break
case 'SELECT':
this._selectItem(items[this.state.highlighted].id)
const itemId = items[this.state.highlighted].id
this.setState({
selectedItems: {
...this.state.selectedItems,
[itemId]: !this.state.selectedItems[itemId]
}
})
break
case 'JUMP_INTO':
const item = items[this.state.highlighted]
@@ -499,9 +525,7 @@ export default class Home extends Component {
}
)
_typesDropdownItems = map(TYPES, (label, type) =>
<MenuItem onClick={() => this._setType(type)}>{label}</MenuItem>
)
// Header --------------------------------------------------------------------
_renderHeader () {
const { type } = this.props
@@ -544,11 +568,9 @@ export default class Home extends Component {
type='text'
/>
<div className='input-group-btn'>
<a
className='btn btn-secondary'
onClick={this._clearFilter}>
<Button onClick={this._clearFilter}>
<Icon icon='clear-search' />
</a>
</Button>
</div>
<div className='input-group-btn'>
<ActionButton
@@ -570,19 +592,26 @@ export default class Home extends Component {
</Container>
}
render () {
const { props } = this
const { user } = this.props
const isAdmin = user && user.permission === 'admin'
const noRegisteredServers = !props.servers || !props.servers.length
// ---------------------------------------------------------------------------
if (!props.areObjectsFetched) {
render () {
const {
areObjectsFetched,
noServersConnected,
servers,
user
} = this.props
const isAdmin = user && user.permission === 'admin'
const noRegisteredServers = !servers || !servers.length
if (!areObjectsFetched) {
return <CenterPanel>
<h2><img src='assets/loading.svg' /></h2>
</CenterPanel>
}
if (props.noServersConnected && isAdmin) {
if (noServersConnected && isAdmin) {
return <CenterPanel>
<Card shadow>
<CardHeader>{_('homeWelcome')}</CardHeader>
@@ -654,12 +683,35 @@ export default class Home extends Component {
const filteredItems = this._getFilteredItems()
const visibleItems = this._getVisibleItems()
const { activePage, sortBy, highlighted } = this.state
const { type } = props
const {
activePage,
expandAll,
highlighted,
selectedHosts,
selectedItems,
selectedPools,
selectedTags,
sortBy
} = this.state
const {
items,
type
} = this.props
const options = OPTIONS[type]
const { Item } = options
const { mainActions, otherActions } = options
const selectedItemsIds = keys(this._selectedItems)
const {
Item,
mainActions,
otherActions,
showHostsSelector,
showPoolsSelector
} = options
// Necessary because indeterminate cannot be used as an attribute
if (this.refs.masterCheckbox) {
this.refs.masterCheckbox.indeterminate = this._getIsSomeSelected() && !this._getIsAllSelected()
}
return <Page header={this._renderHeader()}>
<Shortcuts name='Home' handler={this._getShortcutsHandler()} targetNodeSelector='body' stopPropagation={false} />
@@ -667,13 +719,18 @@ export default class Home extends Component {
<div className={styles.itemContainer}>
<SingleLineRow className={styles.itemContainerHeader}>
<Col smallsize={11} mediumSize={3}>
<input type='checkbox' onChange={() => this._selectAllItems()} ref='masterCheckbox' />
<input
checked={this._getIsAllSelected()}
onChange={this._toggleMaster}
ref='masterCheckbox'
type='checkbox'
/>
{' '}
<span className='text-muted'>
{size(this._selectedItems)
{this._getNumberOfSelectedItems()
? _('homeSelectedItems', {
icon: <Icon icon={type.toLowerCase()} />,
selected: size(this._selectedItems),
selected: this._getNumberOfSelectedItems(),
total: nItems
})
: _('homeDisplayedItems', {
@@ -685,16 +742,15 @@ export default class Home extends Component {
</span>
</Col>
<Col mediumSize={8} className='text-xs-right hidden-sm-down'>
{this.state.displayActions
{this._getNumberOfSelectedItems()
? (
<div>
{mainActions && <div className='btn-group'>
{map(mainActions, (action, key) => (
<Tooltip content={action.tooltip} key={key}>
<ActionButton
btnStyle='secondary'
{...action}
handlerParam={selectedItemsIds}
handlerParam={this._getSelectedItemsIds()}
/>
</Tooltip>
))}
@@ -702,7 +758,7 @@ export default class Home extends Component {
{otherActions && (
<DropdownButton bsStyle='secondary' id='advanced' title={_('homeMore')}>
{map(otherActions, (action, key) => (
<MenuItem key={key} onClick={() => { action.handler(selectedItemsIds, action.params) }}>
<MenuItem key={key} onClick={() => { action.handler(this._getSelectedItemsIds(), action.params) }}>
<Icon icon={action.icon} fixedWidth /> {_(action.labelId)}
</MenuItem>
))}
@@ -710,7 +766,7 @@ export default class Home extends Component {
)}
</div>
) : <div>
{options.showPoolsSelector && (
{showPoolsSelector && (
<OverlayTrigger
trigger='click'
rootClose
@@ -721,16 +777,16 @@ export default class Home extends Component {
autoFocus
multi
onChange={this._updateSelectedPools}
value={this.state.selectedPools}
value={selectedPools}
/>
</Popover>
}
>
<Button className='btn-link'><Icon icon='pool' /> {_('homeAllPools')}</Button>
<Button btnStyle='link'><Icon icon='pool' /> {_('homeAllPools')}</Button>
</OverlayTrigger>
)}
{' '}
{options.showHostsSelector && (
{showHostsSelector && (
<OverlayTrigger
trigger='click'
rootClose
@@ -741,12 +797,12 @@ export default class Home extends Component {
autoFocus
multi
onChange={this._updateSelectedHosts}
value={this.state.selectedHosts}
value={selectedHosts}
/>
</Popover>
}
>
<Button className='btn-link'><Icon icon='host' /> {_('homeAllHosts')}</Button>
<Button btnStyle='link'><Icon icon='host' /> {_('homeAllHosts')}</Button>
</OverlayTrigger>
)}
{' '}
@@ -760,14 +816,14 @@ export default class Home extends Component {
<SelectTag
autoFocus
multi
objects={props.items}
objects={items}
onChange={this._updateSelectedTags}
value={this.state.selectedTags}
value={selectedTags}
/>
</Popover>
}
>
<Button className='btn-link'><Icon icon='tags' /> {_('homeAllTags')}</Button>
<Button btnStyle='link'><Icon icon='tags' /> {_('homeAllTags')}</Button>
</OverlayTrigger>
{' '}
<DropdownButton bsStyle='link' id='sort' title={_('homeSortBy')}>
@@ -785,10 +841,9 @@ export default class Home extends Component {
}
</Col>
<Col smallsize={1} mediumSize={1} className='text-xs-right'>
<button className='btn btn-secondary'
onClick={this._expandAll}>
<Button onClick={this._expandAll}>
<Icon icon='nav' />
</button>
</Button>
</Col>
</SingleLineRow>
{isEmpty(filteredItems)
@@ -798,13 +853,13 @@ export default class Home extends Component {
</a>
</p>
: map(visibleItems, (item, index) => (
<div className={highlighted === index && styles.highlight}>
<div key={item.id} className={highlighted === index && styles.highlight}>
<Item
expandAll={this.state.expandAll}
expandAll={expandAll}
item={item}
key={item.id}
onSelect={this._selectItem}
selected={this._selectedItems[item.id]}
onSelect={this.toggleState(`selectedItems.${item.id}`)}
selected={selectedItems[item.id]}
/>
</div>
))

View File

@@ -46,6 +46,13 @@ import styles from './index.css'
const getHostMetrics = createGetHostMetrics(getPoolHosts)
const getNumberOfSrs = createGetObjectsOfType('SR').count(
createSelector(
(_, props) => props.item.id,
poolId => obj => obj.$pool === poolId
)
)
const getNumberOfVms = createGetObjectsOfType('VM').count(
createSelector(
(_, props) => props.item.id,
@@ -57,6 +64,7 @@ import styles from './index.css'
hostMetrics: getHostMetrics,
missingPaths: getMissingPatches,
poolHosts: getPoolHosts,
nSrs: getNumberOfSrs,
nVms: getNumberOfVms
}
})
@@ -73,7 +81,7 @@ export default class PoolItem extends Component {
}
render () {
const { item: pool, expandAll, selected, hostMetrics, poolHosts, nVms } = this.props
const { item: pool, expandAll, selected, hostMetrics, poolHosts, nSrs, nVms } = this.props
const { missingPatchCount } = this.state
return <div className={styles.item}>
<BlockLink to={`/pools/${pool.id}`}>
@@ -106,6 +114,38 @@ export default class PoolItem extends Component {
}
</EllipsisContainer>
</Col>
<Col mediumSize={1} className='hidden-md-down'>
<EllipsisContainer>
<span className={styles.itemActionButons}>
<Tooltip content={<span>{hostMetrics.count}x {_('hostsTabName')}</span>}>
{(hostMetrics.count > 0)
? <Link to={`/home?s=$pool:${pool.id}&t=host`}>
<Icon icon='host' size='1' fixedWidth />
</Link>
: <Icon icon='host' size='1' fixedWidth />
}
</Tooltip>
&nbsp;
<Tooltip content={<span>{nVms}x {_('vmsTabName')}</span>}>
{(nVms > 0)
? <Link to={`/home?s=$pool:${pool.id}&t=VM`}>
<Icon icon='vm' size='1' fixedWidth />
</Link>
: <Icon icon='vm' size='1' fixedWidth />
}
</Tooltip>
&nbsp;
<Tooltip content={<span>{nSrs}x {_('srsTabName')}</span>}>
{(nSrs > 0)
? <Link to={`/home?s=$pool:${pool.id}&t=SR`}>
<Icon icon='sr' size='1' fixedWidth />
</Link>
: <Icon icon='sr' size='1' fixedWidth />
}
</Tooltip>
</span>
</EllipsisContainer>
</Col>
<Col mediumSize={4} className='hidden-md-down'>
<EllipsisContainer>
<Ellipsis>
@@ -132,9 +172,11 @@ export default class PoolItem extends Component {
<SingleLineRow>
<Col mediumSize={3} className={styles.itemExpanded}>
<span>
<Link to={`/home?s=$pool:${pool.id}&t=host`}>{hostMetrics.count}x <Icon icon='host' /></Link>
{hostMetrics.count}x <Icon icon='host' />
{' '}
<Link to={`/home?s=$pool:${pool.id}&t=VM`}>{nVms}x <Icon icon='vm' /></Link>
{nVms}x <Icon icon='vm' />
{' '}
{nSrs}x <Icon icon='sr' />
{' '}
{hostMetrics.cpus}x <Icon icon='cpu' />
{' '}

View File

@@ -1,16 +1,10 @@
import _ from 'intl'
import assign from 'lodash/assign'
import HostActionBar from './action-bar'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import Link from 'link'
import { NavLink, NavTabs } from 'nav'
import Page from '../page'
import pick from 'lodash/pick'
import React, { cloneElement, Component } from 'react'
import sortBy from 'lodash/sortBy'
import sum from 'lodash/sum'
import Tooltip from 'tooltip'
import { Text } from 'editable'
import { editHost, fetchHostStats, getHostMissingPatches, installAllHostPatches, installHostPatch } from 'xo'
@@ -25,6 +19,15 @@ import {
createGetObjectsOfType,
createSelector
} from 'selectors'
import {
assign,
isEmpty,
isString,
map,
pick,
sortBy,
sum
} from 'lodash'
import TabAdvanced from './tab-advanced'
import TabConsole from './tab-console'
@@ -94,7 +97,7 @@ const isRunning = host => host && host.power_state === 'Running'
const getHostPatches = createSelector(
createGetObjectsOfType('pool_patch'),
createGetObjectsOfType('host_patch').pick(
createSelector(getHost, host => host.patches)
createSelector(getHost, host => isString(host.patches[0]) ? host.patches : [])
),
(poolsPatches, hostsPatches) => map(hostsPatches, hostPatch => ({
...hostPatch,

View File

@@ -16,14 +16,11 @@ const ALLOW_INSTALL_SUPP_PACK = process.env.XOA_PLAN > 1
const forceReboot = host => restartHost(host, true)
const formatPack = (version, pack) => {
const [ author, name ] = pack.split(':')
return <tr>
<th>{_('supplementalPackTitle', { author, name })}</th>
<td>{version}</td>
</tr>
}
const formatPack = ({ name, author, description, version }) => <tr>
<th>{_('supplementalPackTitle', { author, name })}</th>
<td>{description}</td>
<td>{version}</td>
</tr>
export default ({
host

View File

@@ -1,4 +1,5 @@
import _ from 'intl'
import Button from 'button'
import Component from 'base-component'
import CopyToClipboard from 'react-copy-to-clipboard'
import debounce from 'lodash/debounce'
@@ -73,20 +74,19 @@ export default class extends Component {
<input type='text' className='form-control' ref='clipboard' onChange={this._setRemoteClipboard} />
<span className='input-group-btn'>
<CopyToClipboard text={this.state.clipboard || ''}>
<button className='btn btn-secondary'>
<Button>
<Icon icon='clipboard' /> {_('copyToClipboardLabel')}
</button>
</Button>
</CopyToClipboard>
</span>
</div>
</Col>
<Col mediumSize={2}>
<button
className='btn btn-secondary'
<Button
onClick={this._sendCtrlAltDel}
>
<Icon icon='vm-keyboard' /> {_('ctrlAltDelButtonLabel')}
</button>
</Button>
</Col>
</Row>
<Row className='console'>

View File

@@ -8,9 +8,9 @@ import map from 'lodash/map'
import pick from 'lodash/pick'
import SingleLineRow from 'single-line-row'
import some from 'lodash/some'
import StateButton from 'state-button'
import TabButton from 'tab-button'
import Tooltip from 'tooltip'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { confirm } from 'modal'
import { connectStore, noop } from 'utils'
import { Container, Row, Col } from 'grid'
@@ -166,40 +166,42 @@ class PifItem extends Component {
)}
</td>
<td>
{pif.carrier
? <span className='tag tag-success'>
{_('pifStatusConnected')}
</span>
: <span className='tag tag-default'>
{_('pifStatusDisconnected')}
</span>
}
<StateButton
disabledLabel={_('pifDisconnected')}
disabledHandler={connectPif}
disabledTooltip={_('connectPif')}
enabledLabel={_('pifConnected')}
enabledHandler={disconnectPif}
enabledTooltip={_('disconnectPif')}
disabled={pif.attached && (pif.management || pif.disallowUnplug)}
handlerParam={pif}
state={pif.attached}
/>
{' '}
<Tooltip content={pif.carrier ? _('pifPhysicallyConnected') : _('pifPhysicallyDisconnected')}>
<Icon
icon='network'
size='lg'
className={pif.carrier ? 'text-success' : 'text-muted'}
/>
</Tooltip>
</td>
<td>
<ButtonGroup className='pull-right'>
<ActionRowButton
btnStyle='default'
disabled={pif.attached && (pif.management || pif.disallowUnplug)}
handler={pif.attached ? disconnectPif : connectPif}
handlerParam={pif}
icon={pif.attached ? 'disconnect' : 'connect'}
tooltip={pif.attached ? _('disconnectPif') : _('connectPif')}
/>
<ActionRowButton
btnStyle='default'
disabled={pif.physical || pif.disallowUnplug || pif.management}
handler={deletePif}
handlerParam={pif}
icon='delete'
tooltip={_('deletePif')}
/>
</ButtonGroup>
<td className='text-xs-right'>
<ActionRowButton
disabled={pif.physical || pif.disallowUnplug || pif.management}
handler={deletePif}
handlerParam={pif}
icon='delete'
tooltip={_('deletePif')}
/>
</td>
</tr>
}
}
export default (({
export default ({
host,
networks,
pifs,
@@ -232,7 +234,7 @@ export default (({
<th>{_('pifMtuLabel')}</th>
<th>{_('defaultLockingMode')}</th>
<th>{_('pifStatusLabel')}</th>
<th />
<th className='text-xs-right'>{_('pifAction')}</th>
</tr>
</thead>
<tbody>
@@ -244,4 +246,4 @@ export default (({
}
</Col>
</Row>
</Container>)
</Container>

View File

@@ -1,15 +1,18 @@
import _ from 'intl'
import ActionRowButton from 'action-row-button'
import isEmpty from 'lodash/isEmpty'
import React, { Component } from 'react'
import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import Upgrade from 'xoa-upgrade'
import { connectStore, formatSize } from 'utils'
import { Container, Row, Col } from 'grid'
import { createDoesHostNeedRestart } from 'selectors'
import { createDoesHostNeedRestart, createSelector } from 'selectors'
import { FormattedRelative, FormattedTime } from 'react-intl'
import { restartHost } from 'xo'
import {
isEmpty,
isString
} from 'lodash'
const MISSING_PATCH_COLUMNS = [
{
@@ -84,12 +87,56 @@ const INSTALLED_PATCH_COLUMNS = [
}
]
// support for software_version.platform_version ^2.1.1
const INSTALLED_PATCH_COLUMNS_2 = [
{
default: true,
name: _('patchNameLabel'),
itemRenderer: patch => patch.name,
sortCriteria: patch => patch.name
},
{
name: _('patchDescription'),
itemRenderer: patch => patch.description,
sortCriteria: patch => patch.description
},
{
name: _('patchSize'),
itemRenderer: patch => formatSize(patch.size),
sortCriteria: patch => patch.size
}
]
@connectStore(() => ({
needsRestart: createDoesHostNeedRestart((_, props) => props.host)
}))
export default class HostPatches extends Component {
_getPatches = createSelector(
() => this.props.host,
() => this.props.hostPatches,
(host, hostPatches) => {
if (isEmpty(host.patches) && isEmpty(hostPatches)) {
return { patches: null }
}
if (isString(host.patches[0])) {
return {
patches: hostPatches,
columns: INSTALLED_PATCH_COLUMNS
}
}
return {
patches: host.patches,
columns: INSTALLED_PATCH_COLUMNS_2
}
}
)
render () {
const { host, hostPatches, missingPatches, installAllPatches, installPatch } = this.props
const { host, missingPatches, installAllPatches, installPatch } = this.props
const { patches, columns } = this._getPatches()
return process.env.XOA_PLAN > 1
? <Container>
<Row>
@@ -125,13 +172,12 @@ export default class HostPatches extends Component {
</Row>}
<Row>
<Col>
{!isEmpty(hostPatches)
? (
<span>
<h3>{_('hostAppliedPatches')}</h3>
<SortedTable collection={hostPatches} columns={INSTALLED_PATCH_COLUMNS} />
</span>
) : <h4 className='text-xs-center'>{_('patchNothing')}</h4>
{patches
? <span>
<h3>{_('hostAppliedPatches')}</h3>
<SortedTable collection={patches} columns={columns} />
</span>
: <h4 className='text-xs-center'>{_('patchNothing')}</h4>
}
</Col>
</Row>

View File

@@ -5,8 +5,8 @@ import Link from 'link'
import map from 'lodash/map'
import React from 'react'
import SortedTable from 'sorted-table'
import StateButton from 'state-button'
import Tooltip from 'tooltip'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { connectPbd, disconnectPbd, deletePbd, editSr, isSrShared } from 'xo'
import { connectStore, formatSize } from 'utils'
import { Container, Row, Col } from 'grid'
@@ -54,42 +54,29 @@ const SR_COLUMNS = [
},
{
name: _('pbdStatus'),
itemRenderer: storage => storage.attached
? <span>
<span className='tag tag-success'>
{_('pbdStatusConnected')}
</span>
<ButtonGroup className='pull-right'>
<ActionRowButton
btnStyle='default'
handler={disconnectPbd}
handlerParam={storage.pbdId}
icon='disconnect'
tooltip={_('pbdDisconnect')}
/>
</ButtonGroup>
</span>
: <span>
<span className='tag tag-default'>
{_('pbdStatusDisconnected')}
</span>
<ButtonGroup className='pull-right'>
<ActionRowButton
btnStyle='default'
handler={connectPbd}
handlerParam={storage.pbdId}
icon='connect'
tooltip={_('pbdConnect')}
/>
<ActionRowButton
btnStyle='default'
handler={deletePbd}
handlerParam={storage.pbdId}
icon='sr-forget'
tooltip={_('pbdForget')}
/>
</ButtonGroup>
</span>
itemRenderer: storage => <StateButton
disabledLabel={_('pbdStatusDisconnected')}
disabledHandler={connectPbd}
disabledTooltip={_('pbdConnect')}
enabledLabel={_('pbdStatusConnected')}
enabledHandler={disconnectPbd}
enabledTooltip={_('pbdDisconnect')}
handlerParam={storage.pbdId}
state={storage.attached}
/>
},
{
name: _('pbdAction'),
itemRenderer: storage => !storage.attached &&
<ActionRowButton
handler={deletePbd}
handlerParam={storage.pbdId}
icon='sr-forget'
tooltip={_('pbdForget')}
/>,
textAlign: 'right'
}
]

View File

@@ -6,11 +6,13 @@ import isArray from 'lodash/isArray'
import map from 'lodash/map'
import React from 'react'
import Shortcuts from 'shortcuts'
import themes from 'themes'
import _, { IntlProvider } from 'intl'
import { blockXoaAccess } from 'xoa-updater'
import { connectStore, routes } from 'utils'
import { Notification } from 'notification'
import { ShortcutManager } from 'react-shortcuts'
import { ThemeProvider } from 'styled-components'
import { TooltipViewer } from 'tooltip'
import { Container, Row, Col } from 'grid'
// import {
@@ -178,20 +180,24 @@ export default class XoApp extends Component {
const blocked = signedUp && blockXoaAccess(trial) // If we are under expired or unstable trial (signed up only)
return <IntlProvider>
<DocumentTitle title='Xen Orchestra'>
<div style={CONTAINER_STYLE}>
<Shortcuts name='XoApp' handler={this._shortcutsHandler} targetNodeSelector='body' stopPropagation={false} />
<Menu ref='menu' />
<div ref='bodyWrapper' style={BODY_WRAPPER_STYLE}>
<div style={BODY_STYLE}>
{blocked ? <XoaUpdates /> : this.props.children}
<ThemeProvider theme={themes.base}>
<DocumentTitle title='Xen Orchestra'>
<div style={CONTAINER_STYLE}>
<Shortcuts name='XoApp' handler={this._shortcutsHandler} targetNodeSelector='body' stopPropagation={false} />
<Menu ref='menu' />
<div ref='bodyWrapper' style={BODY_WRAPPER_STYLE}>
<div style={BODY_STYLE}>
{blocked
? <XoaUpdates />
: signedUp ? this.props.children : <p>Still loading</p>}
</div>
</div>
<Modal />
<Notification />
<TooltipViewer />
</div>
<Modal />
<Notification />
<TooltipViewer />
</div>
</DocumentTitle>
</DocumentTitle>
</ThemeProvider>
</IntlProvider>
}
}

View File

@@ -1,6 +1,7 @@
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import ActionRowButton from 'action-row-button'
import Button from 'button'
import Component from 'base-component'
import delay from 'lodash/delay'
import find from 'lodash/find'
@@ -55,7 +56,7 @@ const getType = function (param) {
/**
* Tries extracting Object targeted property
*/
const reduceObject = (value, propertyName = 'id') => value && value[propertyName] || value
const reduceObject = (value, propertyName = 'id') => (value != null && value[propertyName]) || value
/**
* Adapts all data "arrayed" by UI-multiple-selectors to job's cross-product trick
@@ -257,8 +258,8 @@ export default class Jobs extends Component {
_handleSubmit = () => {
const {name, method, params} = this.refs
const { job, owner } = this.state
const { job, owner, timeout } = this.state
const _job = {
type: 'call',
name: name.value,
@@ -268,7 +269,8 @@ export default class Jobs extends Component {
type: 'crossProduct',
items: dataToParamVectorItems(method.value.info.properties, params.value)
},
userId: owner
userId: owner,
timeout: timeout ? timeout * 1e3 : undefined
}
job && (_job.id = job.id)
@@ -320,7 +322,10 @@ export default class Jobs extends Component {
}
const { params } = this.refs
params.value = data
this.setState({ owner: job.userId })
this.setState({
owner: job.userId,
timeout: job.timeout && job.timeout / 1e3
})
}
_reset = () => {
@@ -330,7 +335,8 @@ export default class Jobs extends Component {
this.setState({
action: undefined,
job: undefined,
owner: undefined
owner: undefined,
timeout: ''
})
}
@@ -351,13 +357,14 @@ export default class Jobs extends Component {
type === 'user' && permission === 'admin'
render () {
const { state } = this
const {
action,
actions,
job,
jobs,
owner
} = this.state
} = state
const { formatMessage } = this.props.intl
const isJobUserMissing = this._getIsJobUserMissing()
@@ -374,13 +381,14 @@ export default class Jobs extends Component {
/>
<input type='text' ref='name' className='form-control mb-1 mt-1' placeholder={formatMessage(messages.jobNamePlaceholder)} pattern='[^_]+' required />
<SelectPlainObject ref='method' options={actions} optionKey='method' onChange={this._handleSelectMethod} placeholder={_('jobActionPlaceHolder')} />
<input type='number' onChange={this.linkState('timeout')} value={state.timeout} className='form-control mb-1 mt-1' placeholder='Job timeout (seconds)' />
{action && <fieldset>
<GenericInput ref='params' schema={action.info} uiSchema={action.uiSchema} label={action.method} required />
{job && <p className='text-warning'>{_('jobEditMessage', { name: job.name, id: job.id.slice(4, 8) })}</p>}
{process.env.XOA_PLAN > 3
? <span><ActionButton form='newJobForm' handler={this._handleSubmit} icon='save' btnStyle='primary'>{_('saveResourceSet')}</ActionButton>
{' '}
<button type='button' className='btn btn-default' onClick={this._reset}>{_('resetResourceSet')}</button></span>
<Button onClick={this._reset}>{_('resetResourceSet')}</Button></span>
: <span><Upgrade place='health' available={4} /></span>
}
</fieldset>

View File

@@ -1,6 +1,5 @@
import _ from 'intl'
import ActionRowButton from 'action-row-button'
import ActionToggle from 'action-toggle'
import filter from 'lodash/filter'
import find from 'lodash/find'
import forEach from 'lodash/forEach'
@@ -10,10 +9,10 @@ import LogList from '../../logs'
import map from 'lodash/map'
import orderBy from 'lodash/orderBy'
import React, { Component } from 'react'
import StateButton from 'state-button'
import Tooltip from 'tooltip'
import Upgrade from 'xoa-upgrade'
import { addSubscriptions } from 'utils'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { Container } from 'grid'
import { createSelector } from 'selectors'
import {
@@ -104,21 +103,21 @@ export default class Overview extends Component {
const { id } = schedule
return (
<ActionToggle
value={this.state.scheduleTable[id]}
handler={this._updateScheduleState}
<StateButton
disabledLabel={_('jobStateDisabled')}
disabledHandler={enableSchedule}
disabledTooltip={_('logIndicationToEnable')}
enabledLabel={_('jobStateEnabled')}
enabledHandler={disableSchedule}
enabledTooltip={_('logIndicationToDisable')}
handlerParam={id}
size='small' />
state={this.state.scheduleTable[id]}
/>
)
}
_updateScheduleState = id => {
const enabled = this.state.scheduleTable[id]
const method = enabled ? disableSchedule : enableSchedule
return method(id)
}
_getIsScheduleUserMissing = createSelector(
() => this.state.schedules,
() => this.props.users,
@@ -155,6 +154,7 @@ export default class Overview extends Component {
<th>{_('job')}</th>
<th className='hidden-xs-down'>{_('jobScheduling')}</th>
<th>{_('jobState')}</th>
<th className='text-xs-right'>{_('jobAction')}</th>
</tr>
</thead>
<tbody>
@@ -176,25 +176,23 @@ export default class Overview extends Component {
</Link>
</td>
<td className='hidden-xs-down'>{schedule.cron}</td>
<td>
{this._getScheduleToggle(schedule)}
<fieldset className='pull-right'>
<td>{this._getScheduleToggle(schedule)}</td>
<td className='text-xs-right'>
<fieldset>
{!isScheduleUserMissing[schedule.id] && <Tooltip content={_('jobUserNotFound')}><Icon className='mr-1' icon='error' /></Tooltip>}
<ButtonGroup>
<ActionRowButton
icon='delete'
btnStyle='danger'
handler={deleteSchedule}
handlerParam={schedule}
/>
<ActionRowButton
disabled={!isScheduleUserMissing[schedule.id]}
icon='run-schedule'
btnStyle='warning'
handler={runJob}
handlerParam={schedule.job}
/>
</ButtonGroup>
<ActionRowButton
icon='delete'
btnStyle='danger'
handler={deleteSchedule}
handlerParam={schedule}
/>
<ActionRowButton
disabled={!isScheduleUserMissing[schedule.id]}
icon='run-schedule'
btnStyle='warning'
handler={runJob}
handlerParam={schedule.job}
/>
</fieldset>
</td>
</tr>

View File

@@ -1,5 +1,6 @@
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import Button from 'button'
import find from 'lodash/find'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
@@ -171,7 +172,7 @@ export default class Schedules extends Component {
{process.env.XOA_PLAN > 3
? <span><ActionButton form='newScheduleForm' handler={this._handleSubmit} icon='save' btnStyle='primary'>{_('saveBackupJob')}</ActionButton>
{' '}
<button type='button' className='btn btn-secondary' onClick={this._reset}>{_('selectTableReset')}</button></span>
<Button onClick={this._reset}>{_('selectTableReset')}</Button></span>
: <span><Upgrade place='health' available={4} /></span>
}
</div>
@@ -195,9 +196,9 @@ export default class Schedules extends Component {
<td className='hidden-xs-down'>{schedule.cron}</td>
<td className='hidden-xs-down'>{schedule.timezone || _('jobServerTimezone')}</td>
<td>
<button type='button' className='btn btn-primary' onClick={() => this._edit(schedule.id)}><Icon icon='edit' /></button>
<Button btnStyle='primary' onClick={() => this._edit(schedule.id)}><Icon icon='edit' /></Button>
{' '}
<button type='button' className='btn btn-danger' onClick={() => deleteSchedule(schedule)}><Icon icon='delete' /></button>
<Button btnStyle='danger' onClick={() => deleteSchedule(schedule)}><Icon icon='delete' /></Button>
</td>
</tr>)}
</tbody>

View File

@@ -1,8 +1,10 @@
import _, { FormattedDuration } from 'intl'
import ActionButton from 'action-button'
import ActionRowButton from 'action-row-button'
import ButtonGroup from 'button-group'
import classnames from 'classnames'
import forEach from 'lodash/forEach'
import get from 'lodash/get'
import Icon from 'icon'
import includes from 'lodash/includes'
import map from 'lodash/map'
@@ -13,7 +15,6 @@ import renderXoItem from 'render-xo-item'
import SortedTable from 'sorted-table'
import Tooltip from 'tooltip'
import { alert, confirm } from 'modal'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import { connectStore } from 'utils'
import { createGetObject } from 'selectors'
import { FormattedDate } from 'react-intl'
@@ -53,7 +54,7 @@ class JobParam extends Component {
return object
? <span><strong>{object.type || paramKey}</strong>: {renderXoItem(object)} </span>
: <span><strong>{paramKey}:</strong> {id} </span>
: <span><strong>{paramKey}:</strong> {String(id)} </span>
}
}
@@ -71,8 +72,8 @@ class JobReturn extends Component {
const Log = props => <ul className='list-group'>
{map(props.log.calls, call => <li key={call.callKey} className='list-group-item'>
<strong className='text-info'>{call.method}: </strong>
{map(call.params, (value, key) => <JobParam id={value} paramKey={key} key={key} />)}
<strong className='text-info'>{call.method}: </strong><br />
{map(call.params, (value, key) => [ <JobParam id={value} paramKey={key} key={key} />, <br /> ])}
{call.returnedValue && <span>{' '}<JobReturn id={call.returnedValue} /></span>}
{call.error &&
<span className='text-danger'>
@@ -86,7 +87,7 @@ const Log = props => <ul className='list-group'>
</li>)}
</ul>
const showCalls = log => alert(<span>{_('job')} {log.jobId}</span>, <Log log={log} />)
const showCalls = log => alert(_('jobModalTitle', { job: log.jobId }), <Log log={log} />)
const LOG_COLUMNS = [
{
@@ -95,10 +96,15 @@ const LOG_COLUMNS = [
sortCriteria: log => log.jobId
},
{
name: _('job'),
name: _('jobType'),
itemRenderer: log => jobKeyToLabel[log.key],
sortCriteria: log => log.key
},
{
name: _('jobTag'),
itemRenderer: log => get(log, 'calls[0].params.tag'),
sortCriteria: log => get(log, 'calls[0].params.tag')
},
{
name: _('jobStart'),
itemRenderer: log => log.start && <FormattedDate value={new Date(log.start)} month='short' day='numeric' year='numeric' hour='2-digit' minute='2-digit' second='2-digit' />,
@@ -133,11 +139,11 @@ const LOG_COLUMNS = [
<span className='pull-right'>
<ButtonGroup>
<Tooltip content={_('logDisplayDetails')}><ActionRowButton icon='preview' handler={showCalls} handlerParam={log} /></Tooltip>
<Tooltip content={_('remove')}><ActionRowButton btnStyle='default' handler={deleteJobsLog} handlerParam={log.logKey} icon='delete' /></Tooltip>
<Tooltip content={_('remove')}><ActionRowButton handler={deleteJobsLog} handlerParam={log.logKey} icon='delete' /></Tooltip>
</ButtonGroup>
</span>
</span>,
sortCriteria: log => log.hasErrors && ' ' || log.status
sortCriteria: log => log.hasErrors ? ' ' : log.status
}
]

View File

@@ -1,13 +1,12 @@
import _ from 'intl'
import Component from 'base-component'
import classNames from 'classnames'
import Component from 'base-component'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import Link from 'link'
import map from 'lodash/map'
import React from 'react'
import Tooltip from 'tooltip'
import { Button } from 'react-bootstrap-4/lib'
import { UpdateTag } from '../xoa-updates'
import {
addSubscriptions,
@@ -32,6 +31,8 @@ import {
import styles from './index.css'
const returnTrue = () => true
@connectStore(() => ({
isAdmin,
nTasks: createGetObjectsOfType('task').count(
@@ -70,8 +71,9 @@ export default class Menu extends Component {
_checkPermissions = createSelector(
() => this.props.isAdmin,
() => this.props.permissions,
(isAdmin, permissions) => ({ id }) =>
isAdmin || permissions && permissions[id] && permissions[id].operate
(isAdmin, permissions) => isAdmin
? returnTrue
: ({ id }) => permissions && permissions[id] && permissions[id].operate
)
_getNoOperatablePools = createSelector(
@@ -99,11 +101,22 @@ export default class Menu extends Component {
return this.refs.content.offsetHeight
}
_toggleCollapsed = () => {
_toggleCollapsed = event => {
event.preventDefault()
this._removeListener()
this.setState({ collapsed: !this.state.collapsed })
}
_connect = event => {
event.preventDefault()
return connect()
}
_signOut = event => {
event.preventDefault()
return signOut()
}
render () {
const { isAdmin, nTasks, status, user, pools, nHosts } = this.props
const noOperatablePools = this._getNoOperatablePools()
@@ -151,7 +164,7 @@ export default class Menu extends Component {
]},
{ to: '/about', icon: 'menu-about', label: 'aboutPage' },
{ to: '/tasks', icon: 'task', label: 'taskMenu', pill: nTasks },
{ to: '/xosan', icon: 'menu-xosan', label: 'xosan' },
isAdmin && { to: '/xosan', icon: 'menu-xosan', label: 'xosan' },
!(noOperatablePools && noResourceSets) && { to: '/vms/new', icon: 'menu-new', label: 'newMenu', subMenu: [
{ to: '/vms/new', icon: 'menu-new-vm', label: 'newVmPage' },
isAdmin && { to: '/new/sr', icon: 'menu-new-sr', label: 'newSrPage' },
@@ -175,9 +188,9 @@ export default class Menu extends Component {
</span>
</li>
<li>
<Button onClick={this._toggleCollapsed}>
<a className='nav-link' onClick={this._toggleCollapsed} href='#'>
<Icon icon='menu-collapse' size='lg' fixedWidth />
</Button>
</a>
</li>
{map(items, (item, index) =>
item && <MenuLinkItem key={index} item={item} />
@@ -218,10 +231,10 @@ export default class Menu extends Component {
<li>&nbsp;</li>
<li>&nbsp;</li>
<li className='nav-item xo-menu-item'>
<Button className='nav-link' onClick={signOut}>
<a className='nav-link' onClick={this._signOut} href='#'>
<Icon icon='sign-out' size='lg' fixedWidth />
<span className={styles.hiddenCollapsed}>{' '}{_('signOut')}</span>
</Button>
</a>
</li>
<li className='nav-item xo-menu-item'>
<Link className='nav-link text-xs-center' to={'/user'}>
@@ -236,9 +249,9 @@ export default class Menu extends Component {
? <li className='nav-item text-xs-center'>{_('statusConnecting')}</li>
: status === 'disconnected' &&
<li className='nav-item text-xs-center xo-menu-item'>
<Button className='nav-link' onClick={connect}>
<a className='nav-link' onClick={this._connect} href='#'>
<Icon icon='alarm' size='lg' fixedWidth /> {_('statusDisconnected')}
</Button>
</a>
</li>
}
</ul>

View File

@@ -1,36 +1,38 @@
import _, { messages } from 'intl'
import ActionButton from 'action-button'
import BaseComponent from 'base-component'
import clamp from 'lodash/clamp'
import Button from 'button'
import classNames from 'classnames'
import DebounceInput from 'react-debounce-input'
import every from 'lodash/every'
import filter from 'lodash/filter'
import find from 'lodash/find'
import forEach from 'lodash/forEach'
import get from 'lodash/get'
import getEventValue from 'get-event-value'
import Icon from 'icon'
import includes from 'lodash/includes'
import isArray from 'lodash/isArray'
import isEmpty from 'lodash/isEmpty'
import isIp from 'is-ip'
import join from 'lodash/join'
import map from 'lodash/map'
import Page from '../page'
import React from 'react'
import size from 'lodash/size'
import slice from 'lodash/slice'
import store from 'store'
import sum from 'lodash/sum'
import sumBy from 'lodash/sumBy'
import Tags from 'tags'
import Tooltip from 'tooltip'
import Wizard, { Section } from 'wizard'
import { Button } from 'react-bootstrap-4/lib'
import { Container, Row, Col } from 'grid'
import { injectIntl } from 'react-intl'
import { Limits } from 'usage'
import {
clamp,
every,
filter,
find,
forEach,
get,
includes,
isArray,
isEmpty,
join,
map,
slice,
size,
sum,
sumBy
} from 'lodash'
import {
addSshKey,
createVm,
@@ -43,6 +45,7 @@ import {
XEN_DEFAULT_CPU_WEIGHT
} from 'xo'
import {
SelectHost,
SelectIp,
SelectNetwork,
SelectPool,
@@ -88,6 +91,8 @@ const NB_VMS_MAX = 100
const getObject = createGetObject((_, id) => id)
const returnTrue = () => true
// Sub-components
const SectionContent = ({ column, children }) => (
@@ -172,7 +177,7 @@ class Vif extends BaseComponent {
</span>
</LineItem>
<Item>
<Button onClick={onDelete} bsStyle='secondary'>
<Button onClick={onDelete}>
<Icon icon='new-vm-remove' />
</Button>
</Item>
@@ -202,7 +207,8 @@ class Vif extends BaseComponent {
return user && user.preferences && user.preferences.sshKeys
},
keys => keys
)
),
srs: createGetObjectsOfType('SR')
}))
@injectIntl
export default class NewVm extends BaseComponent {
@@ -276,6 +282,7 @@ export default class NewVm extends BaseComponent {
VDIs: [],
VIFs: [],
seqStart: 1,
share: false,
tags: []
})
}
@@ -346,6 +353,7 @@ export default class NewVm extends BaseComponent {
const resourceSet = this._getResourceSet()
const data = {
affinityHost: state.affinityHost && state.affinityHost.id,
clone: !this.isDiskTemplate && state.fastClone,
existingDisks: state.existingDisks,
installation,
@@ -365,6 +373,7 @@ export default class NewVm extends BaseComponent {
pv_args: state.pv_args,
autoPoweron: state.autoPoweron,
bootAfterCreate: state.bootAfterCreate,
share: state.share,
cloudConfig,
coreOs: state.template.name_label === 'CoreOS',
tags: state.tags
@@ -437,7 +446,7 @@ export default class NewVm extends BaseComponent {
cpuWeight: '',
memoryDynamicMax: template.memory.dynamic[1],
// installation
installMethod: template.install_methods && template.install_methods[0] || 'SSH',
installMethod: (template.install_methods != null && template.install_methods[0]) || 'SSH',
sshKeys: this.props.userSshKeys && this.props.userSshKeys.length && [ 0 ],
customConfig: '#cloud-config\n#hostname: myhostname\n#ssh_authorized_keys:\n# - ssh-rsa <myKey>\n#packages:\n# - htop\n',
// interfaces
@@ -485,9 +494,11 @@ export default class NewVm extends BaseComponent {
)
_getCanOperate = createSelector(
() => this.props.isAdmin,
() => this.props.permissions,
permissions => ({ id }) =>
this.props.isAdmin || permissions && permissions[id] && permissions[id].operate
(isAdmin, permissions) => isAdmin
? returnTrue
: ({ id }) => permissions && permissions[id] && permissions[id].operate
)
_getVmPredicate = createSelector(
this._getIsInPool,
@@ -542,6 +553,26 @@ export default class NewVm extends BaseComponent {
this._getCanOperate,
[ (pool, canOperate) => canOperate(pool) ]
)
_getAffinityHostPredicate = createSelector(
() => this.props.pool,
() => this.state.state.existingDisks,
() => this.state.state.VDIs,
() => this.props.srs,
(pool, existingDisks, VDIs, srs) => {
if (!srs) {
return false
}
const containers = [
...map(existingDisks, disk => get(srs, `${disk.$SR}.$container`)),
...map(VDIs, disk => get(srs, `${disk.SR}.$container`))
]
return host => host.$pool === pool.id &&
every(containers, container =>
container === pool.id || container === host.id
)
}
)
_getDefaultNetworkId = () => {
const resourceSet = this._getResolvedResourceSet()
if (resourceSet) {
@@ -769,7 +800,6 @@ export default class NewVm extends BaseComponent {
</Wizard>
<div className={styles.submitSection}>
<ActionButton
btnStyle='secondary'
className={styles.button}
handler={this._reset}
icon='new-vm-reset'
@@ -936,7 +966,7 @@ export default class NewVm extends BaseComponent {
value={newSshKey}
/>
<span className='input-group-btn'>
<Button className='btn btn-secondary' onClick={this._addNewSshKey} disabled={!newSshKey}>
<Button onClick={this._addNewSshKey} disabled={!newSshKey}>
<Icon icon='add' />
</Button>
</span>
@@ -1089,6 +1119,7 @@ export default class NewVm extends BaseComponent {
<SectionContent column>
{map(VIFs, (vif, index) => <div key={index}>
<Vif
networkPredicate={this._getNetworkPredicate()}
onChangeAddresses={this._linkState(`VIFs.${index}.addresses`, '*.id')}
onChangeMac={this._linkState(`VIFs.${index}.mac`)}
onChangeNetwork={this._linkState(`VIFs.${index}.network`, 'id')}
@@ -1100,7 +1131,7 @@ export default class NewVm extends BaseComponent {
{index < VIFs.length - 1 && <hr />}
</div>)}
<Item>
<Button onClick={this._addInterface} bsStyle='secondary'>
<Button onClick={this._addInterface}>
<Icon icon='new-vm-add' />
{' '}
{_('newVmAddInterface')}
@@ -1195,18 +1226,6 @@ export default class NewVm extends BaseComponent {
/>}
</span>
</Item>
{' '}
<Item className='checkbox'>
<label>
<input
checked={!!vdi.bootable}
onChange={this._getOnChangeCheckbox('VDIs', index, 'bootable')}
type='checkbox'
/>
{' '}
{_('newVmBootableLabel')}
</label>
</Item>
<Item label={_('newVmNameLabel')}>
<DebounceInput
className='form-control'
@@ -1231,7 +1250,7 @@ export default class NewVm extends BaseComponent {
/>
</Item>
<Item>
<Button onClick={() => this._removeVdi(index)} bsStyle='secondary'>
<Button onClick={() => this._removeVdi(index)}>
<Icon icon='new-vm-remove' />
</Button>
</Item>
@@ -1239,7 +1258,7 @@ export default class NewVm extends BaseComponent {
{index < VDIs.length - 1 && <hr />}
</div>)}
<Item>
<Button onClick={this._addVdi} bsStyle='secondary'>
<Button onClick={this._addVdi}>
<Icon icon='new-vm-add' />
{' '}
{_('newVmAddDisk')}
@@ -1259,6 +1278,7 @@ export default class NewVm extends BaseComponent {
_renderAdvanced = () => {
const {
affinityHost,
autoPoweron,
bootAfterCreate,
cpuCap,
@@ -1271,13 +1291,14 @@ export default class NewVm extends BaseComponent {
namePattern,
nbVms,
seqStart,
share,
showAdvanced,
tags
} = this.state.state
const { formatMessage } = this.props.intl
return <Section icon='new-vm-advanced' title='newVmAdvancedPanel' done={this._isAdvancedDone()}>
<SectionContent column>
<Button bsStyle='secondary' onClick={this._toggleState('showAdvanced')}>
<Button onClick={this._toggleState('showAdvanced')}>
{showAdvanced ? _('newVmHideAdvanced') : _('newVmShowAdvanced')}
</Button>
</SectionContent>
@@ -1306,6 +1327,17 @@ export default class NewVm extends BaseComponent {
<Tags labels={tags} onChange={this._linkState('tags')} />
</Item>
</SectionContent>,
this._getResourceSet() !== undefined && <SectionContent>
<Item>
<input
checked={share}
onChange={this._getOnChangeCheckbox('share')}
type='checkbox'
/>
&nbsp;
{_('newVmShare')}
</Item>
</SectionContent>,
<SectionContent>
<Item label={_('newVmCpuWeightLabel')}>
<DebounceInput
@@ -1379,7 +1411,7 @@ export default class NewVm extends BaseComponent {
/>
<span className='input-group-btn'>
<Tooltip content={_('newVmNumberRecalculate')}>
<Button bsStyle='secondary' disabled={!multipleVms} onClick={this._updateNbVms}>
<Button disabled={!multipleVms} onClick={this._updateNbVms}>
<Icon icon='arrow-right' />
</Button>
</Tooltip>
@@ -1397,6 +1429,15 @@ export default class NewVm extends BaseComponent {
</Item>
)}
</LineItem>}
</SectionContent>,
<SectionContent>
<Item label={_('newVmAffinityHost')}>
<SelectHost
onChange={this._linkState('affinityHost')}
predicate={this._getAffinityHostPredicate()}
value={affinityHost}
/>
</Item>
</SectionContent>
]}
</Section>

View File

@@ -515,7 +515,7 @@ export default class New extends Component {
type='text'
/>
<span className='input-group-btn'>
<ActionButton icon='search' btnStyle='default' handler={this._handleSearchServer} />
<ActionButton icon='search' handler={this._handleSearchServer} />
</span>
</div>
</fieldset>
@@ -560,7 +560,7 @@ export default class New extends Component {
ref='port'
type='text'
/>
<ActionButton icon='search' btnStyle='default' handler={this._handleSearchServer} />
<ActionButton icon='search' handler={this._handleSearchServer} />
</div>
{auth &&
<fieldset>

View File

@@ -92,7 +92,6 @@ import TabPatches from './tab-patches'
}
})
export default class Pool extends Component {
_setNameDescription = nameDescription => editPool(this.props.pool, { name_description: nameDescription })
_setNameLabel = nameLabel => editPool(this.props.pool, { name_label: nameLabel })

View File

@@ -2,6 +2,7 @@ import _ from 'intl'
import Copiable from 'copiable'
import React from 'react'
import SelectFiles from 'select-files'
import Upgrade from 'xoa-upgrade'
import { Container, Row, Col } from 'grid'
import { installSupplementalPackOnAllHosts } from 'xo'
@@ -33,7 +34,7 @@ export default ({
</Row>
</Container>
<h3 className='mt-1 mb-1'>{_('supplementalPackPoolNew')}</h3>
<div>
<Upgrade place='poolSupplementalPacks' required={2}>
<SelectFiles onChange={file => installSupplementalPackOnAllHosts(pool, file)} />
</div>
</Upgrade>
</div>

View File

@@ -1,9 +1,10 @@
import _ from 'intl'
import ActionRow from 'action-row-button'
import Button from 'button'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
import TabButton from 'tab-button'
import React, { Component } from 'react'
import TabButton from 'tab-button'
import { deleteMessage } from 'xo'
import { createPager } from 'selectors'
import { FormattedRelative, FormattedTime } from 'react-intl'
@@ -42,12 +43,12 @@ export default class TabLogs extends Component {
: <div>
<Row>
<Col className='text-xs-right'>
<button className='btn btn-lg btn-tab' onClick={this._previousPage}>
<Button size='large' onClick={this._previousPage}>
&lt;
</button>
<button className='btn btn-lg btn-tab' onClick={this._nextPage}>
</Button>
<Button size='large' onClick={this._nextPage}>
&gt;
</button>
</Button>
<TabButton
btnStyle='danger'
handler={this._removeAllLogs}

View File

@@ -1,6 +1,8 @@
import _ from 'intl'
import ActionRowButton from 'action-row-button'
import BaseComponent from 'base-component'
import Button from 'button'
import ButtonGroup from 'button-group'
import Icon from 'icon'
import isEmpty from 'lodash/isEmpty'
import map from 'lodash/map'
@@ -9,10 +11,9 @@ import some from 'lodash/some'
import SortedTable from 'sorted-table'
import TabButton from 'tab-button'
import Tooltip from 'tooltip'
import { Button, ButtonGroup } from 'react-bootstrap-4/lib'
import { Text, Number } from 'editable'
import { Container, Row, Col } from 'grid'
import { connectStore } from 'utils'
import { Container, Row, Col } from 'grid'
import { Text, Number } from 'editable'
import { Toggle } from 'form'
import {
createFinder,
@@ -205,10 +206,9 @@ class PifItem extends Component {
</span>
}
</td>
<td>
<ButtonGroup className='pull-right'>
<td className='text-xs-right'>
<ButtonGroup>
<ActionRowButton
btnStyle='default'
disabled={disableUnplug}
handler={pif.attached ? disconnectPif : connectPif}
handlerParam={pif}
@@ -228,7 +228,7 @@ class PifsItem extends BaseComponent {
return <div>
<Tooltip content={showPifs ? _('hidePifs') : _('showPifs')}>
<Button bsSize='small' bsStyle='secondary' className='mb-1 pull-right' onClick={this.toggleState('showPifs')}>
<Button size='small' className='mb-1 pull-right' onClick={this.toggleState('showPifs')}>
<Icon icon={showPifs ? 'hidden' : 'shown'} />
</Button>
</Tooltip>
@@ -272,9 +272,8 @@ class NetworkActions extends Component {
render () {
const { network, disableNetworkDelete } = this.props
return <ButtonGroup className='pull-right'>
return <ButtonGroup>
<ActionRowButton
btnStyle='default'
disabled={disableNetworkDelete}
handler={deleteNetwork}
handlerParam={network}
@@ -324,7 +323,8 @@ const NETWORKS_COLUMNS = [
},
{
name: '',
itemRenderer: network => <NetworkActions network={network} />
itemRenderer: network => <NetworkActions network={network} />,
textAlign: 'right'
}
]

View File

@@ -1,6 +1,7 @@
import Component from 'base-component'
import HostsPatchesTable from 'hosts-patches-table'
import React from 'react'
import Upgrade from 'xoa-upgrade'
import { connectStore } from 'utils'
import { Container, Row, Col } from 'grid'
import { createGetObjectsOfType } from 'selectors'
@@ -20,7 +21,7 @@ export default class TabPatches extends Component {
_getContainer = () => this.refs.container
render () {
return (
return <Upgrade place='poolPatches' required={2}>
<Container>
<Row>
<Col className='text-xs-right'>
@@ -37,6 +38,6 @@ export default class TabPatches extends Component {
</Col>
</Row>
</Container>
)
</Upgrade>
}
}

View File

@@ -454,7 +454,7 @@ export class Edit extends Component {
<input className='form-control' type='number' min={0} onChange={this.linkState(`ipPools.${index}.quantity`)} value={firstDefined(ipPool.quantity, '')} placeholder='∞' />
</Col>
<Col mediumSize={2}>
<ActionButton btnStyle='secondary' icon='delete' handler={this._removeIpPool} handlerParam={index} />
<ActionButton icon='delete' handler={this._removeIpPool} handlerParam={index} />
</Col>
</Row>)}
<Row>
@@ -465,7 +465,7 @@ export class Edit extends Component {
<input className='form-control' type='number' min={0} onChange={this.linkState('newIpPoolQuantity')} value={state.newIpPoolQuantity || ''} placeholder='∞' />
</Col>
<Col mediumSize={2}>
<ActionButton btnStyle='secondary' icon='add' handler={this._addIpPool} />
<ActionButton icon='add' handler={this._addIpPool} />
</Col>
</Row>
</Col>
@@ -478,7 +478,7 @@ export class Edit extends Component {
<li className='list-group-item text-xs-center'>
<div className='btn-toolbar'>
<ActionButton btnStyle='primary' icon='save' handler={this._save} type='submit'>{_('saveResourceSet')}</ActionButton>
<ActionButton btnStyle='secondary' icon='reset' handler={this._reset}>{_('resetResourceSet')}</ActionButton>
<ActionButton icon='reset' handler={this._reset}>{_('resetResourceSet')}</ActionButton>
{resourceSet && <ActionButton btnStyle='danger' icon='delete' handler={deleteResourceSet} handlerParam={resourceSet}>{_('deleteResourceSet')}</ActionButton>}
</div>
</li>
@@ -686,7 +686,6 @@ export default class Self extends Component {
{_('resourceSetNew')}
</ActionButton>
<ActionButton
btnStyle='secondary'
handler={recomputeResourceSetsLimits}
icon='refresh'
>

View File

@@ -1,6 +1,7 @@
import _ from 'intl'
import ActionButton from 'action-button'
import ActionRowButton from 'action-row-button'
import ButtonGroup from 'button-group'
import Component from 'base-component'
import filter from 'lodash/filter'
import forEach from 'lodash/forEach'
@@ -19,7 +20,6 @@ import { connectStore } from 'utils'
import { Container } from 'grid'
import { error } from 'notification'
import { SelectHighLevelObject, SelectRole, SelectSubject } from 'select-objects'
import { ButtonGroup } from 'react-bootstrap-4/lib'
import {
createGetObjectsOfType,
@@ -161,7 +161,7 @@ export default class Acls extends Component {
const newSomeTypeFilters = some(newTypeFilters)
// If some objects need to be removed from the selected objects
if (!newTypeFilters[type] || !someTypeFilters && newSomeTypeFilters) {
if (!newTypeFilters[type] || (!someTypeFilters && newSomeTypeFilters)) {
this.setState({
objects: filter(objects, ({ type }) => !newSomeTypeFilters || newTypeFilters[type])
})
@@ -172,7 +172,7 @@ export default class Acls extends Component {
someTypeFilters: some(newTypeFilters)
}, () => {
// If some objects need to be removed from the selected objects
if (!this.state.typeFilters[type] || !someTypeFilters && this.state.someTypeFilters) {
if (!this.state.typeFilters[type] || (!someTypeFilters && this.state.someTypeFilters)) {
this.setState({
objects: filter(objects, this._getObjectPredicate())
})
@@ -243,7 +243,7 @@ export default class Acls extends Component {
<SelectHighLevelObject multi onChange={this.linkState('objects')} value={objects} predicate={this._getObjectPredicate()} />
</div>
<div className='form-group mb-1'>
<ButtonGroup className='mr-1'>
<ButtonGroup>
{map(TYPES, type =>
<ActionButton
btnStyle={typeFilters[type] ? 'success' : 'secondary'}
@@ -256,7 +256,8 @@ export default class Acls extends Component {
/>
)}
</ButtonGroup>
<ActionButton tooltip='Select all' btnStyle='secondary' size='small' icon='add' handler={this._selectAll} />
{' '}
<ActionButton tooltip='Select all' size='small' icon='add' handler={this._selectAll} />
</div>
<div className='form-group'>
<SelectRole onChange={this.linkState('action')} value={action} />

View File

@@ -1,6 +1,6 @@
import _ from 'intl'
import ActionButton from 'action-button'
import { Button } from 'react-bootstrap-4/lib'
import Button from 'button'
import Component from 'base-component'
import Dropzone from 'dropzone'
import Icon from 'icon'
@@ -77,7 +77,6 @@ export default class Config extends Component {
{_('importConfig')}
</ActionButton>
<Button
bsStyle='secondary'
onClick={this._unselectFile}
>
{_('importVmsCleanList')}
@@ -93,7 +92,7 @@ export default class Config extends Component {
<br />
<div className='mt-1'>
<h2><Icon icon='export' /> {_('exportConfig')}</h2>
<Button bsStyle='primary' onClick={exportConfig}>{_('downloadConfig')}</Button>
<Button btnStyle='primary' onClick={exportConfig}>{_('downloadConfig')}</Button>
</div>
</div>
}

View File

@@ -41,7 +41,7 @@ class UserDisplay extends Component {
const { id, users } = this.props
return <span>
{id && (users && users[id] && users[id].email) || <em>&lt;{_('unknownUser')}&gt;</em>}
{(id && users && users[id] && users[id].email) || <em>&lt;{_('unknownUser')}&gt;</em>}
{' '}
<ActionButton className='pull-right' btnStyle='primary' size='small' icon='remove' handler={this._removeUser} />
</span>

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