Compare commits

...

534 Commits

Author SHA1 Message Date
Julien Fontanet
242a02836c 5.15.1 2017-12-29 17:26:35 +01:00
Pierre Donias
6936f223f3 fix(new-xosan): suggestions can be null as well (#2544) 2017-12-29 17:24:00 +01:00
Julien Fontanet
eb8dfc86ca 5.15.0 2017-12-29 16:50:51 +01:00
Pierre Donias
02c715e1cc feat(xosan): license management (#2528) 2017-12-29 16:48:21 +01:00
Olivier Lambert
8cb53b0c4e changelog for 5.15 2017-12-29 16:04:44 +01:00
Julien Fontanet
629f68ffd7 fix(precommit hook): pass if no tests found 2017-12-28 17:18:32 +01:00
Pierre Donias
d3691313e6 chore(package): update complex-matcher (#2538) 2017-12-27 16:50:52 +01:00
Julien Fontanet
9aed4f6fba chore(package): use complex-matcher package (#2536) 2017-12-27 11:16:39 +01:00
Julien Fontanet
ef17cb1c6c fix: remove unused PropTypes 2017-12-18 17:08:38 +01:00
Julien Fontanet
ecc086f15d chore: do not use React.PropTypes directly 2017-12-18 17:00:04 +01:00
Julien Fontanet
be9eb8ce91 chore(package): update dependencies 2017-12-18 17:00:04 +01:00
Rajaa.BARHTAOUI
18ca6b935c feat(host/network): use SortedTable (#2512)
See #2416
2017-12-14 16:07:32 +01:00
Julien Fontanet
b3769019e5 fix(react-novnc): support IPv6 in URL (#2531)
Fixes #2530
2017-12-14 14:39:49 +01:00
Rajaa.BARHTAOUI
506a6b0cf4 feat(vm/network): use SortedTable (#2460)
See #2416
2017-12-14 11:42:52 +01:00
Julien Fontanet
a18df93c4f chore: remove incorrect file
This file has been added by mistake during prettier integration.
2017-12-13 16:06:57 +01:00
Pierre Donias
684269321b fix(select): multi select should not close on item selection (#2526)
New default behaviour introduced by react-select v1.0.0-rc.9
2017-12-12 13:10:41 +01:00
Rajaa.BARHTAOUI
d7eeeca268 SortedTable/groupedActions: disabled props (#2471)
* Fixes conflits

* Fixes

* Fixes
2017-12-08 15:37:19 +01:00
Pierre Donias
4c21175ca7 feat(notification/error): link to settings/logs for admins (#2521)
Fixes #2516
2017-12-07 11:08:11 +01:00
Julien Fontanet
377efcd054 feat(ButtonLink): a button which acts like a link (#2144) 2017-12-07 10:28:47 +01:00
Pierre Donias
072401f600 fix(VM/disks/attach): confusing button label (#2520) 2017-12-06 16:19:06 +01:00
Pierre Donias
1ce7d94261 feat(self/edit): add labels to each field (#2519)
Fixes #2509
2017-12-06 15:58:52 +01:00
Julien Fontanet
8178de8a6b chore: prettify non-source files 2017-12-06 13:52:12 +01:00
Julien Fontanet
eb37c7d7d8 chore(package): update prettier to 1.9.1 2017-12-06 13:47:01 +01:00
Pierre Donias
9465459ef9 fix(xoa-update): ansi_up (#2518) 2017-12-06 11:52:35 +01:00
Pierre Donias
52a71cec91 fix(messages): bad prettier wrapping on some messages (#2517) 2017-12-05 17:37:53 +01:00
Rajaa.BARHTAOUI
2e1b32fadc feat(jobs/schedules): use SortedTable (#2483)
See #2416
2017-11-24 11:03:57 +01:00
Julien Fontanet
0c8f3ea824 chore(package): prepublish → prepublishOnly 2017-11-23 12:19:48 +01:00
Julien Fontanet
bf45cdd2b8 chore(package): update dependencies 2017-11-20 15:20:55 +01:00
Julien Fontanet
fd47403ec9 5.14.2 2017-11-17 14:59:08 +01:00
badrAZ
137b8e7f7f feat(job/log): add merge size and speed (#2488)
Fixes #2426
2017-11-17 14:42:34 +01:00
Julien Fontanet
4a8c5a980a chore(xo): remove unused import 2017-11-16 17:03:50 +01:00
Julien Fontanet
233aca7911 chore(xo): replace superagent by fetch 2017-11-16 16:51:48 +01:00
Julien Fontanet
0178ce0a91 fix(logs): better filters without false positives (#2468) 2017-11-15 10:50:07 +01:00
Julien Fontanet
142233453a fix(lint-staged): always create stash (#2494)
To avoid failing `git stash pop`.
2017-11-14 18:17:14 +01:00
Julien Fontanet
b12a804fb3 fix(test): eslint run 2017-11-14 16:20:12 +01:00
Julien Fontanet
3fe5efbfab feat: prettier integration (#2490) 2017-11-14 15:25:02 +01:00
Pierre Donias
d71323a67d feat(SortedTable): pass objects instead of ids to action handlers (#2476) 2017-11-09 15:10:54 +01:00
Julien Fontanet
eb46711e34 chore(package): update dependencies 2017-11-08 11:31:44 +01:00
Julien Fontanet
748b09d8fa feat(home): use more formatSizeShort 2017-11-08 11:18:39 +01:00
Olivier Lambert
e6a32b53fc feat(home/VM): add sparklines in the expanded view (#2470)
Fixes #2469

* chore(home): mutualize MiniStats
2017-11-08 10:12:35 +01:00
Julien Fontanet
7ce9e8a959 chore(grid): add docs 2017-11-07 16:26:59 +01:00
Julien Fontanet
b817cb86d0 5.14.1 2017-11-03 09:53:24 +01:00
Pierre Donias
3e1b2119c4 fix(new-vm): handle creation without vGPU (#2467)
Fixes #2466
2017-11-03 09:53:09 +01:00
Julien Fontanet
bfa31be3b7 5.14.0 2017-10-31 17:52:13 +01:00
Pierre Donias
584da2f56a feat(VM): add vGPUs support (#2463)
Fixes #2413
2017-10-31 17:46:23 +01:00
Olivier Lambert
6a071942a5 feat(changelog): update changelog for 5.14 release 2017-10-31 13:48:01 +01:00
Nicolas Raynaud
ac8787e930 fix(XOSAN): missing brackets in JSX (#2454) 2017-10-30 16:59:10 +01:00
Julien Fontanet
ab60bc46cf chore(xo/deleteMessage): typo (#2461) 2017-10-30 14:52:37 +01:00
Olivier Lambert
b67310ae75 feat(vm): add snapshot description. (#2459)
Fixes #2458
2017-10-27 14:02:11 +02:00
Pierre Donias
020618554a feat(SortedTable/individualActions): disabled prop (#2456) 2017-10-25 11:22:48 +02:00
Julien Fontanet
38ec7ac34f 5.13.3 2017-10-24 15:51:19 +02:00
Julien Fontanet
a78151c93e feat(home): sort VMs by snapshots (#2451)
Fixes #2450
2017-10-23 11:46:04 +02:00
Julien Fontanet
285b1fb36e fix(new-vm): multiline cloud config 2017-10-23 11:39:18 +02:00
Julien Fontanet
16e8c87cc6 5.13.2 2017-10-23 10:17:00 +02:00
Olivier Lambert
93ffb77e81 fix(vm snapshot): copyVm accepts Id (#2447)
Fixes #2446
2017-10-20 16:46:56 +02:00
Julien Fontanet
88f6d77047 fix(vm/snapshots): reverse chronological order
Fixes #2442
2017-10-19 16:31:56 +02:00
Pierre Donias
3ff63927f3 feat(updater): troubleshooting link on updater error (#2443)
Fixes #1610
2017-10-19 10:15:34 +02:00
Julien Fontanet
d396593d99 5.13.1 2017-10-18 17:40:28 +02:00
Julien Fontanet
62b64ad0b6 feat(file restore): improve multiple files selection UX (#2440)
- keep select empty
- keep select open after file selection
- remove selected files from options

Fixes #2438
2017-10-18 17:34:23 +02:00
Pierre Donias
6f33a79644 feat(patches): hide paid patches to XS-free users (#2441)
Fixes #2382
2017-10-18 17:14:46 +02:00
Olivier Lambert
67f31407d7 feat(home/host): include XS version in expanded view. Fixes #2439 2017-10-18 16:25:42 +02:00
Julien Fontanet
f05d2d0063 chore(gulpfile): do not watch node_modules 2017-10-17 17:08:05 +02:00
Julien Fontanet
682deb4b56 chore(Scheduler): columns break under large 2017-10-16 15:36:07 +02:00
Julien Fontanet
64fac454b5 chore(Scheduler): align center 2017-10-16 15:35:38 +02:00
Julien Fontanet
e9c60bc958 fix(SchedulePreview): fix month mapping (#2435)
Fix #2427
2017-10-16 15:13:25 +02:00
badrAZ
7a8c0831bd fix(backup): use retention instead of depth (#2092)
Fixes #1935
2017-10-16 10:21:47 +02:00
Julien Fontanet
0ca1af8606 feat(Scheduler): group months/days and hours/minutes 2017-10-16 10:19:18 +02:00
Julien Fontanet
e81f88e676 feat(SortedTable): snappier filter (#2430)
- lower typing debounce (500ms → 250ms)
- no more debounce on selection in filters list
- no more debounce on clear
2017-10-16 09:55:50 +02:00
Julien Fontanet
e96a8af9ef feat(scheduling): group hours in 4 rows of 6 (#2433)
Easier to understand than in 3 rows of 8.
2017-10-16 09:32:15 +02:00
Julien Fontanet
d8393d8500 feat(SortedTable): save page in URL (#2421)
Fixes vatesfr/xo-web#2405
2017-10-09 17:57:26 +02:00
Julien Fontanet
44b74e6135 feat(vm/snapshots): use SortedTable (#2417)
See #2416.
2017-10-09 10:13:18 +02:00
Julien Fontanet
f31417a85b fix: tests and coding style 2017-10-06 19:34:12 +02:00
Julien Fontanet
1c6967594c chore(package): update dependencies 2017-10-06 17:36:34 +02:00
Julien Fontanet
59f8a58b21 feat(SortedTable): always use a valid page (#2403)
Fixes #2401
2017-10-04 18:08:45 +02:00
Julien Fontanet
4d1f647a89 feat(SortedTable): compacter pagination 2017-10-04 17:56:19 +02:00
Pierre Donias
86e5206b4d fix(XOSAN): ask user to restart toolstacks after installing XOSAN pack (#2404) 2017-10-04 17:55:34 +02:00
Julien Fontanet
105ede5b1d fix(SortedTable): fix item selection 2017-10-01 12:46:25 +02:00
Julien Fontanet
bb8a25cc9d 5.13.0 2017-09-29 16:06:39 +02:00
Pierre Donias
54c3d843be feat(SortedTable): keyboard shortcuts support (#2383)
Fixes #2330
2017-09-29 16:04:52 +02:00
Pierre Donias
4a1407786c feat(xosan): beta phase 2+ (#2394)
XOSAN: beta phase 2+
2017-09-29 15:24:09 +02:00
Olivier Lambert
f5e3aef86c feat(changelog): finishing changelog for 5.13 release 2017-09-29 14:00:14 +02:00
Julien Fontanet
37c8a7c2b2 fix(i18n): use utf-8 dots instead of 3 dots for ellipsis (#2393)
Fixes #2391
2017-09-29 13:39:56 +02:00
Olivier Lambert
1a788fae7e Merge branch 'next-release' into olt-dots 2017-09-29 11:45:38 +02:00
Julien Fontanet
8efc083a70 style(SortedTable): fix double props due to bad merge 2017-09-29 11:29:40 +02:00
Julien Fontanet
f196a9ebc4 feat(SortedTable): display message when no items (#2389)
Fixes #2388

Also:
- pagination is hidden when only one page (of non filtered items)
- filter is hidden when there are items
2017-09-29 11:07:47 +02:00
Olivier Lambert
06704ce467 fix(i18n): use utf-8 dots instead of 3 dots for ellipsis. Fixes #2391 2017-09-29 10:52:15 +02:00
Julien Fontanet
8524db2903 chore(SortedTable): avoid creating functions in render for rowAction (#2390) 2017-09-29 10:01:05 +02:00
Julien Fontanet
60df3bc633 feat(SortedTable): ability to select ALL items (#2375)
Fixes #2324
2017-09-28 16:24:57 +02:00
Julien Fontanet
5014b95206 feat(SortedTable): extend checkbox hitbox to the whole cell (#2385)
Fix #2329
2017-09-28 11:48:29 +02:00
Olivier Lambert
a2464fa968 feat(changelog): changelog for 5.13 release 2017-09-28 10:56:23 +02:00
Pierre Donias
033153c8b9 feat(VM/disks): handle add/remove VDI with self service (#2369)
Fixes #2348
2017-09-27 17:37:00 +02:00
Pierre Donias
a74a857ffe feat(remotes/new): warning message when SMB type is selected (#2384)
Fixes #2316
2017-09-27 16:42:55 +02:00
Julien Fontanet
f0fe369cfd fix(new/sr): loading with concurrent tasks (#2381) 2017-09-27 11:41:01 +02:00
Julien Fontanet
457ba5f24c feat(new/sr): auto select single IQN/LUN (#2380) 2017-09-27 11:13:43 +02:00
Julien Fontanet
d41b04313a chore(SelectIqn): cleaner code (#2378) 2017-09-27 10:47:51 +02:00
Olivier Lambert
34be34e7b3 fix(new-sr): fix lun detection (#2377)
Fixes #2374
2017-09-27 10:39:18 +02:00
Julien Fontanet
dbc9fdcfa6 feat(SortedTable): add link to filter syntax doc (#2364)
Fixes #2305
2017-09-26 14:07:51 +02:00
Olivier Lambert
76b20f0fb6 fix(sparklines): pin to react-sparklines 1.6.0 to fix #2370 (#2373) 2017-09-26 10:30:17 +02:00
Julien Fontanet
80ca2052c2 feat(SortedTable): new filterUrlParam prop (#2366)
Fixes #2301
2017-09-25 17:37:33 +02:00
Julien Fontanet
3e5d8be507 feat(settings/servers): use SortedTable (#2365)
Fixes #2340
2017-09-25 12:14:59 +02:00
Julien Fontanet
114e5e1fa0 fix(xo): correctly return the promises
Fixes #2347
2017-09-13 11:42:05 +02:00
Julien Fontanet
c38d4e275b chore(package): update dependencies 2017-09-12 13:24:10 +02:00
Julien Fontanet
8cc9dea9aa chore(package): update dependencies 2017-09-11 13:04:45 +02:00
Nicolas Raynaud
d3dcf6d305 XOSAN: fix issue where displayed gluster size could be wrong. 2017-09-08 09:50:25 +02:00
Julien Fontanet
02439bd23d fix(SortedTable): clicking on indeterminate master unselect all 2017-09-01 16:18:05 +02:00
Julien Fontanet
a9eb1f3d27 5.12.0 2017-08-31 18:06:42 +02:00
Olivier Lambert
9a0544c4aa feat(changelog): add changelog for 5.12 2017-08-31 17:59:25 +02:00
Pierre Donias
31c365313b feat(xosan): beta 2 (#2331) 2017-08-31 17:56:53 +02:00
Pierre Donias
b44017ca95 feat(pool/actions): add SR, add VM and disconnect server (#2308)
Fixes #2307
2017-08-31 17:50:59 +02:00
Julien Fontanet
289112af27 feat(SortedTable): support range selection with shift key (#2328)
Fixes #2323
2017-08-31 17:25:48 +02:00
Julien Fontanet
4d2dc4eece feat(backups): add retention param for CR (#2325)
Fixes #1692
2017-08-31 17:21:56 +02:00
Pierre Donias
712101d8d6 feat(render-xo-item): icon color to show if PIF is physically connected (#2327)
Fixes #2326
2017-08-30 17:53:10 +02:00
Pierre Donias
828ba5d448 fix(ActionBar): pending prop & undefined prop access (#2322) 2017-08-28 15:54:01 +02:00
Olivier Lambert
03a2ff8e8c fix(migrate): re-display migrate button for offline VMs (#2321) 2017-08-28 15:23:33 +02:00
badrAZ
75487203cf fix(job/log): remove unused function (#2319) 2017-08-24 15:24:21 +02:00
badrAZ
6ad751f079 feat(SortedTable): add grouped actions feature (#2288)
Fixes #2276
2017-08-24 15:23:40 +02:00
Pierre Donias
78ddad839e feat(home): resource sets filter dropdown (#2314)
Fixes #2303
2017-08-17 17:03:33 +02:00
Pierre Donias
812cecdcc4 fix(groups): "Add user" selector should be empty after adding a user (#2312)
Fixes #2196
2017-08-17 10:48:24 +02:00
Pierre Donias
4b49da7d8f feat(srs/advanced): total VDIs to coalesce (#2309)
Fixes #2300
2017-08-16 18:08:46 +02:00
Pierre Donias
fc8c37d66c fix(home): set to page 1 when filter changes (#2311)
Fixes #2310
2017-08-16 17:42:26 +02:00
Julien Fontanet
0d618e6477 fix(sr/disks): fix links
Thanks @Danp2 :)
2017-08-14 11:29:58 +02:00
badrAZ
d7e2b12d3d feat(tasks): add pool filter (#2295)
Fixes #2293
2017-08-11 18:01:08 +02:00
badrAZ
2ae4ed3999 fix: display "loading" while fetching objects (#2294)
Fixes  #2285
2017-08-11 17:47:12 +02:00
badrAZ
eaaf70e52e feat(logs): add a call state filter (#2290)
Fixes #2246
2017-08-11 16:07:19 +02:00
Julien Fontanet
bb4ebcd198 chore(sr/disks): fix link to snapshot 2017-08-09 17:40:16 +02:00
Julien Fontanet
a8b7431a02 feat(card/Card): allow passing extra props 2017-08-09 14:27:07 +02:00
Julien Fontanet
a5ec70f7fa chore(card/Card): remove unused propType 2017-08-09 14:26:50 +02:00
Julien Fontanet
75dfdd4854 fix(sr/disks): link to dashboard/health if snapshot parent is missing 2017-08-09 14:26:06 +02:00
badrAZ
3f29dd129f fix(vm/action-bar): no migrate when VM is halted (#2291)
Fixes #2233
2017-08-08 14:53:36 +02:00
Julien Fontanet
7b19341406 fix(xo/createSubscription): race condition on unsubscribe
Issue if the sole subscriber unsubscribe before the call finishes
2017-08-08 10:15:50 +02:00
badrAZ
838ad58946 feat(pool): ability to designate a new master (#2289)
Fixes #2213
2017-08-03 12:38:19 +02:00
Julien Fontanet
ec4a7325da 5.11.0 2017-07-31 18:26:18 +02:00
Olivier Lambert
efbd588c9e feat(objects): remove a useless br (#2283) 2017-07-31 17:49:00 +02:00
Olivier Lambert
e535e064fa feat(changelog): add changelog for 5.11 version 2017-07-31 11:23:24 +02:00
Pierre Donias
448068178b feat(action-bar): implement as standard JSX (#2281) 2017-07-28 17:26:16 +02:00
Julien Fontanet
7308d9ca96 fix(xo): fix API method name 2017-07-27 10:50:46 +02:00
badrAZ
e642f54815 feat(SR/advanced): display the non-healthy vdi chains (#2273)
Fixes #2178
2017-07-26 14:03:55 +02:00
badrAZ
ebb6cb17ea fix: a user with ACLs should not be able to create a VM (#2220)
Fixes #2191 #2202
2017-07-24 11:32:59 +02:00
Pierre Donias
08a0aa9f98 fix(new-vm): disks' device property (#2271)
- existingDisks: use vbd.position as the device
- new VDIs: do not assign any device, xo-server will do it
2017-07-21 17:20:17 +02:00
Pierre Donias
8a933c98e3 fix: multiple fixes (#2272) 2017-07-21 10:36:14 +02:00
Julien Fontanet
363b22bffe feat(logs): display duration for each call (#2266) 2017-07-07 12:19:18 +02:00
Julien Fontanet
79a85659aa 5.10.5 2017-07-05 17:36:31 +02:00
Pierre Donias
eca145e113 chore(intl/locales/fr): update and new translations (#2260) 2017-07-04 14:44:52 +02:00
Julien Fontanet
04b75fb3b3 chore(intl/locales/fr): update translation 2017-07-04 12:21:27 +02:00
Julien Fontanet
582e220a02 chore(intl/messages): remove extraneous space 2017-07-04 12:21:07 +02:00
Julien Fontanet
2e87abefc4 fix(react-novnc): use correct port 2017-07-04 11:40:34 +02:00
Julien Fontanet
5ea19ee56f chore(intl/locales/es): update (#2256) 2017-07-03 16:24:04 +02:00
Julien Fontanet
2ee733399e 5.10.4 2017-06-30 19:34:31 +02:00
Julien Fontanet
73f228c719 Merge branch 'next-release' into stable 2017-06-30 19:34:19 +02:00
Julien Fontanet
ba79673715 5.10.3 2017-06-30 19:32:53 +02:00
Julien Fontanet
86b0962063 fix(react-novnc): url.port is not undefined when empty 2017-06-30 19:32:08 +02:00
Julien Fontanet
e2a482a6ca 5.10.2 2017-06-30 19:15:43 +02:00
Julien Fontanet
8924444cc1 fix(react-novnc): use correct var (sic) 2017-06-30 19:14:35 +02:00
Julien Fontanet
8e83b0ffd2 5.10.1 2017-06-30 19:12:55 +02:00
Julien Fontanet
451384bcdc fix(react-novnc): default ports if missing 2017-06-30 19:12:17 +02:00
Julien Fontanet
b733164c50 5.10.0 2017-06-30 18:27:46 +02:00
badrAZ
72d9d8ba86 feat(backups): optional VDI→SR mapping (#2201)
Fixes #2070
2017-06-30 18:27:31 +02:00
Olivier Lambert
7c7646c65c feat(changelog): add the last changes for 5.10 release 2017-06-30 17:37:47 +02:00
Olivier Lambert
b1c087451e feat(console): remove the tip regarding console layout issues (#2251) 2017-06-30 17:30:53 +02:00
Nicolas Raynaud
f0f72f3bdd update noVNC to latest upstream version (#1780)
Fixes #404
2017-06-30 16:55:42 +02:00
badrAZ
0ab3267541 feat(self-service): improve IP pool UI (#2228)
Fixes #2203
2017-06-30 16:38:01 +02:00
badrAZ
995e76d323 feat(job/log): add more details on a backup (#2245)
Fixes #2239
2017-06-30 16:33:31 +02:00
Olivier Lambert
62a9f805c2 feat(changelog): add one enhancement 2017-06-30 13:43:37 +02:00
Olivier Lambert
84ea95a641 feat(changelog): initial changelog for 5.10 release 2017-06-30 10:08:44 +02:00
Olivier Lambert
316de42cd9 feat(host): forget host. Fixes #1934 (#2244)
feat(host): forget host. Fixes #1934
2017-06-29 12:27:47 +02:00
badrAZ
bc2256fc86 fix(vm/action-bar): always display migrate button (#2232)
Fixes #2212
2017-06-23 14:50:43 +02:00
Julien Fontanet
f0d85f4c4e fix(vm): remove unused imports 2017-06-20 19:19:29 +02:00
Julien Fontanet
1801f9cb06 feat(Home): display total disk size for each VM 2017-06-20 19:07:57 +02:00
Julien Fontanet
4be018ad15 feat(selectors/createSumBy): sum collection of items 2017-06-20 17:51:47 +02:00
Julien Fontanet
5dcf060975 chore(selectors/createPicker): avoid running collection wrapper when possible 2017-06-20 17:51:00 +02:00
Julien Fontanet
59f6b1f0c8 fix(StateButton): missing semicolons in CSS 2017-06-20 15:19:51 +02:00
Julien Fontanet
ae38a85b19 chore(package): update dependencies 2017-06-20 15:08:41 +02:00
badrAZ
324dbbcfc8 fix(modal/alert): resolve on close (#2224)
Fixes #2222
2017-06-20 15:08:33 +02:00
Julien Fontanet
9770b77df4 feat(sr/disks): improve filters 2017-06-20 10:41:50 +02:00
Julien Fontanet
0f91de389a fix(Health): check VBDs for orphaned VDI snapshots
VDI snapshots attached to a VM are not considered orphaned.
2017-06-19 16:41:55 +02:00
Julien Fontanet
7f5a623b37 feat(sr/general): display disks size 2017-06-08 11:59:44 +02:00
Julien Fontanet
c7cf73ff05 fix(sr/disks): difference between no VM and unknown VM 2017-06-08 11:59:08 +02:00
Julien Fontanet
4aab425cef 5.9.1 2017-06-08 10:23:34 +02:00
Julien Fontanet
0d9666639f fix(sr): unused imports 2017-06-06 17:08:12 +02:00
Julien Fontanet
6c26c09685 fix(sr/general): show VDI snaphots 2017-06-06 16:57:56 +02:00
Julien Fontanet
819f650b48 chore(sr/general): better retrieve VM associated to VDI 2017-06-06 16:57:56 +02:00
Julien Fontanet
353eba6365 fix(sr/disks): show the correct attached VM for snapshots 2017-06-06 16:57:55 +02:00
Julien Fontanet
063302b91d feat(renderXoUnknownItem): expose it 2017-06-06 16:57:55 +02:00
Julien Fontanet
562b51bc2f feat(SortedTable): can accept component instead of itemRenderer 2017-06-06 16:57:55 +02:00
Julien Fontanet
e33a6f9a05 chore(xo): use tap() from promise-toolbox 2017-06-05 15:50:16 +02:00
Danp2
b9db4e7704 fix(xo/deleteGroup): properly handle confirm rejection (#2197)
Resolve issue with canceling / exiting dialog
2017-06-05 15:48:31 +02:00
Julien Fontanet
3270d9c3a7 chore(plugins): coding style 2017-06-02 16:57:32 +02:00
Julien Fontanet
6d7399f96c fix(plugins): remove collpase button if not configurable 2017-06-02 16:57:32 +02:00
Julien Fontanet
886ef87bc5 fix(plugins): primary style on save button 2017-06-02 16:57:32 +02:00
Julien Fontanet
1e5dc9efe7 fix(json-schema-input/string): consider empty value as undefined (#2192) 2017-06-02 16:21:30 +02:00
Julien Fontanet
28ec66bf3b fix(backups): handle object values without id prop 2017-06-02 12:11:55 +02:00
Julien Fontanet
9199784a23 5.9.0 2017-05-31 18:14:51 +02:00
Pierre Donias
c7e447db6f feat(host): update patches when joining pool (#2187)
Fixes #878
2017-05-31 17:59:11 +02:00
Pierre Donias
f81615f8b6 feat(dashboard/health): VDIs attached to control domain (#2183)
Fixes #2126
2017-05-31 16:33:37 +02:00
badrAZ
12caceb02b feat: start a VM even when forbidden (#2161)
Fixes #2119
2017-05-31 16:05:57 +02:00
Julien Fontanet
30f71ab444 feat(selectors/createDoesHostNeedRestart): use host.rebootRequired (#2179) 2017-05-31 15:33:05 +02:00
Pierre Donias
fe04481ca3 feat(xo): subscribe to missing patches instead of explicitly checking (#2182) 2017-05-31 15:02:05 +02:00
Pierre Donias
7766e8edcd Better createDoesHostNeedRestart selector 2017-05-31 14:54:07 +02:00
Olivier Lambert
31d417c9d3 feat(changelog): added info for 5.9 release 2017-05-31 13:46:29 +02:00
Pierre Donias
5ed29197cf Use host.rebootRequired boolean 2017-05-30 16:26:32 +02:00
Pierre Donias
ff5f3e12d3 feat(selectors/createDoesHostNeedRestart): use host.patchesRequiringReboot
Fixes #2124
2017-05-30 16:26:32 +02:00
badrAZ
240180405c fix(job/logs): correctly extract vm id from returned value (#2167) 2017-05-30 15:51:37 +02:00
badrAZ
edca6495fc feat(self-service): add button "Select all" to the selects (#2181) 2017-05-30 12:39:20 +02:00
BCedric
8a9b753b01 feat(host/patches): advise to patch from pool (#2130)
Fixes #2057
2017-05-29 14:52:54 +02:00
Julien Fontanet
445fc696c9 fix(backup/new): clarify enabled setting (#2177) 2017-05-29 10:39:15 +02:00
Julien Fontanet
492e2362be chore(utils/firstDefined): fix comment, null is considered defined 2017-05-26 15:50:28 +02:00
badrAZ
1acee209be feat(backup/new): DR previous backups can be removed first (#2173)
Fixes #2157
2017-05-26 13:34:16 +02:00
Olivier Lambert
6785c48709 feat(tasks): display task description if it exists (#2172)
Fixes #2125
2017-05-25 12:45:33 +02:00
Olivier Lambert
808e674503 feat(menu): hide About entry if non-admin (on XOA) (#2170) 2017-05-24 17:36:13 +02:00
Julien Fontanet
6b2650282d 5.8.3 2017-05-23 18:53:10 +02:00
Julien Fontanet
475be2ee30 fix(vm/advanced): behave with missing container 2017-05-23 18:52:39 +02:00
Julien Fontanet
12e1da4ef2 5.8.2 2017-05-23 18:24:21 +02:00
Julien Fontanet
780d072bb7 fix(new/vm): check pool is defined (#2169)
Fixes #2168
2017-05-23 17:38:07 +02:00
Julien Fontanet
f7e5a5cf92 fix(Icon): prop-types → prop-types-decorator 2017-05-17 15:49:34 +02:00
Nicolas Raynaud
3574c8de5c fix(package): update react-select to 1.0.0-rc.4 (#2150)
Fixes #2142
2017-05-17 15:46:04 +02:00
Julien Fontanet
b09ab4d403 fix(Button): fix @propTypes() use 2017-05-17 15:30:36 +02:00
Olivier Lambert
1997f4af51 feat(host): add RAM usage for memory bar in tooltip. Fixes #2149 2017-05-16 21:33:40 +02:00
Danp2
347cd063a3 Fix scanFilesError (#2153) 2017-05-16 21:08:46 +02:00
Olivier Lambert
74a4519a33 fix(i18n): English typo 2017-05-16 15:20:52 +02:00
BCedric
20acf7cfb2 feat(vm/general): display when the VM was last running (#2147)
Fixes #1613
2017-05-16 15:16:44 +02:00
Julien Fontanet
99bc34b2da fix(form/toggle): remove debug trace 2017-05-15 16:48:50 +02:00
Julien Fontanet
f65b5e3ddd feat(settings/server): add click for info on error icon 2017-05-15 16:48:09 +02:00
Julien Fontanet
dc10492b84 fix(xo/connectServer): refresh subscription also in case of error 2017-05-15 16:48:08 +02:00
Julien Fontanet
6f7c10537b fix(vm/advanced): add missing key prop 2017-05-15 16:48:08 +02:00
Julien Fontanet
7f503cfc21 chore(form/toggle): simplify implementation 2017-05-15 16:48:08 +02:00
Julien Fontanet
9dbef0c20a chore(Icon): use propTypes decorator 2017-05-15 16:48:08 +02:00
Julien Fontanet
923166b4e3 feat(Icon): pass extraneous props down 2017-05-15 16:48:08 +02:00
Julien Fontanet
b420128e40 chore(settings/servers): remove useless styles 2017-05-15 16:48:08 +02:00
Julien Fontanet
7776a6ce23 5.8.1 2017-05-12 16:13:44 +02:00
Julien Fontanet
8db949734a feat(settings/servers): improve self-signed error 2017-05-12 16:07:49 +02:00
badrAZ
bb5bdfb9b2 feat(settings/servers): allow unauthorized certificates (#2148)
Fixes #2138
2017-05-12 12:01:08 +02:00
BCedric
9fac3ecd81 feat(backup/file-restore): explicit compatible backups (#2146) 2017-05-11 14:59:31 +02:00
BCedric
8a84cc2627 fix: display when host is disabled (#2121)
Fixes #2098
2017-05-09 17:16:38 +02:00
Julien Fontanet
61179ec67d feat(prop-types): can also be used to set context types 2017-05-09 14:33:15 +02:00
badrAZ
59fc5955ba fix(vm/advanced): affinity host selector (#2143)
Do not remove the current affinity host from the options.

Fixes #2141.
2017-05-09 10:57:31 +02:00
Julien Fontanet
e853ba6244 chore(BaseComponent): use explicit tests 2017-05-07 22:17:20 +02:00
badrAZ
fb40ae7264 feat(vm): ability to choose the cores per socket when creating or editing a VM (#2127)
Fixes #130
2017-05-04 16:12:17 +02:00
badrAZ
f629047be2 chore(vm/new-vm): use _linkState instead of _getOnChange (#2134) 2017-05-04 16:06:50 +02:00
Olivier Lambert
278d8adf1b fix(dashboard): compute correctly the total SR size and used space (#2132)
Fixes #2123
2017-05-03 17:12:58 +02:00
Olivier Lambert
87087d55aa feat(sr): also display unmanaged VDIs (#2131) 2017-05-03 17:07:03 +02:00
Julien Fontanet
46e95fe7eb feat(backup/new): min timeout is 1s 2017-04-28 22:30:32 +02:00
Julien Fontanet
090c9ea4d7 5.8.0 2017-04-28 16:35:45 +02:00
Olivier Lambert
647eb7299e feat(changelog): update changelog 2017-04-28 16:06:47 +02:00
Olivier Lambert
027652e80a feat(changelog): release 5.8 2017-04-28 11:35:40 +02:00
badrAZ
185d380c36 feat(vm/advanced): ability to switch between Cirrus/Standard VGA adapters (#531) (#2091)
Fixes #158
2017-04-27 17:07:54 +02:00
Olivier Lambert
9008b5c4e7 fix(dashboard): flip usage/total. Fixes #2115 2017-04-26 16:42:40 +02:00
Julien Fontanet
f5ad59803e fix(Wizard): take care of falsy children 2017-04-26 12:43:08 +02:00
Pierre Donias
81d1d7ba13 feat(XOSAN): ask user to restart toolstacks after pack installation (#2114) 2017-04-25 16:29:16 +02:00
Julien Fontanet
3328e71805 fix(backup/new): correctly edit schedule.enabled 2017-04-25 15:04:08 +02:00
Julien Fontanet
d7e3dbac26 fix(spelling): VDB → VBD 2017-04-25 14:32:46 +02:00
Olivier Lambert
905182bf2e feat(vm/disks): boot order only for HVMs and devices cannot be non-bootable (#2113)
Fixes #2015
2017-04-25 14:31:41 +02:00
Julien Fontanet
a0146290ee fix(package): downgrade resect to 2.5.4
Since reselect 3, selectors are pure which is incompatible with our use of inputs via closures:

```js
_getFoo = createSelector(
  () => this.props.bar,
  () => this.state.baz,
  (bar, baz) => /* ... */
)
```
2017-04-25 14:14:04 +02:00
Julien Fontanet
173aa22432 fix(intl/messages): correctly format numbers (#2112) 2017-04-25 12:29:56 +02:00
Julien Fontanet
9e5b871ebe chore(Wizard): use React.Children.map 2017-04-25 12:00:44 +02:00
Julien Fontanet
8824ce55ec chore(package): update some dependencies 2017-04-25 12:00:44 +02:00
Olivier Lambert
155edf5533 feat(dashboard): fix power test and remove users link for non-admins (#2111)
Fixes #2108
2017-04-25 12:00:34 +02:00
Olivier Lambert
6d06e1f89d feat(Menu): remove About if non admins (#2110)
Fixes #2109
2017-04-25 11:29:47 +02:00
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
Julien Fontanet
f9a3ad14d1 5.6.1 2017-02-23 18:39:04 +01:00
Pierre Donias
1b86f533f7 feat(XOSAN) (#1955) 2017-02-23 18:34:20 +01:00
greenkeeper[bot]
46416fb026 chore(package): update notifyjs to version 3.0.0 (#1940)
https://greenkeeper.io/
2017-02-15 12:41:38 +01:00
Julien Fontanet
54ed37c95d chore: update yarn.lock 2017-02-14 15:21:44 +01:00
Julien Fontanet
fd79b47d9e fix(tasks): do not break on unknown host 2017-02-14 15:13:37 +01:00
greenkeeper[bot]
be8333824b chore(package): update browserify to version 14.1.0 (#1938)
https://greenkeeper.io/
2017-02-14 09:05:21 +01:00
Julien Fontanet
55daffc791 fix(pool): predicate for host to add is now dynamic (#1923) 2017-02-07 11:17:33 +01:00
Pierre Donias
375baf7fe5 feat(home): link to VM's resource set (#1920)
Fixes #1905
2017-02-01 17:01:40 +01:00
Pierre Donias
815e74c93c feat(vm): allow snapshot with ACLs (#1916)
Fixes #1865
2017-02-01 15:35:54 +01:00
Pierre Donias
547d6fbc93 feat(pool/advanced): install supplemental pack on all pool's hosts (#1910)
See #1896
2017-02-01 13:08:16 +01:00
greenkeeper[bot]
b45a4b9e6c chore(package): update chartist-plugin-legend to version 0.6.1 (#1915)
https://greenkeeper.io/
2017-02-01 09:21:28 +01:00
greenkeeper[bot]
3436d0256a chore(package): update modular-css to version 3.0.0 (#1914)
https://greenkeeper.io/
2017-01-31 08:35:26 +01:00
greenkeeper[bot]
2627cfd426 chore(package): update react-chartist to version 0.12.0 (#1912)
https://greenkeeper.io/
2017-01-30 10:53:38 +01:00
Olivier Lambert
34b18c00a1 fix(lang): button verbiage. Fixes #1911 2017-01-30 10:07:00 +01:00
Julien Fontanet
e13af7f5f0 5.6.0 2017-01-27 16:41:29 +01:00
Nicolas Raynaud
ca08613292 fix(VM import): behave if network is not an object (#1908) 2017-01-27 13:03:30 +01:00
Pierre Donias
4ab63591a0 feat(host/advanced): install a supplemental pack (#1895)
Fixes #1460
2017-01-25 16:08:45 +01:00
Pierre Donias
5b4f98b03b fix(scheduling): improve TimePicker UI (#1898)
Fixes #1893
2017-01-25 15:59:03 +01:00
Pierre Donias
f396d61633 feat(settings/logs): easy bug reporting from errors (#1907)
See #1602
2017-01-25 15:06:18 +01:00
Julien Fontanet
9f8c0c8cdf fix(scheduling): month range is 0-11 not 1-12 (#1894)
* fix(scheduling): month range is 0-11 not 1-12

* fix(scheduling): remove unused import
2017-01-19 17:46:37 +01:00
Julien Fontanet
198777ffab fix(xo): improve plugin errors 2017-01-19 16:42:38 +01:00
Pierre Donias
29c5ca1132 feat(host/advanced): display installed supplemental packs (#1890)
Fixes #1506
2017-01-16 18:03:57 +01:00
Pierre Donias
05d6f3d1ed feat(iso-device): show DVDs in ISO selector (#1889)
Fixes #1880
2017-01-16 17:06:56 +01:00
Olivier Lambert
536e82de3d feat(readme): add live chat link and icon 2017-01-16 14:54:57 +01:00
Pierre Donias
c59be7c315 fix(backup/new): smart backup pool filter (#1888)
Leaving the pool filter empty now means no filtering on pools, it should be clearer.

See #1885
2017-01-16 14:49:50 +01:00
Pierre Donias
b327bb5bd0 feat(backup/file-restore): restore multiple files (#1886)
Fixes #1877
2017-01-16 09:33:30 +01:00
Julien Fontanet
a3103587f5 chore(package): use husky instead of ghooks 2017-01-11 10:07:16 +01:00
Julien Fontanet
1bb11b574f chore: update yarn.lock 2017-01-11 10:05:18 +01:00
greenkeeper[bot]
405efe6a31 chore(package): update react-redux to version 5.0.0 (#1825)
https://greenkeeper.io/
2017-01-11 09:22:07 +01:00
Pierre Donias
73663c3703 fix(locales/fr): multiple fixes (#1876) 2017-01-10 17:42:41 +01:00
Julien Fontanet
421ee7125b chore: update yarn.lock 2017-01-09 10:12:31 +01:00
Julien Fontanet
1a6166b63c chore: update yarn.lock 2017-01-09 10:04:37 +01:00
Julien Fontanet
3828e75b7d fix(package): use bootstrap 4.0.0-alpha5 exactly
Bootstrap 4.0.0-alpha6 is not compatible, we'll migrate with beta1.

See #1871
2017-01-09 10:02:43 +01:00
greenkeeper[bot]
154da142c7 chore(package): update gulp-sourcemaps to version 2.2.3 (#1874)
https://greenkeeper.io/
2017-01-09 09:58:50 +01:00
Pierre Donias
312cd60dd1 feat(settings/servers): error handling (#1868)
Fixes #1833
2017-01-06 16:38:24 +01:00
greenkeeper[bot]
6bf522f72f chore(package): update promise-toolbox to version 0.8.0 (#1870)
https://greenkeeper.io/
2017-01-06 11:39:58 +01:00
Pierre Donias
a844f8d459 fix(backup/new): prevent negative depth values (#1869)
Fixes #1851
2017-01-06 11:12:56 +01:00
Julien Fontanet
8ee206174b fix(vm/advanced): PV args edition for all PV VMs
Currently only available if there are already PV args.
2017-01-05 16:51:56 +01:00
Olivier Lambert
1a08e24a5c feat(pool): harmonize links to hosts and storages (#1867)
Fixes #1864
2017-01-05 14:29:06 +01:00
greenkeeper[bot]
086cd0e038 chore(package): update modular-css to version 2.0.0 (#1862)
https://greenkeeper.io/
2017-01-04 09:43:32 +01:00
Julien Fontanet
42d123318c chore(pakcage): upgrade react/react-dom to v15.4.1 2017-01-03 15:28:53 +01:00
Julien Fontanet
89f160317c 5.5.3 2017-01-03 15:19:09 +01:00
Julien Fontanet
9ccd1a0362 chore: update yarn.lock 2017-01-03 15:18:27 +01:00
Pierre Donias
d116d014bc fix(host/network): PIF deletion (#1861)
Fixes #1853
2017-01-03 15:13:44 +01:00
Pierre Donias
7956cabcf4 feat(pool): combined stats tab (#1848)
Fixes #1324
2017-01-03 14:58:22 +01:00
Pierre Donias
36c61ad357 feat(vm/snapshots): icon to indicate if a snapshot is quiesce (#1860)
Fixes #1858
2017-01-03 10:54:58 +01:00
greenkeeper[bot]
25d60360d5 chore(package): update modular-css to version 1.0.0 (#1859)
https://greenkeeper.io/
2017-01-03 09:38:00 +01:00
Olivier Lambert
1e5579e3ad feat(icons): use microchip for the CPU icon 2017-01-02 17:14:05 +01:00
Julien Fontanet
77d43b2280 fix(settings/logs): correctly refresh users 2017-01-02 16:36:09 +01:00
Julien Fontanet
33e8929e8b fix(settings/groups): avoid error when adding a user 2017-01-02 16:36:02 +01:00
Julien Fontanet
b79fa9cb9f feat(backup/overview): sort by start if not end
Avoid unfinished (broken) jobs to stay at the top.
2017-01-02 16:34:36 +01:00
Olivier Lambert
a2812a85bd feat(vm): redirect to home after converting VM to template (#1857)
Fixes #1855
2017-01-02 14:12:33 +01:00
Olivier Lambert
e8ff46a8ba fix(health): add link from SR to hosts and not only for pools. Fixes #1850 (#1856) 2017-01-02 11:29:32 +01:00
Julien Fontanet
351c01d642 fix(intl/messages): typo
Fix #1849
2016-12-24 09:15:54 +01:00
Julien Fontanet
e333b1d083 chore(package): use new shorter paths for Jest serializer 2016-12-22 16:58:26 +01:00
Julien Fontanet
5ad49de642 5.5.2 2016-12-22 12:27:03 +01:00
Fabrice Marsaud
b45bb5c144 fix(xoa-updater): use the new source info (#1846) 2016-12-22 12:26:02 +01:00
Julien Fontanet
9402596f69 5.5.1 2016-12-22 11:18:32 +01:00
Fabrice Marsaud
096687ae2c fix(xoa-updates): also refresh on plan change (#1843) 2016-12-22 11:16:45 +01:00
Julien Fontanet
210b5de992 fix(backup/file-restore-modal): fetched timestamps are in seconds
Follow up to for #1840.
2016-12-20 17:10:55 +01:00
Julien Fontanet
f742fdbf1b fix(backup/file-restore): fetched timestamps are in seconds
Fixes #1840
2016-12-20 16:49:37 +01:00
Pierre Donias
e7026c522d fix(editable/XoSelect): update value before saving (#1835) 2016-12-20 13:54:22 +01:00
Julien Fontanet
c21fc4beda 5.5.0 2016-12-20 13:39:05 +01:00
Julien Fontanet
edf6fe782e chore: update yarn.lock 2016-12-20 13:36:39 +01:00
Pierre Donias
3cbb6c4a98 feat(backup): implement file restore (#1838)
Fixes #1590
2016-12-20 13:34:59 +01:00
Olivier Lambert
568a50acc5 feat(changelog): update changelog 2016-12-19 17:59:14 +01:00
Pierre Donias
fbcb756cef fix(backup/new): remove "Only metadata" option (#1837)
Fixes #1818
2016-12-19 17:53:04 +01:00
Pierre Donias
81eb4ba4f9 fix(form/Select): make text wrap when too long (#1836)
Fixes #1832
2016-12-19 17:15:03 +01:00
Olivier Lambert
0cc14d2ab8 fix(intl): fix the place holder for NFS path, removing the initial slash 2016-12-19 13:48:49 +01:00
greenkeeper[bot]
6aedadc982 chore(package): update jest to version 18.0.0 (#1831)
https://greenkeeper.io/
2016-12-15 12:34:05 +01:00
Olivier Lambert
a8d10dab3c feat(changelog): changelog for 5.5 release 2016-12-15 12:19:35 +01:00
Pierre Donias
1ff6ff1d7a fix(getDefaultNetworkForVif): allow PIFs with no IP (#1830)
Fixes #1788
2016-12-15 10:31:03 +01:00
Pierre Donias
8afe4a85dc fix(form/select-plain-object): make it controlled (#1829) 2016-12-14 17:18:31 +01:00
greenkeeper[bot]
c57fbdce63 chore(package): update index-modules to version 0.2.1 (#1822) 2016-12-12 16:49:29 +01:00
greenkeeper[bot]
bdc0278fd1 chore(package): update gulp-sass to version 3.0.0 (#1820)
https://greenkeeper.io/
2016-12-10 15:07:20 +01:00
Fabrice Marsaud
c3ac8d0587 fix(xoa-updater): propose refresh when xo-web is not up to date (#1815)
Fixes #1801.
2016-12-08 16:24:47 +01:00
Julien Fontanet
f3a5e1e97c feat: yarn integration (#1813) 2016-12-07 14:36:51 +01:00
Julien Fontanet
919aa5fc43 feat(tests): basic tests for grid components 2016-12-07 14:06:20 +01:00
Julien Fontanet
416c98ffd2 chore(tests): use jest instead of ava 2016-12-07 14:06:20 +01:00
Pierre Donias
8094447183 fix(self-service): make it controlled and multiple fixes (#1812)
Fixes:
- VIF IP edition was not possible if the current IP's IP-pool did not exist anymore
- VM creation: removing the pool/resource set in the selector was broken
- Prevent selecting a negative number as an IP pool quantity
- Prevent deleting a resource set that hasn't been created yet
- Remove console.log
2016-12-06 16:43:07 +01:00
Julien Fontanet
575375d3e0 5.4.1 2016-12-05 14:08:56 +01:00
Julien Fontanet
4296ae02dc fix(vm/network): fix IP addresses concatenation 2016-12-05 14:04:43 +01:00
Julien Fontanet
0e40af0515 feat(patch-react): name the patched render function 2016-12-05 14:03:39 +01:00
Julien Fontanet
5d3a0e7a41 fix(patch-react): assign arguments object directly 2016-12-05 10:43:09 +01:00
Julien Fontanet
8ae2aae37a fix(package): update xo-acl-resolver to v0.2.3
It ships a workaround for an issue when a VDI snapshot $snapshot_of is itself.
2016-12-02 16:18:31 +01:00
Pierre Donias
83b3cf406a fix(lang): auto-refresh XO app when changing language (#1809)
Fixes #1800
2016-12-02 13:09:35 +01:00
Julien Fontanet
1643ced4e0 feat: do not break if a component throws 2016-12-01 16:49:36 +01:00
Pierre Donias
b2a1840da7 fix(vm-import): SR selector disable condition (#1808)
Fixes #1804
2016-12-01 15:54:58 +01:00
Pierre Donias
b9f20d1e80 fix(select-objects): allow integer IDs (#1807)
Fixes #1805
2016-12-01 15:39:47 +01:00
Pierre Donias
0c77781be8 feat(backup/smart): tags/pools select all and negation (#1802)
Fixes #1503
2016-12-01 14:56:18 +01:00
Julien Fontanet
83245af1e2 feat: improve debugging in production (#1806)
* feat(build): enable sourcemaps in production

* feat(index): let browsers handle unhandled rejections

They do a much better job to display the error and its trace with sourcemaps.
2016-12-01 14:22:07 +01:00
Julien Fontanet
7db806a461 fix(xo/subcriptions): handle unsubscription during notification 2016-12-01 12:15:12 +01:00
Olivier Lambert
92b15fb1e2 feat(consoles): update tip about console issue to point to XS ticket 2016-11-29 11:00:30 +01:00
Pierre Donias
7b5182111c fix(dashboard/health): message parsing and link to object view (#1796)
Related to #1776
2016-11-29 09:53:19 +01:00
Nicolas Raynaud
82b1b81999 fix(dashboard/overview): graphs on Safari (#1771)
There seems to be a bug in Safari with flex layout, hard coding the height of the graph seems to do the trick.

Fixes #1755
2016-11-29 09:21:04 +01:00
Pierre Donias
f0a430f350 feat(acls): filter object selector (#1791)
Fixes #1515
2016-11-28 12:05:56 +01:00
Olivier Lambert
90f95b7270 feat(i18n): added selector for simplified Chinese 2016-11-28 10:53:41 +01:00
Olivier Lambert
15e6a93fac feat(i18n): added simplified Chinese translation 2016-11-28 10:45:33 +01:00
Julien Fontanet
01541a2577 feat(@autoControlledInput): make controlled inputs able to handle uncontrolled mode
See #1628.
2016-11-24 11:34:21 +01:00
Olivier Lambert
8c70bc0a17 feat(i18n): fix wrong translate key 2016-11-24 10:16:44 +01:00
Nicolas Raynaud
9d96074604 chore(package): update react-shortcuts to v1.3.1 (#1792)
Fix #1691
2016-11-24 09:58:56 +01:00
Julien Fontanet
114a4028f4 fix: coding style fixes 2016-11-24 09:57:02 +01:00
Julien Fontanet
b342a4ba17 5.4.0 2016-11-23 11:09:24 +01:00
Olivier Lambert
fcbf037619 feat(changelog): prepare for the 5.4 release 2016-11-22 18:23:23 +01:00
Pierre Donias
a8e4ab433d feat(backup/new): warning when using delta or continuous rep with XS<6.5 (#1782) 2016-11-22 17:10:26 +01:00
Pierre Donias
6613ba02ab feat(dashboard/health): better formating of alert messages (#1781)
Fixes #1776
2016-11-22 16:41:07 +01:00
Pierre Donias
2af7fde83f fix(dashboard/health): delete orphaned VDIs (#1778)
Fixes #1622
2016-11-21 15:49:23 +01:00
Pierre Donias
19a0d4bc98 feat(timezone-picker): make server timezone less central (#1774)
Fixes #1706
2016-11-21 15:48:01 +01:00
Pierre Donias
9ed49b1f27 fix(menu): non admin users should not see backups and SRs in menu (#1777)
Fixes #1773
2016-11-21 15:45:34 +01:00
Pierre Donias
d56df30a22 feat(host): link to VMs list (#1772)
Fixes #1432
2016-11-18 12:50:43 +01:00
Julien Fontanet
64908068d9 fix: remove Intl polyfill
Safari 10 fixes this issue.

Fixes #1629
2016-11-18 12:40:24 +01:00
Pierre Donias
fe69d59aeb feat(tags): link to home page with tag filter (#1770)
Fixes #1763
2016-11-18 11:31:12 +01:00
Julien Fontanet
b65e737f84 feat(complex-matcher): support matching boolean props
Fixes #1768
2016-11-17 15:28:52 +01:00
Pierre Donias
bd274fdc3c feat(home): can display SRs (#1767)
Fixes #1764
2016-11-17 15:03:46 +01:00
greenkeeper[bot]
ac19249c63 chore(package): update xo-common to version 0.1.0 (#1765)
https://greenkeeper.io/
2016-11-17 05:28:32 +01:00
badrAZ
2abff1fec8 fix(settings/plugins): fix test when no test schema (#1766) 2016-11-16 15:38:48 +01:00
badrAZ
f1a6cfae0d feat(settings/plugins): display plugin test failures (#1754) 2016-11-16 10:53:57 +01:00
greenkeeper[bot]
e43e90ed3c chore(package): update react-chartist to version 0.11.0 (#1761)
https://greenkeeper.io/
2016-11-15 12:58:44 +01:00
greenkeeper[bot]
0ee88fe0dc Update dependencies to enable Greenkeeper 🌴 (#1757)
https://greenkeeper.io/
2016-11-15 11:02:45 +01:00
Pierre Donias
07e7f2e14d fix(menu): jobs should not be available to non-admin users (#1760)
Fixes #1759
2016-11-15 09:46:37 +01:00
Julien Fontanet
366ab95a2f feat(backup,job): ability to set the job owner (#1758)
Fixes #1733
2016-11-14 17:43:02 +01:00
Pierre Donias
ca723068a1 Fix clearable condition. 2016-11-14 17:29:29 +01:00
Pierre Donias
e424a105b3 Minor fixes. 2016-11-14 17:23:05 +01:00
Pierre Donias
32d2f92413 Multiple fixes. 2016-11-14 16:48:33 +01:00
Pierre Donias
898e2ff010 feat(job,backup): handle unexisting job creator
See #1733
2016-11-14 15:32:52 +01:00
Pierre Donias
dfa5e76870 feat(sr/disks): also display VDI snapshots (#1750)
Fixes #1723
2016-11-09 16:38:52 +01:00
Julien Fontanet
c93dd12fae feat(settings/plugins): testing
See #1749
2016-11-09 16:34:29 +01:00
Julien Fontanet
dbb1b1e582 fix(settings/plugins): unloading 2016-11-09 16:19:07 +01:00
Julien Fontanet
76388ee160 feat(utils/Debug): correctly display undefined 2016-11-09 16:18:32 +01:00
Pierre Donias
5ec2eee69a fix(settings/plugins): no plugins message condition (#1746) 2016-11-09 10:08:18 +01:00
Pierre Donias
31875a36fe fix(backup/restore): do not display duplicate tags (#1745)
Fixes #1734
2016-11-08 17:43:01 +01:00
Olivier Lambert
c50598b78e fix(jobs): last slicing fixed for job IDs 2016-11-08 15:37:44 +01:00
Pierre Donias
2f0c81d9ad feat(settings/plugins): message when no plugins (#1744)
Fixes #1670
2016-11-08 15:30:36 +01:00
Olivier Lambert
c22f89c6bb fix(jobs): better slicing of job IDs 2016-11-08 15:29:46 +01:00
Pierre Donias
568a23cd35 feat(pool/networks): change columns layout. Fixes #1696 2016-11-08 15:20:13 +01:00
Pierre Donias
eb7c4c131d fix(xo): add xo-common package (#1742) 2016-11-07 18:18:07 +01:00
Pierre Donias
f0664cd2c7 feat(xo/restartHost-s): trigger error modal if host could not be restarted (#1740)
Fixes #1717
2016-11-07 17:10:02 +01:00
Julien Fontanet
570eb7bc89 chore(package): update jsonrpc-websocket-client to v0.1.1 2016-11-07 16:48:10 +01:00
Pierre Donias
1ee91b4925 feat(backup/overview): display jobs in a SortedTable (#1741)
Fixes #1726
2016-11-07 14:43:16 +01:00
Julien Fontanet
69fee37f00 5.3.2 2016-11-04 11:43:49 +01:00
Julien Fontanet
49be66ae69 fix(xo/deleteSchedule): resolveIds() → resolveId()
Fixes #1737
2016-11-04 10:53:52 +01:00
Julien Fontanet
a0efe6895c fix(jobs/schedules/<id>/edit): correctly pass id prop
Fixes #1736
2016-11-04 10:31:32 +01:00
Julien Fontanet
8ef07e917d fix(jobs/<id>/edit): correctly set the id prop
Fixes #1728
2016-11-04 10:00:55 +01:00
Olivier Lambert
d3995b7bab feat(job): slicing some job ids 2016-11-03 18:00:49 +01:00
Olivier Lambert
c353e71ce7 feat(jobs): slice ids to be more human readable (#1735) 2016-11-03 16:48:48 +01:00
Olivier Lambert
a3570a1c9f feat(pif): use carrier to detect physical link (#1732)
Fixes #1702
2016-11-02 16:59:43 +01:00
Pierre Donias
c593c98e6d fix(settings/acls): display only valid ACLs (#1730) 2016-11-02 14:32:22 +01:00
Olivier Lambert
a4b5b532f2 feat(health): add links to some objects (#1729)
Fixes #1700
2016-11-02 12:29:50 +01:00
Julien Fontanet
6357f23aeb fix(package): use bootstrap 4.0.0-alpha5 2016-10-31 11:26:33 +01:00
Olivier Lambert
01d9b3bd0e feat(home/pools): add links to VMs or hosts and add VM number (#1720)
Fixes #1226
2016-10-28 14:35:32 +02:00
Pierre Donias
6b428f7587 fix(IP pools): behave with missing VIFs (#1716) 2016-10-28 11:57:29 +02:00
Pierre Donias
f829aa76d7 fix(dashboard/stats): requires at least Enterprise plan (#1718) 2016-10-28 11:43:36 +02:00
Fabrice Marsaud
a72051e96f fix(xoa-updater): fix double connection (#1714)
* fix double connection
* fix minified class name display
2016-10-28 10:16:56 +02:00
Pierre Donias
797622ba66 fix(settings/config): export/import should be available to Starter plan (#1715) 2016-10-28 09:52:41 +02:00
253 changed files with 46649 additions and 18166 deletions

12
.eslintrc.js Normal file
View File

@@ -0,0 +1,12 @@
module.exports = {
extends: ['standard', 'standard-jsx'],
globals: {
__DEV__: true,
},
parser: 'babel-eslint',
rules: {
'comma-dangle': ['error', 'always-multiline'],
'no-var': 'error',
'prefer-const': 'error',
},
}

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

4
.prettierrc.js Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
semi: false,
singleQuote: true,
}

View File

@@ -4,9 +4,7 @@ node_js:
#- '4' # npm 3's flat tree is needed because some packages do not
# declare their deps correctly (e.g. chartist-plugin-tooltip)
cache:
directories:
- node_modules
cache: yarn
# Use containers.
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/

View File

@@ -1,19 +1,375 @@
# ChangeLog
## **5.3.1** (2016-10-27)
## **5.15.0** (2017-12-29)
### Enhancements
- Improve backup restore view [\#1609](https://github.com/vatesfr/xo-web/issues/1609)
- Move location of NFS mount point [\#1405](https://github.com/vatesfr/xo-web/issues/1405)
- Modify VLAN of an existing network [\#1092](https://github.com/vatesfr/xo-web/issues/1092)
- Ability to export/import XO config [\#786](https://github.com/vatesfr/xo-web/issues/786)
* VDI resize online method removed in 7.3 [#2542](https://github.com/vatesfr/xo-web/issues/2542)
* Smart replace VDI.pool_migrate removed from XenServer 7.3 Free [#2541](https://github.com/vatesfr/xo-web/issues/2541)
* New memory constraints in XenServer 7.3 [#2540](https://github.com/vatesfr/xo-web/issues/2540)
* Link to Settings/Logs for admins in error notifications [#2516](https://github.com/vatesfr/xo-web/issues/2516)
* [Self Service] Do not use placehodlers to describe inputs [#2509](https://github.com/vatesfr/xo-web/issues/2509)
* Obfuscate password in log in LDAP plugin test [#2506](https://github.com/vatesfr/xo-web/issues/2506)
* Log rotation [#2492](https://github.com/vatesfr/xo-web/issues/2492)
* Continuous Replication TAG [#2473](https://github.com/vatesfr/xo-web/issues/2473)
* Graphs in VM list view [#2469](https://github.com/vatesfr/xo-web/issues/2469)
* [Delta Backups] Do not include merge duration in transfer speed stat [#2426](https://github.com/vatesfr/xo-web/issues/2426)
* Warning for disperse mode [#2537](https://github.com/vatesfr/xo-web/issues/2537)
### Bugs
* VM console doesn't work when using IPv6 in URL [#2530](https://github.com/vatesfr/xo-web/issues/2530)
* Retention issue with failed basic backup [#2524](https://github.com/vatesfr/xo-web/issues/2524)
* [VM/Advanced] Check that the autopower on setting is working [#2489](https://github.com/vatesfr/xo-web/issues/2489)
* Cloud config drive create fail on XenServer < 7 [#2478](https://github.com/vatesfr/xo-web/issues/2478)
* VM create fails due to missing vGPU id [#2466](https://github.com/vatesfr/xo-web/issues/2466)
## **5.14.0** (2017-10-31)
### Enhancements
* VM snapshot description display [#2458](https://github.com/vatesfr/xo-web/issues/2458)
* [Home] Ability to sort VM by number of snapshots [#2450](https://github.com/vatesfr/xo-web/issues/2450)
* Display XS version in host view [#2439](https://github.com/vatesfr/xo-web/issues/2439)
* [File restore]: Clarify the possibility to select multiple files [#2438](https://github.com/vatesfr/xo-web/issues/2438)
* [Continuous Replication] Time in replicated VMs [#2431](https://github.com/vatesfr/xo-web/issues/2431)
* [SortedTable] Active page in URL param [#2405](https://github.com/vatesfr/xo-web/issues/2405)
* replace all '...' with the UTF-8 equivalent [#2391](https://github.com/vatesfr/xo-web/issues/2391)
* [SortedTable] Explicit when no items [#2388](https://github.com/vatesfr/xo-web/issues/2388)
* Handle patching licenses [#2382](https://github.com/vatesfr/xo-web/issues/2382)
* Credential leaking in logs for messages regarding invalid credentials and "too fast authentication" [#2363](https://github.com/vatesfr/xo-web/issues/2363)
* [SortedTable] Keyboard support [#2330](https://github.com/vatesfr/xo-web/issues/2330)
* token.create should accept an expiration [#1769](https://github.com/vatesfr/xo-web/issues/1769)
* On updater error, display link to documentation [#1610](https://github.com/vatesfr/xo-web/issues/1610)
* Add basic vGPU support [#2413](https://github.com/vatesfr/xo-web/issues/2413)
* Storage View - Disk Tab - real disk usage [#2475](https://github.com/vatesfr/xo-web/issues/2475)
### Bugs
* Config drive - Custom config not working properly [#2449](https://github.com/vatesfr/xo-web/issues/2449)
* Snapshot sorted table breaks copyVm [#2446](https://github.com/vatesfr/xo-web/issues/2446)
* [vm/snapshots] Incorrect default sort order [#2442](https://github.com/vatesfr/xo-web/issues/2442)
* [Backups/Jobs] Incorrect months mapping [#2427](https://github.com/vatesfr/xo-web/issues/2427)
* [Xapi#barrier()] Not compatible with XenServer < 6.1 [#2418](https://github.com/vatesfr/xo-web/issues/2418)
* [SortedTable] Change page when no more items on the page [#2401](https://github.com/vatesfr/xo-web/issues/2401)
* Review and fix creating a VM from a snapshot [#2343](https://github.com/vatesfr/xo-web/issues/2343)
* Unable to edit / save restored backup job [#1922](https://github.com/vatesfr/xo-web/issues/1922)
## **5.13.0** (2017-09-29)
### Enhancements
* replace all '...' with the UTF-8 equivalent [#2391](https://github.com/vatesfr/xo-web/issues/2391)
* [SortedTable] Explicit when no items [#2388](https://github.com/vatesfr/xo-web/issues/2388)
* Auto select iqn or lun if there is only one [#2379](https://github.com/vatesfr/xo-web/issues/2379)
* [Sparklines] Hide points [#2370](https://github.com/vatesfr/xo-web/issues/2370)
* Allow xo-server-recover-account to generate a random password [#2360](https://github.com/vatesfr/xo-web/issues/2360)
* Add disk in existing VM as self user [#2348](https://github.com/vatesfr/xo-web/issues/2348)
* Sorted table for Settings/server [#2340](https://github.com/vatesfr/xo-web/issues/2340)
* Sign in should be case insensitive [#2337](https://github.com/vatesfr/xo-web/issues/2337)
* [SortedTable] Extend checkbox click to whole column [#2329](https://github.com/vatesfr/xo-web/issues/2329)
* [SortedTable] Ability to select all items (across pages) [#2324](https://github.com/vatesfr/xo-web/issues/2324)
* [SortedTable] Range selection [#2323](https://github.com/vatesfr/xo-web/issues/2323)
* Warning on SMB remote creation [#2316](https://github.com/vatesfr/xo-web/issues/2316)
* [Home | SortedTable] Add link to syntax doc in the filter input [#2305](https://github.com/vatesfr/xo-web/issues/2305)
* [SortedTable] Add optional binding of filter to an URL query [#2301](https://github.com/vatesfr/xo-web/issues/2301)
* [Home][Keyboard navigation] Allow selecting the objects [#2214](https://github.com/vatesfr/xo-web/issues/2214)
* SR view / Disks: option to display non managed VDIs [#1724](https://github.com/vatesfr/xo-web/issues/1724)
* Continuous Replication Retention [#1692](https://github.com/vatesfr/xo-web/issues/1692)
### Bugs
* iSCSI issue on LUN selector [#2374](https://github.com/vatesfr/xo-web/issues/2374)
* Errors in VM copy are not properly reported [#2347](https://github.com/vatesfr/xo-web/issues/2347)
* Removing a PIF IP fails [#2346](https://github.com/vatesfr/xo-web/issues/2346)
* Review and fix creating a VM from a snapshot [#2343](https://github.com/vatesfr/xo-web/issues/2343)
* iSCSI LUN Detection fails with authentification [#2339](https://github.com/vatesfr/xo-web/issues/2339)
* Fix PoolActionBar to add a new SR [#2307](https://github.com/vatesfr/xo-web/issues/2307)
* [VM migration] Error if default SR not accessible to target host [#2180](https://github.com/vatesfr/xo-web/issues/2180)
* A job shouldn't executable more than once at the same time [#2053](https://github.com/vatesfr/xo-web/issues/2053)
## **5.12.0** (2017-08-31)
### Enhancements
* PIF selector with physical status [#2326](https://github.com/vatesfr/xo-web/issues/2326)
* [SortedTable] Range selection [#2323](https://github.com/vatesfr/xo-web/issues/2323)
* Self service filter for home/VM view [#2303](https://github.com/vatesfr/xo-web/issues/2303)
* SR/Disks Display total of VDIs to coalesce [#2300](https://github.com/vatesfr/xo-web/issues/2300)
* Pool filter in the task view [#2293](https://github.com/vatesfr/xo-web/issues/2293)
* "Loading" while fetching objects [#2285](https://github.com/vatesfr/xo-web/issues/2285)
* [SortedTable] Add grouped actions feature [#2276](https://github.com/vatesfr/xo-web/issues/2276)
* Add a filter to the backups' log [#2246](https://github.com/vatesfr/xo-web/issues/2246)
* It should not be possible to migrate a halted VM. [#2233](https://github.com/vatesfr/xo-web/issues/2233)
* [Home][Keyboard navigation] Allow selecting the objects [#2214](https://github.com/vatesfr/xo-web/issues/2214)
* Allow to set pool master [#2213](https://github.com/vatesfr/xo-web/issues/2213)
* Continuous Replication Retention [#1692](https://github.com/vatesfr/xo-web/issues/1692)
### Bugs
* Home pagination bug [#2310](https://github.com/vatesfr/xo-web/issues/2310)
* Fix PoolActionBar to add a new SR [#2307](https://github.com/vatesfr/xo-web/issues/2307)
* VM snapshots are not correctly deleted [#2304](https://github.com/vatesfr/xo-web/issues/2304)
* Parallel deletion of VMs fails [#2297](https://github.com/vatesfr/xo-web/issues/2297)
* Continous replication create multiple zombie disks [#2292](https://github.com/vatesfr/xo-web/issues/2292)
* Add user to Group issue [#2196](https://github.com/vatesfr/xo-web/issues/2196)
* [VM migration] Error if default SR not accessible to target host [#2180](https://github.com/vatesfr/xo-web/issues/2180)
## **5.11.0** (2017-07-31)
### Enhancements
- Storage VHD chain health [\#2178](https://github.com/vatesfr/xo-web/issues/2178)
### Bug fixes
- No web VNC console [\#2258](https://github.com/vatesfr/xo-web/issues/2258)
- Patching issues [\#2254](https://github.com/vatesfr/xo-web/issues/2254)
- Advanced button in VM creation for self service user [\#2202](https://github.com/vatesfr/xo-web/issues/2202)
- Hide "new VM" menu entry if not admin or not self service user [\#2191](https://github.com/vatesfr/xo-web/issues/2191)
## **5.10.0** (2017-06-30)
### Enhancements
- Improve backup log display [\#2239](https://github.com/vatesfr/xo-web/issues/2239)
- Patch SR detection improvement [\#2215](https://github.com/vatesfr/xo-web/issues/2215)
- Less strict coalesce detection [\#2207](https://github.com/vatesfr/xo-web/issues/2207)
- IP pool UI improvement [\#2203](https://github.com/vatesfr/xo-web/issues/2203)
- Ability to clear "Auto power on" flag for DR-ed VM [\#2097](https://github.com/vatesfr/xo-web/issues/2097)
- [Delta backup restoration] Choose SR for each VDIs [\#2070](https://github.com/vatesfr/xo-web/issues/2070)
- Ability to forget an host (even if no longer present) [\#1934](https://github.com/vatesfr/xo-web/issues/1934)
### Bug fixes
- Cross pool migrate fail [\#2248](https://github.com/vatesfr/xo-web/issues/2248)
- ActionButtons with modals stay in pending state forever [\#2222](https://github.com/vatesfr/xo-web/issues/2222)
- Permission issue for a user on self service VMs [\#2212](https://github.com/vatesfr/xo-web/issues/2212)
- Self-Service resource loophole [\#2198](https://github.com/vatesfr/xo-web/issues/2198)
- Backup log no longer shows the name of destination VM [\#2195](https://github.com/vatesfr/xo-web/issues/2195)
- State not restored when exiting modal dialog [\#2194](https://github.com/vatesfr/xo-web/issues/2194)
- [Xapi#exportDeltaVm] Cannot read property 'managed' of undefined [\#2189](https://github.com/vatesfr/xo-web/issues/2189)
- VNC keyboard layout change [\#404](https://github.com/vatesfr/xo-web/issues/404)
## **5.9.0** (2017-05-31)
### Enhancements
- Allow DR to remove previous backup first [\#2157](https://github.com/vatesfr/xo-web/issues/2157)
- Feature request - add amount of RAM to memory bars [\#2149](https://github.com/vatesfr/xo-web/issues/2149)
- Make the acceptability of invalid certificates configurable [\#2138](https://github.com/vatesfr/xo-web/issues/2138)
- label of VM names in tasks link [\#2135](https://github.com/vatesfr/xo-web/issues/2135)
- Backup report timezone [\#2133](https://github.com/vatesfr/xo-web/issues/2133)
- xo-server-recover-account [\#2129](https://github.com/vatesfr/xo-web/issues/2129)
- Detect disks attached to control domain [\#2126](https://github.com/vatesfr/xo-web/issues/2126)
- Add task description in Tasks view [\#2125](https://github.com/vatesfr/xo-web/issues/2125)
- Host reboot warning after patching for 7.1 [\#2124](https://github.com/vatesfr/xo-web/issues/2124)
- Continuous Replication - possibility run VM without a clone [\#2119](https://github.com/vatesfr/xo-web/issues/2119)
- Unreachable host should be detected [\#2099](https://github.com/vatesfr/xo-web/issues/2099)
- Orange icon when host is is disabled [\#2098](https://github.com/vatesfr/xo-web/issues/2098)
- Enhanced backup report logs [\#2096](https://github.com/vatesfr/xo-web/issues/2096)
- Only show failures when configured to report on failures [\#2095](https://github.com/vatesfr/xo-web/issues/2095)
- "Add all" button in self service [\#2081](https://github.com/vatesfr/xo-web/issues/2081)
- Patch and pack mechanism changed on Ely [\#2058](https://github.com/vatesfr/xo-web/issues/2058)
- Tip or ask people to patch from pool view [\#2057](https://github.com/vatesfr/xo-web/issues/2057)
- File restore - Remind compatible backup [\#1930](https://github.com/vatesfr/xo-web/issues/1930)
- Reporting for halted vm time [\#1613](https://github.com/vatesfr/xo-web/issues/1613)
- Add standalone XS server to a pool and patch it to the pool level [\#878](https://github.com/vatesfr/xo-web/issues/878)
- Add Cores-per-sockets [\#130](https://github.com/vatesfr/xo-web/issues/130)
### Bug fixes
- VM creation is broken for non-admins [\#2168](https://github.com/vatesfr/xo-web/issues/2168)
- Can't create cloud config drive [\#2162](https://github.com/vatesfr/xo-web/issues/2162)
- Select is "moving" [\#2142](https://github.com/vatesfr/xo-web/issues/2142)
- Select issue for affinity host [\#2141](https://github.com/vatesfr/xo-web/issues/2141)
- Dashboard Storage Usage incorrect [\#2123](https://github.com/vatesfr/xo-web/issues/2123)
- Detect unmerged *base copy* and prevent too long chains [\#2047](https://github.com/vatesfr/xo-web/issues/2047)
## **5.8.0** (2017-04-28)
### Enhancements
- Limit About view info for non-admins [\#2109](https://github.com/vatesfr/xo-web/issues/2109)
- Enabling/disabling boot device on HVM VM [\#2105](https://github.com/vatesfr/xo-web/issues/2105)
- Filter: Hide snapshots in SR disk view [\#2102](https://github.com/vatesfr/xo-web/issues/2102)
- Smarter XOSAN install [\#2084](https://github.com/vatesfr/xo-web/issues/2084)
- PL translation [\#2079](https://github.com/vatesfr/xo-web/issues/2079)
- Remove the "share this VM" option if not in self service [\#2061](https://github.com/vatesfr/xo-web/issues/2061)
- "connected" status graphics are not the same on the host storage and networking tabs [\#2060](https://github.com/vatesfr/xo-web/issues/2060)
- Ability to view and edit `vga` and `videoram` fields in VM view [\#158](https://github.com/vatesfr/xo-web/issues/158)
- Performances [\#1](https://github.com/vatesfr/xen-api/issues/1)
### Bug fixes
- Dashboard display issues [\#2108](https://github.com/vatesfr/xo-web/issues/2108)
- Dashboard CPUs Usage [\#2105](https://github.com/vatesfr/xo-web/issues/2105)
- [Dashboard/Overview] Warning [\#2090](https://github.com/vatesfr/xo-web/issues/2090)
- VM creation displays all networks [\#2086](https://github.com/vatesfr/xo-web/issues/2086)
- Cannot change HA mode for a VM [\#2080](https://github.com/vatesfr/xo-web/issues/2080)
- [Smart backup] Tags selection does not work [\#2077](https://github.com/vatesfr/xo-web/issues/2077)
- [Backup jobs] Timeout should be in seconds, not milliseconds [\#2076](https://github.com/vatesfr/xo-web/issues/2076)
- Missing VM templates [\#2075](https://github.com/vatesfr/xo-web/issues/2075)
- [transport-email] From header not set [\#2074](https://github.com/vatesfr/xo-web/issues/2074)
- Missing objects should be displayed in backup edition [\#2052](https://github.com/vatesfr/xo-web/issues/2052)
## **5.7.0** (2017-03-31)
### Enhancements
- Improve ActionButton error reporting [\#2048](https://github.com/vatesfr/xo-web/issues/2048)
- Home view master checkbox UI issue [\#2027](https://github.com/vatesfr/xo-web/issues/2027)
- HU Translation [\#2019](https://github.com/vatesfr/xo-web/issues/2019)
- [Usage report] Add name for all objects [\#2017](https://github.com/vatesfr/xo-web/issues/2017)
- [Home] Improve inter-types linkage [\#2012](https://github.com/vatesfr/xo-web/issues/2012)
- Remove bootable checkboxes in VM creation [\#2007](https://github.com/vatesfr/xo-web/issues/2007)
- Do not display bootable toggles for disks of non-PV VMs [\#1996](https://github.com/vatesfr/xo-web/issues/1996)
- Try to match network VLAN for VM migration modal [\#1990](https://github.com/vatesfr/xo-web/issues/1990)
- [Usage reports] Add VM names in addition to UUIDs [\#1984](https://github.com/vatesfr/xo-web/issues/1984)
- Host affinity in "advanced" VM creation [\#1983](https://github.com/vatesfr/xo-web/issues/1983)
- Add job tag in backup logs [\#1982](https://github.com/vatesfr/xo-web/issues/1982)
- Possibility to add a label/description to servers [\#1965](https://github.com/vatesfr/xo-web/issues/1965)
- Possibility to create shared VM in a resource set [\#1964](https://github.com/vatesfr/xo-web/issues/1964)
- Clearer display of disabled (backup) jobs [\#1958](https://github.com/vatesfr/xo-web/issues/1958)
- Job should have a configurable timeout [\#1956](https://github.com/vatesfr/xo-web/issues/1956)
- Sort failed VMs in backup report [\#1950](https://github.com/vatesfr/xo-web/issues/1950)
- Support for UNIX socket path [\#1944](https://github.com/vatesfr/xo-web/issues/1944)
- Interface - Host Patching - Button Verbiage [\#1911](https://github.com/vatesfr/xo-web/issues/1911)
- Display if a VM is in Self Service (and which group) [\#1905](https://github.com/vatesfr/xo-web/issues/1905)
- Install supplemental pack on a whole pool [\#1896](https://github.com/vatesfr/xo-web/issues/1896)
- Allow VM snapshots with ACLs [\#1865](https://github.com/vatesfr/xo-web/issues/1886)
- Icon to indicate if a snapshot is quiesce [\#1858](https://github.com/vatesfr/xo-web/issues/1858)
- Pool Ips input too permissive [\#1731](https://github.com/vatesfr/xo-web/issues/1731)
- Select is going on top after each choice [\#1359](https://github.com/vatesfr/xo-web/issues/1359)
### Bug fixes
- Missing objects should be displayed in backup edition [\#2052](https://github.com/vatesfr/xo-web/issues/2052)
- Search bar content changes while typing [\#2035](https://github.com/vatesfr/xo-web/issues/2035)
- VM.$guest_metrics.PV_drivers_up_to_date is deprecated in XS 7.1 [\#2024](https://github.com/vatesfr/xo-web/issues/2024)
- Bootable flag selection checkbox for extra disk not fetched [\#1994](https://github.com/vatesfr/xo-web/issues/1994)
- Home view Changing type must reset paging [\#1993](https://github.com/vatesfr/xo-web/issues/1993)
- XOSAN menu item should only be displayed to admins [\#1968](https://github.com/vatesfr/xo-web/issues/1968)
- Object type change are not correctly handled in UI [\#1967](https://github.com/vatesfr/xo-web/issues/1967)
- VM creation is stuck when using ISO/DVD as install method [\#1966](https://github.com/vatesfr/xo-web/issues/1966)
- Install pack on whole pool fails [\#1957](https://github.com/vatesfr/xo-web/issues/1957)
- Consoles are broken in next-release [\#1954](https://github.com/vatesfr/xo-web/issues/1954)
- [VHD merge] Increase BAT when necessary [\#1939](https://github.com/vatesfr/xo-web/issues/1939)
- Issue on VM restore time [\#1936](https://github.com/vatesfr/xo-web/issues/1936)
- Two remotes should not be able to have the same name [\#1879](https://github.com/vatesfr/xo-web/issues/1879)
- Selfservice limits not honored after VM creation [\#1695](https://github.com/vatesfr/xo-web/issues/1695)
## **5.6.0** (2017-01-27)
Reporting, LVM File level restore.
### Enhancements
- Do not stop patches install if already applied [\#1904](https://github.com/vatesfr/xo-web/issues/1904)
- Improve scheduling UI [\#1893](https://github.com/vatesfr/xo-web/issues/1893)
- Smart backup and tag [\#1885](https://github.com/vatesfr/xo-web/issues/1885)
- Missing embeded API documention [\#1882](https://github.com/vatesfr/xo-web/issues/1882)
- Add local DVD in CD selector [\#1880](https://github.com/vatesfr/xo-web/issues/1880)
- File level restore for LVM [\#1878](https://github.com/vatesfr/xo-web/issues/1878)
- Restore multiple files from file level restore [\#1877](https://github.com/vatesfr/xo-web/issues/1877)
- Add a VM tab for host & pool views [\#1864](https://github.com/vatesfr/xo-web/issues/1864)
- Icon to indicate if a snapshot is quiesce [\#1858](https://github.com/vatesfr/xo-web/issues/1858)
- UI for disconnect hosts comp [\#1833](https://github.com/vatesfr/xo-web/issues/1833)
- Eject all xs-guest.iso in a pool [\#1798](https://github.com/vatesfr/xo-web/issues/1798)
- Display installed supplemental pack on host [\#1506](https://github.com/vatesfr/xo-web/issues/1506)
- Install supplemental pack on host comp [\#1460](https://github.com/vatesfr/xo-web/issues/1460)
- Pool-wide combined stats [\#1324](https://github.com/vatesfr/xo-web/issues/1324)
### Bug fixes
- IP-address not released when VM removed [\#1906](https://github.com/vatesfr/xo-web/issues/1906)
- Interface broken due to new Bootstrap Alpha [\#1871](https://github.com/vatesfr/xo-web/issues/1871)
- Self service recompute all limits broken [\#1866](https://github.com/vatesfr/xo-web/issues/1866)
- Patch not found error for XS 6.5 [\#1863](https://github.com/vatesfr/xo-web/issues/1863)
- Convert To Template issues [\#1855](https://github.com/vatesfr/xo-web/issues/1855)
- Removing PIF seems to fail [\#1853](https://github.com/vatesfr/xo-web/issues/1853)
- Depth should be >= 1 in backup creation [\#1851](https://github.com/vatesfr/xo-web/issues/1851)
- Wrong link in Dashboard > Health [\#1850](https://github.com/vatesfr/xo-web/issues/1850)
- Incorrect file dates shown in new File Restore feature [\#1840](https://github.com/vatesfr/xo-web/issues/1840)
- IP allocation problem [\#1747](https://github.com/vatesfr/xo-web/issues/1747)
- Selfservice limits not honored after VM creation [\#1695](https://github.com/vatesfr/xo-web/issues/1695)
## **5.5.0** (2016-12-20)
File level restore.
### Enhancements
- Better auto select network when migrate VM [\#1788](https://github.com/vatesfr/xo-web/issues/1788)
- Plugin for passive backup job reporting in Nagios [\#1664](https://github.com/vatesfr/xo-web/issues/1664)
- File level restore for delta backup [\#1590](https://github.com/vatesfr/xo-web/issues/1590)
- Better select filters for ACLs [\#1515](https://github.com/vatesfr/xo-web/issues/1515)
- All pools and "negative" filters [\#1503](https://github.com/vatesfr/xo-web/issues/1503)
- VM copy with disk selection [\#826](https://github.com/vatesfr/xo-web/issues/826)
- Disable metadata exports [\#1818](https://github.com/vatesfr/xo-web/issues/1818)
### Bug fixes
- Tool small selector [\#1832](https://github.com/vatesfr/xo-web/issues/1832)
- Replication does not work from a VM created by a CR or delta backup [\#1811](https://github.com/vatesfr/xo-web/issues/1811)
- Can't add a SSH key in VM creation [\#1805](https://github.com/vatesfr/xo-web/issues/1805)
- Issue when no default SR in a pool [\#1804](https://github.com/vatesfr/xo-web/issues/1804)
- XOA doesn't refresh after an update anymore [\#1801](https://github.com/vatesfr/xo-web/issues/1801)
- Shortcuts not inhibited on inputs on Safari [\#1691](https://github.com/vatesfr/xo-web/issues/1691)
## **5.4.0** (2016-11-23)
### Enhancements
- XML display in alerts [\#1776](https://github.com/vatesfr/xo-web/issues/1776)
- Remove some view for non admin users [\#1773](https://github.com/vatesfr/xo-web/issues/1773)
- Complex matcher should support matching boolean values [\#1768](https://github.com/vatesfr/xo-web/issues/1768)
- Home SR view [\#1764](https://github.com/vatesfr/xo-web/issues/1764)
- Filter on tag click [\#1763](https://github.com/vatesfr/xo-web/issues/1763)
- Testable plugins [\#1749](https://github.com/vatesfr/xo-web/issues/1749)
- Backup/Restore Design fix. [\#1734](https://github.com/vatesfr/xo-web/issues/1734)
- Display the owner of a \(backup\) job [\#1733](https://github.com/vatesfr/xo-web/issues/1733)
- Use paginated table for backup jobs [\#1726](https://github.com/vatesfr/xo-web/issues/1726)
- SR view / Disks: should display snapshot VDIs [\#1723](https://github.com/vatesfr/xo-web/issues/1723)
- Restored VM should have an identifiable name [\#1719](https://github.com/vatesfr/xo-web/issues/1719)
- If host reboot action returns NO\_HOSTS\_AVAILABLE, ask to force [\#1717](https://github.com/vatesfr/xo-web/issues/1717)
- Hide xo-server timezone in backups [\#1706](https://github.com/vatesfr/xo-web/issues/1706)
- Enable hyperlink for Hostname for Issues [\#1700](https://github.com/vatesfr/xo-web/issues/1700)
- Pool/network - Modify column [\#1696](https://github.com/vatesfr/xo-web/issues/1696)
- UI - Plugins - Display a message if no plugins [\#1670](https://github.com/vatesfr/xo-web/issues/1670)
- Display warning/error for delta backup on XS older than 6.5 [\#1647](https://github.com/vatesfr/xo-web/issues/1647)
- XO without internet access doesn't work [\#1629](https://github.com/vatesfr/xo-web/issues/1629)
- Improve backup restore view [\#1609](https://github.com/vatesfr/xo-web/issues/1609)
- UI Enhancement - Acronym for dummy [\#1604](https://github.com/vatesfr/xo-web/issues/1604)
- Slack XO plugin for backup report [\#1593](https://github.com/vatesfr/xo-web/issues/1593)
- Expose XAPI exceptions in the UI [\#1481](https://github.com/vatesfr/xo-web/issues/1481)
- Running VMs in the host overview, all VMs in the pool overview [\#1432](https://github.com/vatesfr/xo-web/issues/1432)
- Move location of NFS mount point [\#1405](https://github.com/vatesfr/xo-web/issues/1405)
- Home: Pool list - additionnal informations for pool [\#1226](https://github.com/vatesfr/xo-web/issues/1226)
- Modify VLAN of an existing network [\#1092](https://github.com/vatesfr/xo-web/issues/1092)
- Wrong instructions for CLI upgrade [\#787](https://github.com/vatesfr/xo-web/issues/787)
- Ability to export/import XO config [\#786](https://github.com/vatesfr/xo-web/issues/786)
- Test button for transport-email plugin [\#697](https://github.com/vatesfr/xo-web/issues/697)
- Merge `scheduler` API into `schedule` [\#664](https://github.com/vatesfr/xo-web/issues/664)
### Bug fixes
- Should jobs be accessible to non admins? [\#1759](https://github.com/vatesfr/xo-web/issues/1759)
- Schedules deletion is not working [\#1737](https://github.com/vatesfr/xo-web/issues/1737)
- Editing a job from the jobs overview page does not work [\#1736](https://github.com/vatesfr/xo-web/issues/1736)
- Editing a schedule from jobs overview does not work [\#1728](https://github.com/vatesfr/xo-web/issues/1728)
- ACLs not correctly imported [\#1722](https://github.com/vatesfr/xo-web/issues/1722)
- Some Bootstrap style broken [\#1721](https://github.com/vatesfr/xo-web/issues/1721)
- Not properly sign out on auth token expiration [\#1711](https://github.com/vatesfr/xo-web/issues/1711)
- Hosts/<UUID>/network status is incorrect [\#1702](https://github.com/vatesfr/xo-web/issues/1702)
- Patches application fails "Found : Moved Temporarily" [\#1701](https://github.com/vatesfr/xo-web/issues/1701)
- Password generation for user creation is not working [\#1678](https://github.com/vatesfr/xo-web/issues/1678)
- \#/dashboard/health Remove All Orphaned VDIs [\#1622](https://github.com/vatesfr/xo-web/issues/1622)
- Create a new SR - CIFS/SAMBA Broken [\#1615](https://github.com/vatesfr/xo-web/issues/1615)
- xo-cli --list-objects: truncated output ? 64k buffer limitation ? [\#1356](https://github.com/vatesfr/xo-web/issues/1356)
## **5.3.0** (2016-10-20)
@@ -118,7 +474,7 @@
- Tooltip on OS icon in VM view [\#1416](https://github.com/vatesfr/xo-web/issues/1416)
- Display pool master [\#1407](https://github.com/vatesfr/xo-web/issues/1407)
- Missing tooltips in VM creation view [\#1402](https://github.com/vatesfr/xo-web/issues/1402)
- Handle VDB disconnect and connect [\#1397](https://github.com/vatesfr/xo-web/issues/1397)
- Handle VBD disconnect and connect [\#1397](https://github.com/vatesfr/xo-web/issues/1397)
- Eject host from a pool [\#1395](https://github.com/vatesfr/xo-web/issues/1395)
- Improve pool general view [\#1393](https://github.com/vatesfr/xo-web/issues/1393)
- Improve patching system [\#1392](https://github.com/vatesfr/xo-web/issues/1392)

View File

@@ -1,4 +1,4 @@
# Xen Orchestra Web [![Build Status](https://travis-ci.org/vatesfr/xo-web.png?branch=master)](https://travis-ci.org/vatesfr/xo-web)
# Xen Orchestra Web [![Chat with us](https://storage.crisp.im/plugins/images/936925df-f37b-4ba8-bab0-70cd2edcb0be/badge.svg)](https://go.crisp.im/chat/embed/?website_id=-JzqzzwddSV7bKGtEyAQ) [![Build Status](https://travis-ci.org/vatesfr/xo-web.png?branch=master)](https://travis-ci.org/vatesfr/xo-web)
![](http://i.imgur.com/tRffA5y.png)

View File

@@ -2,17 +2,17 @@
// ===================================================================
var SRC_DIR = __dirname + '/src' // eslint-disable-line no-path-concat
var DIST_DIR = __dirname + '/dist' // eslint-disable-line no-path-concat
const SRC_DIR = __dirname + '/src' // eslint-disable-line no-path-concat
const DIST_DIR = __dirname + '/dist' // eslint-disable-line no-path-concat
// Port to use for the livereload server.
//
// It must be available and if possible unique to not conflict with other projects.
// http://www.random.org/integers/?num=1&min=1024&max=65535&col=1&base=10&format=plain&rnd=new
var LIVERELOAD_PORT = 26242
const LIVERELOAD_PORT = 26242
var PRODUCTION = process.env.NODE_ENV === 'production'
var DEVELOPMENT = !PRODUCTION
const PRODUCTION = process.env.NODE_ENV === 'production'
const DEVELOPMENT = !PRODUCTION
if (!process.env.XOA_PLAN) {
process.env.XOA_PLAN = '5' // Open Source
@@ -20,12 +20,12 @@ if (!process.env.XOA_PLAN) {
// ===================================================================
var gulp = require('gulp')
const gulp = require('gulp')
// ===================================================================
function lazyFn (factory) {
var fn = function () {
let fn = function () {
fn = factory()
return fn.apply(this, arguments)
}
@@ -37,19 +37,19 @@ function lazyFn (factory) {
// -------------------------------------------------------------------
var livereload = lazyFn(function () {
var livereload = require('gulp-refresh')
const livereload = lazyFn(function () {
const livereload = require('gulp-refresh')
livereload.listen({
port: LIVERELOAD_PORT
port: LIVERELOAD_PORT,
})
return livereload
})
var pipe = lazyFn(function () {
var current
const pipe = lazyFn(function () {
let current
function pipeCore (streams) {
var i, n, stream
let i, n, stream
for (i = 0, n = streams.length; i < n; ++i) {
stream = streams[i]
if (!stream) {
@@ -57,14 +57,12 @@ var pipe = lazyFn(function () {
} else if (stream instanceof Array) {
pipeCore(stream)
} else {
current = current
? current.pipe(stream)
: stream
current = current ? current.pipe(stream) : stream
}
}
}
var push = Array.prototype.push
const push = Array.prototype.push
return function (streams) {
try {
if (!(streams instanceof Array)) {
@@ -81,7 +79,7 @@ var pipe = lazyFn(function () {
}
})
var resolvePath = lazyFn(function () {
const resolvePath = lazyFn(function () {
return require('path').resolve
})
@@ -89,37 +87,35 @@ var resolvePath = lazyFn(function () {
// Similar to `gulp.src()` but the pattern is relative to `SRC_DIR`
// and files are automatically watched when not in production mode.
var src = lazyFn(function () {
const src = lazyFn(function () {
function resolve (path) {
return path
? resolvePath(SRC_DIR, path)
: SRC_DIR
return path ? resolvePath(SRC_DIR, path) : SRC_DIR
}
return PRODUCTION
? function src (pattern, opts) {
var base = resolve(opts && opts.base)
const base = resolve(opts && opts.base)
return gulp.src(pattern, {
base: base,
cwd: base,
passthrough: opts && opts.passthrough,
sourcemaps: opts && opts.sourcemaps
sourcemaps: opts && opts.sourcemaps,
})
}
: function src (pattern, opts) {
var base = resolve(opts && opts.base)
const base = resolve(opts && opts.base)
return pipe(
gulp.src(pattern, {
base: base,
cwd: base,
passthrough: opts && opts.passthrough,
sourcemaps: opts && opts.sourcemaps
sourcemaps: opts && opts.sourcemaps,
}),
require('gulp-watch')(pattern, {
base: base,
cwd: base
cwd: base,
}),
require('gulp-plumber')()
)
@@ -129,17 +125,15 @@ var src = lazyFn(function () {
// Similar to `gulp.dest()` but the output directory is relative to
// `DIST_DIR` and default to `./`, and files are automatically live-
// reloaded when not in production mode.
var dest = lazyFn(function () {
const dest = lazyFn(function () {
function resolve (path) {
return path
? resolvePath(DIST_DIR, path)
: DIST_DIR
return path ? resolvePath(DIST_DIR, path) : DIST_DIR
}
var opts = {
const opts = {
sourcemaps: {
path: '.'
}
path: '.',
},
}
return PRODUCTION
@@ -147,7 +141,7 @@ var dest = lazyFn(function () {
return gulp.dest(resolve(path), opts)
}
: function dest (path) {
var stream = gulp.dest(resolve(path), opts)
const stream = gulp.dest(resolve(path), opts)
stream.pipe(livereload())
return stream
}
@@ -160,9 +154,9 @@ function browserify (path, opts) {
opts = {}
}
var bundler = require('browserify')(path, {
let bundler = require('browserify')(path, {
basedir: SRC_DIR,
debug: DEVELOPMENT, // TODO: enable also in production but need to make it work with gulp-uglify.
debug: true,
extensions: opts.extensions,
fullPaths: false,
paths: SRC_DIR + '/common',
@@ -170,12 +164,12 @@ function browserify (path, opts) {
// Required by Watchify.
cache: {},
packageCache: {}
packageCache: {},
})
var plugins = opts.plugins
for (var i = 0, n = plugins && plugins.length; i < n; ++i) {
var plugin = plugins[i]
const plugins = opts.plugins
for (let i = 0, n = plugins && plugins.length; i < n; ++i) {
const plugin = plugins[i]
bundler.plugin(require(plugin[0]), plugin[1])
}
@@ -183,7 +177,11 @@ function browserify (path, opts) {
// FIXME: does not work with react-intl (?!)
// bundler.plugin('bundle-collapser/plugin')
} else {
bundler = require('watchify')(bundler)
bundler = require('watchify')(bundler, {
// do not watch in `node_modules`
// https://github.com/browserify/watchify#options
ignoreWatch: true,
})
}
// Append the extension if necessary.
@@ -192,11 +190,11 @@ function browserify (path, opts) {
}
path = resolvePath(SRC_DIR, path)
var stream = new (require('readable-stream'))({
objectMode: true
let stream = new (require('readable-stream'))({
objectMode: true,
})
var write
let write
function bundle () {
bundler.bundle(function onBundle (error, buffer) {
if (error) {
@@ -204,11 +202,13 @@ function browserify (path, opts) {
return
}
write(new (require('vinyl'))({
base: SRC_DIR,
contents: buffer,
path: path
}))
write(
new (require('vinyl'))({
base: SRC_DIR,
contents: buffer,
path: path,
})
)
})
}
@@ -240,9 +240,10 @@ gulp.task(function buildPages () {
return pipe(
src('index.pug', { sourcemaps: true }),
require('gulp-pug')(),
DEVELOPMENT && require('gulp-embedlr')({
port: LIVERELOAD_PORT
}),
DEVELOPMENT &&
require('gulp-embedlr')({
port: LIVERELOAD_PORT,
}),
dest()
)
})
@@ -252,12 +253,16 @@ gulp.task(function buildScripts () {
browserify('./index.js', {
plugins: [
// ['css-modulesify', {
['modular-css/browserify', {
css: DIST_DIR + '/modules.css'
}]
]
[
'modular-css/browserify',
{
css: DIST_DIR + '/modules.css',
},
],
],
}),
PRODUCTION && require('gulp-uglify')(),
require('gulp-sourcemaps').init({ loadMaps: true }),
PRODUCTION && require('gulp-uglify/composer')(require('uglify-es'))(),
dest()
)
})
@@ -266,10 +271,7 @@ gulp.task(function buildStyles () {
return pipe(
src('index.scss', { sourcemaps: true }),
require('gulp-sass')(),
require('gulp-autoprefixer')([
'last 1 version',
'> 1%'
]),
require('gulp-autoprefixer')(['last 1 version', '> 1%']),
PRODUCTION && require('gulp-csso')(),
dest()
)
@@ -280,22 +282,20 @@ gulp.task(function copyAssets () {
src(['assets/**/*', 'favicon.*']),
src('fontawesome-webfont.*', {
base: __dirname + '/node_modules/font-awesome/fonts', // eslint-disable-line no-path-concat
passthrough: true
passthrough: true,
}),
src(['!*.css', 'font-mfizz.*'], {
base: __dirname + '/node_modules/font-mfizz/dist', // eslint-disable-line no-path-concat
passthrough: true
passthrough: true,
}),
dest()
)
})
gulp.task('build', gulp.parallel(
'buildPages',
'buildScripts',
'buildStyles',
'copyAssets'
))
gulp.task(
'build',
gulp.parallel('buildPages', 'buildScripts', 'buildStyles', 'copyAssets')
)
// -------------------------------------------------------------------

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-web",
"version": "5.3.1",
"version": "5.15.1",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -31,10 +31,12 @@
"npm": ">=3"
},
"devDependencies": {
"ansi_up": "^1.3.0",
"asap": "^2.0.4",
"ava": "^0.16.0",
"babel-eslint": "^7.0.0",
"@nraynaud/novnc": "0.6.1",
"ansi_up": "^2.0.2",
"asap": "^2.0.6",
"babel-eslint": "^8.0.3",
"babel-plugin-dev": "^1.0.0",
"babel-plugin-lodash": "^3.2.11",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-plugin-transform-react-constant-elements": "^6.5.0",
"babel-plugin-transform-react-inline-elements": "^6.6.5",
@@ -44,87 +46,111 @@
"babel-preset-es2015": "^6.6.0",
"babel-preset-react": "^6.5.0",
"babel-preset-stage-0": "^6.5.0",
"babel-register": "^6.16.3",
"babel-runtime": "^6.6.1",
"babelify": "^7.2.0",
"babel-register": "^6.26.0",
"babel-runtime": "^6.26.0",
"babelify": "^8.0.0",
"benchmark": "^2.1.0",
"bootstrap": "github:twbs/bootstrap#v4-dev",
"browserify": "^13.0.0",
"bundle-collapser": "^1.2.1",
"chartist": "^0.9.4",
"chartist-plugin-legend": "^0.5.0",
"bootstrap": "4.0.0-alpha.5",
"browserify": "^14.5.0",
"bundle-collapser": "^1.3.0",
"chartist": "^0.10.1",
"chartist-plugin-legend": "^0.6.1",
"chartist-plugin-tooltip": "0.0.11",
"classnames": "^2.2.3",
"complex-matcher": "^0.1.1",
"cookies-js": "^1.2.2",
"d3": "^4.2.8",
"dependency-check": "^2.5.1",
"event-to-promise": "^0.7.0",
"font-awesome": "^4.5.0",
"font-mfizz": "github:fizzed/font-mfizz",
"get-stream": "^2.3.0",
"ghooks": "^1.1.1",
"globby": "^6.0.0",
"d3": "^4.12.0",
"dependency-check": "^2.9.2",
"enzyme": "^3.1.1",
"enzyme-adapter-react-15": "^1.0.5",
"enzyme-to-json": "^3.3.0",
"eslint": "^4.13.1",
"eslint-config-standard": "^10.2.1",
"eslint-config-standard-jsx": "^4.0.2",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^5.2.1",
"eslint-plugin-promise": "^3.6.0",
"eslint-plugin-react": "^7.4.0",
"eslint-plugin-standard": "^3.0.1",
"event-to-promise": "^0.8.0",
"font-awesome": "^4.7.0",
"font-mfizz": "^2.4.1",
"get-stream": "^3.0.0",
"globby": "^7.1.1",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-autoprefixer": "^3.1.0",
"gulp-csso": "^2.0.0",
"gulp-autoprefixer": "^4.0.0",
"gulp-csso": "^3.0.0",
"gulp-embedlr": "^0.5.2",
"gulp-plumber": "^1.1.0",
"gulp-pug": "^3.1.0",
"gulp-refresh": "^1.1.0",
"gulp-sass": "^2.2.0",
"gulp-uglify": "^2.0.0",
"gulp-sass": "^3.0.0",
"gulp-sourcemaps": "^2.2.3",
"gulp-uglify": "^3.0.0",
"gulp-watch": "^4.3.5",
"human-format": "^0.6.0",
"index-modules": "0.0.0",
"is-ip": "^1.0.0",
"jsonrpc-websocket-client": "0.0.1-5",
"human-format": "^0.9.2",
"husky": "^0.14.3",
"immutable": "^3.8.2",
"index-modules": "^0.3.0",
"is-ip": "^2.0.0",
"jest": "^22.0.0",
"jsonrpc-websocket-client": "^0.2.0",
"kindof": "^2.0.0",
"later": "^1.2.0",
"lint-staged": "^6.0.0",
"lodash": "^4.6.1",
"loose-envify": "^1.1.0",
"make-error": "^1.2.1",
"marked": "^0.3.5",
"modular-css": "^0.28.0",
"moment": "^2.13.0",
"moment-timezone": "^0.5.4",
"notifyjs": "^2.0.1",
"novnc-node": "^0.5.3",
"promise-toolbox": "^0.7.0",
"marked": "^0.3.7",
"modular-css": "^7.2.0",
"moment": "^2.20.0",
"moment-timezone": "^0.5.14",
"notifyjs": "^3.0.0",
"prettier": "^1.9.2",
"promise-toolbox": "^0.9.5",
"prop-types": "^15.6.0",
"random-password": "^0.1.2",
"react": "^15.0.0",
"react-addons-shallow-compare": "^15.1.0",
"react": "^15.4.1",
"react-addons-shallow-compare": "^15.6.2",
"react-addons-test-utils": "^15.6.2",
"react-bootstrap-4": "^0.29.1",
"react-chartist": "^0.10.1",
"react-copy-to-clipboard": "^4.0.2",
"react-debounce-input": "^2.4.0",
"react-dnd": "^2.1.4",
"react-dnd-html5-backend": "^2.1.2",
"react-chartist": "^0.13.0",
"react-copy-to-clipboard": "^5.0.1",
"react-dnd": "^2.5.4",
"react-dnd-html5-backend": "^2.5.4",
"react-document-title": "^2.0.2",
"react-dom": "^15.0.0",
"react-dropzone": "^3.5.0",
"react-intl": "^2.0.1",
"react-key-handler": "^0.3.0",
"react-notify": "^2.0.1",
"react-overlays": "^0.6.0",
"react-redux": "^4.4.0",
"react-dom": "^15.4.1",
"react-dropzone": "^4.2.3",
"react-intl": "^2.4.0",
"react-key-handler": "^1.0.1",
"react-notify": "^3.0.0",
"react-overlays": "^0.8.3",
"react-redux": "^5.0.6",
"react-router": "^3.0.0",
"react-select": "^1.0.0-beta13",
"react-shortcuts": "^1.0.7",
"react-sparklines": "^1.5.0",
"react-select": "^1.1.0",
"react-shortcuts": "^2.0.0",
"react-sparklines": "1.6.0",
"react-test-renderer": "^15.6.2",
"react-virtualized": "^8.0.8",
"readable-stream": "^2.0.6",
"redux": "^3.3.1",
"redux-devtools": "^3.1.1",
"readable-stream": "^2.3.3",
"redux": "^3.7.2",
"redux-devtools": "^3.4.1",
"redux-devtools-dock-monitor": "^1.1.0",
"redux-devtools-log-monitor": "^1.0.5",
"redux-devtools-log-monitor": "^1.4.0",
"redux-thunk": "^2.0.1",
"reselect": "^2.2.1",
"standard": "^8.4.0",
"superagent": "^2.0.0",
"tar-stream": "^1.5.2",
"vinyl": "^2.0.0",
"reselect": "^2.5.4",
"semver": "^5.4.1",
"styled-components": "^2.3.0",
"tar-stream": "^1.5.5",
"uglify-es": "^3.2.2",
"uncontrollable-input": "^0.0.1",
"url-parse": "^1.2.0",
"vinyl": "^2.1.0",
"watchify": "^3.7.0",
"xml2js": "^0.4.17",
"xo-acl-resolver": "^0.2.2",
"whatwg-fetch": "^2.0.3",
"xml2js": "^0.4.19",
"xo-acl-resolver": "^0.2.3",
"xo-common": "^0.1.1",
"xo-lib": "^0.8.0",
"xo-remote-parser": "^0.3"
},
@@ -133,11 +159,13 @@
"build": "npm run build-indexes && NODE_ENV=production gulp build",
"build-indexes": "index-modules --auto src",
"dev": "npm run build-indexes && NODE_ENV=development gulp build",
"dev-test": "ava --watch",
"lint": "standard",
"posttest": "npm run lint",
"prepublish": "npm run build",
"test": "ava"
"dev-test": "jest --watch",
"lint-staged-stash": "touch .lint-staged && git stash save --include-untracked --keep-index && true",
"lint-staged-unstash": "git stash pop && rm -f .lint-staged && true",
"posttest": "eslint --ignore-path .gitignore src/",
"precommit": "lint-staged",
"prepublishOnly": "npm run build",
"test": "jest"
},
"browserify": {
"transform": [
@@ -145,15 +173,6 @@
"loose-envify"
]
},
"ava": {
"babel": "inherit",
"files": [
"src/**/*.spec.js"
],
"require": [
"babel-register"
]
},
"babel": {
"env": {
"development": {
@@ -170,6 +189,8 @@
}
},
"plugins": [
"dev",
"lodash",
"transform-decorators-legacy",
"transform-runtime"
],
@@ -179,15 +200,20 @@
"stage-0"
]
},
"config": {
"ghooks": {
"commit-msg": "npm test"
}
"jest": {
"setupTestFrameworkScriptFile": "./setup-tests.js",
"snapshotSerializers": [
"enzyme-to-json/serializer"
]
},
"standard": {
"ignore": [
"dist"
],
"parser": "babel-eslint"
"lint-staged": {
"*.js": [
"lint-staged-stash",
"prettier --write",
"eslint --fix",
"jest --findRelatedTests --passWithNoTests",
"git add",
"lint-staged-unstash"
]
}
}

4
setup-tests.js Normal file
View File

@@ -0,0 +1,4 @@
import { configure } from 'enzyme'
import Adapter from 'enzyme-adapter-react-15'
configure({ adapter: new Adapter() })

View File

@@ -27,6 +27,13 @@ $ct-series-colors: (
flex-direction: column-reverse;
}
// safari has a bug in flex computing that prevent charts from showing see #1755
// by fixing the height with a value found in Chrome it seems like it fixes the issue without breaking the layout
// elsewhere
.dashboardItem .ct-chart {
height: 150px;
}
// Line in charts with only 2px in width
.ct-line {
stroke-width: 2px;

View File

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

View File

@@ -1,46 +1,60 @@
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 propTypes from 'prop-types-decorator'
import React, { cloneElement } from 'react'
import { noop } from 'lodash'
const ActionBar = ({ actions, param }) => (
import ButtonGroup from './button-group'
export const Action = ({
display,
handler,
handlerParam,
icon,
label,
pending,
redirectOnSuccess,
}) => (
<ActionButton
handler={handler}
handlerParam={handlerParam}
icon={icon}
pending={pending}
redirectOnSuccess={redirectOnSuccess}
size='large'
tooltip={display === 'icon' ? label : undefined}
>
{display === 'both' && label}
</ActionButton>
)
Action.propTypes = {
display: propTypes.oneOf(['icon', 'both']),
handler: propTypes.func.isRequired,
icon: propTypes.string.isRequired,
label: propTypes.node,
pending: propTypes.bool,
redirectOnSuccess: propTypes.string,
}
const ActionBar = ({ children, handlerParam = noop, display = 'both' }) => (
<ButtonGroup>
{map(actions, (button, index) => {
if (!button) {
{React.Children.map(children, (child, key) => {
if (!child) {
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 { props } = child
return cloneElement(child, {
display: props.display || display,
handlerParam: props.handlerParam || handlerParam,
key,
})
})}
</ButtonGroup>
)
ActionBar.propTypes = {
actions: React.PropTypes.arrayOf(
React.PropTypes.shape({
label: React.PropTypes.string.isRequired,
icon: React.PropTypes.string.isRequired,
handler: React.PropTypes.func,
redirectOnSuccess: React.PropTypes.string
})
).isRequired,
display: React.PropTypes.oneOf(['icon', 'text', 'both'])
display: propTypes.oneOf(['icon', 'both']),
handlerParam: propTypes.any,
}
export { ActionBar as default }

View File

@@ -1,73 +1,94 @@
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 propTypes from './prop-types-decorator'
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,
redirectOnSuccess: propTypes.oneOfType([
propTypes.func,
propTypes.string
]),
size: propTypes.oneOf([
'large',
'small'
]),
tooltip: propTypes.node
// 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]),
// 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 {
handler,
handlerParam
} = this.props
const { children, handler, handlerParam, tooltip } = this.props
try {
this.setState({
error: null,
working: true
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({
working: false
working: false,
})
} catch (error) {
this.setState({
error,
working: false
working: false,
})
// 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)
)
}
}
}
@@ -82,7 +103,9 @@ export default class ActionButton extends Component {
const { form } = this.props
if (form) {
document.getElementById(form).addEventListener('submit', this._eventListener)
document
.getElementById(form)
.addEventListener('submit', this._eventListener)
}
}
@@ -90,41 +113,39 @@ export default class ActionButton extends Component {
const { form } = this.props
if (form) {
document.getElementById(form).removeEventListener('submit', this._eventListener)
document
.getElementById(form)
.removeEventListener('submit', this._eventListener)
}
}
render () {
const {
props: {
btnStyle,
children,
className,
disabled,
form,
icon,
size: bsSize,
style,
tooltip
},
state: { error, working }
props: { children, icon, 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 />
{children && ' '}
{children}
</Button>
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
return tooltip
? <Tooltip content={tooltip}>{button}</Tooltip>
: button
const button = (
<Button {...props}>
<Icon icon={pending || working ? 'loading' : icon} fixedWidth />
{children && ' '}
{children}
</Button>
)
return tooltip ? <Tooltip content={tooltip}>{button}</Tooltip> : button
}
}

View File

@@ -5,10 +5,6 @@ import ActionButton from '../action-button'
import styles from './index.css'
const ActionRowButton = props => (
<ActionButton
{...props}
className={styles.button}
size='small'
/>
<ActionButton {...props} className={styles.button} size='small' />
)
export { ActionRowButton as default }

View File

@@ -1,15 +1,16 @@
import React from 'react'
import ActionButton from './action-button'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
const ActionToggle = ({ className, value, ...props }) =>
const ActionToggle = ({ className, value, ...props }) => (
<ActionButton
{...props}
btnStyle={value ? 'success' : null}
icon={value ? 'toggle-on' : 'toggle-off'}
/>
)
export default propTypes({
value: propTypes.bool
value: propTypes.bool,
})(ActionToggle)

View File

@@ -1,30 +1,14 @@
import clone from 'lodash/clone'
import includes from 'lodash/includes'
import isArray from 'lodash/isArray'
import forEach from 'lodash/forEach'
import map from 'lodash/map'
import { Component } from 'react'
import { PureComponent } from 'react'
import { cowSet } from 'utils'
import { includes, isArray, forEach, map } from 'lodash'
import getEventValue from './get-event-value'
import invoke from './invoke'
import shallowEqual from './shallow-equal'
// Should components logs every renders?
//
// Usually set to process.env.NODE_ENV !== 'production'.
const VERBOSE = false
const cowSet = (object, path, value, depth) => {
if (depth >= path.length) {
return value
}
object = clone(object)
const prop = path[depth]
object[prop] = cowSet(object[prop], path, value, depth + 1)
return object
}
const get = (object, path, depth) => {
if (depth >= path.length) {
return object
@@ -36,7 +20,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,30 +30,28 @@ 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)
}
}
// See https://preactjs.com/guide/linked-state
linkState (name, targetPath) {
const key = targetPath
? `${name}##${targetPath}`
: name
const key = targetPath !== undefined ? `${name}##${targetPath}` : name
let linkedState = this._linkedState
let cb
if (!linkedState) {
if (linkedState === null) {
linkedState = this._linkedState = {}
} else if ((cb = linkedState[key])) {
} else if ((cb = linkedState[key]) !== undefined) {
return cb
}
let getValue
if (targetPath) {
if (targetPath !== undefined) {
const path = targetPath.split('.')
getValue = event => get(getEventValue(event), path, 0)
} else {
@@ -85,7 +67,7 @@ export default class BaseComponent extends Component {
return (linkedState[key] = event => {
this.setState({
[name]: getValue(event)
[name]: getValue(event),
})
})
}
@@ -93,9 +75,9 @@ export default class BaseComponent extends Component {
toggleState (name) {
let linkedState = this._linkedState
let cb
if (!linkedState) {
if (linkedState === null) {
linkedState = this._linkedState = {}
} else if ((cb = linkedState[name])) {
} else if ((cb = linkedState[name]) !== undefined) {
return cb
}
@@ -108,17 +90,10 @@ export default class BaseComponent extends Component {
return (linkedState[name] = () => {
this.setState({
[name]: !this.state[name]
[name]: !this.state[name],
})
})
}
shouldComponentUpdate (newProps, newState) {
return !(
shallowEqual(this.props, newProps) &&
shallowEqual(this.state, newState)
)
}
}
if (VERBOSE) {

View File

@@ -8,7 +8,7 @@ const sendNotification = (title, body) => {
new Notify(title, {
body,
timeout: 5,
icon: 'assets/logo.png'
icon: 'assets/logo.png',
}).show()
}

View File

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

28
src/common/button-link.js Normal file
View File

@@ -0,0 +1,28 @@
import React from 'react'
import { routerShape } from 'react-router/lib/PropTypes'
import Button from './button'
import propTypes from './prop-types-decorator'
const ButtonLink = ({ to, ...props }, { router }) => {
props.onClick = () => {
router.push(to)
}
return <Button {...props} />
}
propTypes(
{
to: propTypes.oneOfType([
propTypes.func,
propTypes.object,
propTypes.string,
]),
},
{
router: routerShape,
}
)(ButtonLink)
export { ButtonLink as default }

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

@@ -0,0 +1,53 @@
import classNames from 'classnames'
import React from 'react'
import propTypes from './prop-types-decorator'
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({
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']),
})(Button)
export { Button as default }

View File

@@ -1,51 +1,40 @@
import React from 'react'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
const CARD_STYLE = {
minHeight: '100%'
minHeight: '100%',
}
const CARD_STYLE_WITH_SHADOW = {
...CARD_STYLE,
boxShadow: '0 10px 6px -6px #777' // https://css-tricks.com/almanac/properties/b/box-shadow/
boxShadow: '0 10px 6px -6px #777', // https://css-tricks.com/almanac/properties/b/box-shadow/
}
const CARD_HEADER_STYLE = {
minHeight: '100%',
textAlign: 'center'
textAlign: 'center',
}
export const Card = propTypes({
disableMaxHeight: propTypes.bool,
shadow: propTypes.bool
})(({
children,
shadow
}) => (
<div className='card' style={shadow ? CARD_STYLE_WITH_SHADOW : CARD_STYLE}>
{children}
</div>
))
shadow: propTypes.bool,
})(({ shadow, ...props }) => {
props.className = 'card'
props.style = shadow ? CARD_STYLE_WITH_SHADOW : CARD_STYLE
return <div {...props} />
})
export const CardHeader = propTypes({
className: propTypes.string
})(({
children,
className
}) => (
className: propTypes.string,
})(({ children, className }) => (
<h4 className={`card-header ${className || ''}`} style={CARD_HEADER_STYLE}>
{children}
</h4>
))
export const CardBlock = propTypes({
className: propTypes.string
})(({
children,
className
}) => (
<div className={`card-block ${className || ''}`}>
{children}
</div>
className: propTypes.string,
})(({ children, className }) => (
<div className={`card-block ${className || ''}`}>{children}</div>
))

View File

@@ -2,11 +2,10 @@ import React from 'react'
import styles from './index.css'
const CenterPanel = ({ children }) =>
const CenterPanel = ({ children }) => (
<div className={styles.container}>
<div className={styles.content}>
{children}
</div>
<div className={styles.content}>{children}</div>
</div>
)
export { CenterPanel as default }

View File

@@ -1,18 +1,24 @@
import React from 'react'
import Button from './button'
import Component from './base-component'
import Icon from './icon'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
@propTypes({
children: propTypes.any.isRequired,
className: propTypes.string,
buttonText: propTypes.any.isRequired
buttonText: propTypes.any.isRequired,
defaultOpen: propTypes.bool,
})
export default class Collapse extends Component {
state = {
isOpened: this.props.defaultOpen,
}
_onClick = () => {
this.setState({
isOpened: !this.state.isOpened
isOpened: !this.state.isOpened,
})
}
@@ -22,9 +28,10 @@ export default class Collapse extends Component {
return (
<div className={props.className}>
<button className='btn btn-lg btn-primary btn-block' onClick={this._onClick}>
{props.buttonText} <Icon icon={`chevron-${isOpened ? 'up' : 'down'}`} />
</button>
<Button block btnStyle='primary' size='large' onClick={this._onClick}>
{props.buttonText}{' '}
<Icon icon={`chevron-${isOpened ? 'up' : 'down'}`} />
</Button>
{isOpened && props.children}
</div>
)

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

@@ -0,0 +1,61 @@
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-decorator'
@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,101 +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,
options: propTypes.oneOfType([
propTypes.arrayOf(propTypes.string),
propTypes.number,
propTypes.objectOf(propTypes.string),
propTypes.string
]),
onChange: propTypes.func,
placeholder: propTypes.string,
required: propTypes.bool,
step: propTypes.any,
type: propTypes.string,
value: propTypes.any
})
export default class Combobox extends Component {
static defaultProps = {
type: 'text'
}
get value () {
return this.refs.input.value
}
set value (value) {
this.refs.input.value = value
}
_handleChange = event => {
const { onChange } = this.props
if (onChange) {
onChange(event.target.value)
}
}
_setText (value) {
this.refs.input.value = value
}
render () {
const { props } = this
const options = ensureArray(props.options)
const Input = (
<input
className='form-control'
defaultValue={props.defaultValue}
disabled={props.disabled}
options={options}
onChange={this._handleChange}
placeholder={props.placeholder}
ref='input'
required={props.required}
step={props.step}
type={props.type}
value={props.value}
/>
)
if (!size(options)) {
return Input
}
return (
<div className='input-group'>
<div className='input-group-btn'>
<DropdownButton
bsStyle='secondary'
className={styles.button}
disabled={props.disabled}
id='selectInput'
title=''
>
{map(options, option => (
<MenuItem key={option} onClick={() => this._setText(option)}>
{option}
</MenuItem>
))}
</DropdownButton>
</div>
{Input}
</div>
)
}
}

View File

@@ -1,18 +0,0 @@
import {
parse,
toString
} from './'
import {
ast,
pattern
} from './index.fixtures'
export default ({ benchmark }) => {
benchmark('parse', () => {
parse(pattern)
})
benchmark('toString', () => {
ast::toString()
})
}

View File

@@ -1,18 +0,0 @@
import {
createAnd,
createOr,
createNot,
createProperty,
createString
} from './'
export const pattern = 'foo !"\\\\ \\"" name:|(wonderwoman batman)'
export const ast = createAnd([
createString('foo'),
createNot(createString('\\ "')),
createProperty('name', createOr([
createString('wonderwoman'),
createString('batman')
]))
])

View File

@@ -1,405 +0,0 @@
import every from 'lodash/every'
import filter from 'lodash/filter'
import forEach from 'lodash/forEach'
import isArray from 'lodash/isArray'
import isPlainObject from 'lodash/isPlainObject'
import isString from 'lodash/isString'
import map from 'lodash/map'
import some from 'lodash/some'
import filterReduce from '../filter-reduce'
import invoke from '../invoke'
// ===================================================================
const RAW_STRING_CHARS = invoke(() => {
const chars = { __proto__: null }
const add = (a, b = a) => {
let i = a.charCodeAt(0)
const j = b.charCodeAt(0)
while (i <= j) {
chars[String.fromCharCode(i++)] = true
}
}
add('$')
add('-')
add('.')
add('0', '9')
add('_')
add('A', 'Z')
add('a', 'z')
return chars
})
const isRawString = string => {
const { length } = string
for (let i = 0; i < length; ++i) {
if (!RAW_STRING_CHARS[string[i]]) {
return false
}
}
return true
}
// -------------------------------------------------------------------
export const createAnd = children => children.length === 1
? children[0]
: { type: 'and', children }
export const createOr = children => children.length === 1
? children[0]
: { type: 'or', children }
export const createNot = child => ({ type: 'not', child })
export const createProperty = (name, child) => ({ type: 'property', name, child })
export const createString = value => ({ type: 'string', value })
// -------------------------------------------------------------------
// *and = terms
// terms = term+
// term = ws (groupedAnd | or | not | property | string) ws
// ws = ' '*
// groupedAnd = "(" and ")"
// *or = "|" ws "(" terms ")"
// *not = "!" term
// *property = string ws ":" term
// *string = quotedString | rawString
// quotedString = "\"" ( /[^"\]/ | "\\\\" | "\\\"" )+
// rawString = /[a-z0-9-_.]+/i
export const parse = invoke(() => {
let i
let n
let input
// -----
const backtrace = parser => () => {
const pos = i
const node = parser()
if (node != null) {
return node
}
i = pos
}
// -----
const parseAnd = () => parseTerms(createAnd)
const parseTerms = fn => {
let term = parseTerm()
if (!term) {
return
}
const terms = [ term ]
while ((term = parseTerm())) {
terms.push(term)
}
return fn(terms)
}
const parseTerm = () => {
parseWs()
const child = (
parseGroupedAnd() ||
parseOr() ||
parseNot() ||
parseProperty() ||
parseString()
)
if (child) {
parseWs()
return child
}
}
const parseWs = () => {
while (input[i] === ' ') {
++i
}
return true
}
const parseGroupedAnd = backtrace(() => {
let and
if (
input[i++] === '(' &&
(and = parseAnd()) &&
input[i++] === ')'
) {
return and
}
})
const parseOr = backtrace(() => {
let or
if (
input[i++] === '|' &&
parseWs() &&
input[i++] === '(' &&
(or = parseTerms(createOr)) &&
input[i++] === ')'
) {
return or
}
})
const parseNot = backtrace(() => {
let child
if (
input[i++] === '!' &&
(child = parseTerm())
) {
return createNot(child)
}
})
const parseProperty = backtrace(() => {
let name, child
if (
(name = parseString()) &&
parseWs() &&
(input[i++] === ':') &&
(child = parseTerm())
) {
return createProperty(name.value, child)
}
})
const parseString = () => {
let value
if (
(value = parseQuotedString()) != null ||
(value = parseRawString()) != null
) {
return createString(value)
}
}
const parseQuotedString = backtrace(() => {
if (input[i++] !== '"') {
return
}
const value = []
let char
while (i < n && (char = input[i++]) !== '"') {
if (char === '\\') {
char = input[i++]
}
value.push(char)
}
return value.join('')
})
const parseRawString = () => {
let value = ''
let c
while (
(c = input[i]) &&
RAW_STRING_CHARS[c]
) {
++i
value += c
}
if (value.length) {
return value
}
}
return input_ => {
if (!input_) {
return
}
i = 0
input = input_.split('')
n = input.length
try {
return parseAnd()
} finally {
input = null
}
}
})
// -------------------------------------------------------------------
const _getPropertyClauseStrings = ({ child }) => {
const { type } = child
if (type === 'or') {
const strings = []
forEach(child.children, child => {
if (child.type === 'string') {
strings.push(child.value)
}
})
return strings
}
if (type === 'string') {
return [ child.value ]
}
return []
}
// Find possible values for property clauses in a and clause.
export const getPropertyClausesStrings = function () {
if (!this) {
return {}
}
const { type } = this
if (type === 'property') {
return {
[this.name]: _getPropertyClauseStrings(this)
}
}
if (type === 'and') {
const strings = {}
forEach(this.children, node => {
if (node.type === 'property') {
const { name } = node
const values = strings[name]
if (values) {
values.push.apply(values, _getPropertyClauseStrings(node))
} else {
strings[name] = _getPropertyClauseStrings(node)
}
}
})
return strings
}
return {}
}
// -------------------------------------------------------------------
export const removePropertyClause = function (name) {
let type
if (
!this ||
(type = this.type) === 'property' && this.name === name
) {
return
}
if (type === 'and') {
return createAnd(filter(this.children, node =>
node.type !== 'property' || node.name !== name
))
}
return this
}
// -------------------------------------------------------------------
const _addAndClause = (node, child, predicate, reducer) =>
createAnd(filterReduce(
node.type === 'and'
? node.children
: [ node ],
predicate,
reducer,
child
))
export const setPropertyClause = function (name, child) {
const property = createProperty(
name,
isString(child) ? createString(child) : child
)
if (!this) {
return property
}
return _addAndClause(
this,
property,
node => node.type === 'property' && node.name === name,
)
}
// -------------------------------------------------------------------
export const execute = invoke(() => {
const visitors = {
and: ({ children }, value) => (
every(children, child => child::execute(value))
),
not: ({ child }, value) => (
!child::execute(value)
),
or: ({ children }, value) => (
some(children, child => child::execute(value))
),
property: ({ name, child }, value) => (
value != null && child::execute(value[name])
),
string: invoke(() => {
const match = (pattern, value) => {
if (isString(value)) {
return value.toLowerCase().indexOf(pattern) !== -1
}
if (isArray(value) || isPlainObject(value)) {
return some(value, value => match(pattern, value))
}
return false
}
return ({ value: pattern }, value) => (
match(pattern.toLowerCase(), value)
)
})
}
return function (value) {
return visitors[this.type](this, value)
}
})
// -------------------------------------------------------------------
export const toString = invoke(() => {
const toStringTerms = terms => map(terms, toString).join(' ')
const toStringGroup = terms => `(${toStringTerms(terms)})`
const visitors = {
and: ({ children }) => toStringGroup(children),
not: ({ child }) => `!${toString(child)}`,
or: ({ children }) => `|${toStringGroup(children)}`,
property: ({ name, child }) => `${toString(createString(name))}:${toString(child)}`,
string: ({ value }) => isRawString(value)
? value
: `"${value.replace(/\\|"/g, match => `\\${match}`)}"`
}
const toString = node => visitors[node.type](node)
// Special case for a root “and”: do not add braces.
return function () {
return !this
? ''
: this.type === 'and'
? toStringTerms(this.children)
: toString(this)
}
})
// -------------------------------------------------------------------
export const create = pattern => {
pattern = parse(pattern)
if (!pattern) {
return
}
return value => pattern::execute(value)
}

View File

@@ -1,53 +0,0 @@
import test from 'ava'
import {
getPropertyClausesStrings,
parse,
setPropertyClause,
toString
} from './'
import {
ast,
pattern
} from './index.fixtures'
test('getPropertyClausesStrings', t => {
let tmp = parse('foo bar:baz baz:|(foo bar)')::getPropertyClausesStrings()
t.deepEqual(
tmp,
{
bar: [ 'baz' ],
baz: [ 'foo', 'bar' ]
}
)
})
test('parse', t => {
t.deepEqual(parse(pattern), ast)
})
test('setPropertyClause', t => {
t.is(
null::setPropertyClause('foo', 'bar')::toString(),
'foo:bar'
)
t.is(
parse('baz')::setPropertyClause('foo', 'bar')::toString(),
'baz foo:bar'
)
t.is(
parse('plip foo:baz plop')::setPropertyClause('foo', 'bar')::toString(),
'plip plop foo:bar'
)
t.is(
parse('foo:|(baz plop)')::setPropertyClause('foo', 'bar')::toString(),
'foo:bar'
)
})
test('toString', t => {
t.is(pattern, ast::toString())
})

View File

@@ -1,31 +1,34 @@
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 propTypes from '../prop-types-decorator'
import Tooltip from '../tooltip'
import styles from './index.css'
const Copiable = propTypes({
data: propTypes.string,
tagName: propTypes.string
})(({ className, tagName = 'span', ...props }) => createElement(
tagName,
{
...props,
className: classNames(styles.container, className)
},
props.children,
' ',
<Tooltip content={_('copyToClipboard')}>
<CopyToClipboard text={props.data || props.children}>
<button className={classNames('btn btn-sm btn-secondary', styles.button)}>
<Icon icon='clipboard' />
</button>
</CopyToClipboard>
</Tooltip>
))
tagName: propTypes.string,
})(({ className, tagName = 'span', ...props }) =>
createElement(
tagName,
{
...props,
className: classNames(styles.container, className),
},
props.children,
' ',
<Tooltip content={_('copyToClipboard')}>
<CopyToClipboard text={props.data || props.children}>
<Button className={styles.button} size='small'>
<Icon icon='clipboard' />
</Button>
</CopyToClipboard>
</Tooltip>
)
)
export { Copiable as default }

View File

@@ -0,0 +1,60 @@
import React from 'react'
import { debounce } from 'lodash'
import getEventValue from './get-event-value'
const DEFAULT_DELAY = ({ debounceTimeout = 250 }) => debounceTimeout
const debounceComponentDecorator = (delay = DEFAULT_DELAY) => Component =>
class DebouncedComponent extends React.Component {
constructor (props) {
super()
this.state = { value: props.value }
this._notify = debounce(event => {
this.props.onChange(event)
}, typeof delay === 'function' ? delay(props) : delay)
this._onChange = event => {
this.setState({ value: getEventValue(event) })
event.persist()
this._notify(event)
}
this._wrappedInstance = null
this._onRef = ref => {
this._wrappedInstance = ref
}
}
componentWillReceiveProps ({ value }) {
if (value !== this.props.value) {
this._notify.cancel()
this.setState({ value })
}
}
componentWillUnmount () {
this._notify.flush()
}
getWrappedInstance () {
return this._wrappedInstance
}
render () {
const props = {
...this.props,
onChange: this._onChange,
ref: this._onRef,
value: this.state.value,
}
return <Component {...props} />
}
}
export { debounceComponentDecorator as default }
// common components
export const Input = debounceComponentDecorator()('input')
export const Textarea = debounceComponentDecorator()('textarea')

View File

@@ -1,19 +1,21 @@
import React, { Component, PropTypes } from 'react'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { isPromise } from 'promise-toolbox'
const toString = value => JSON.stringify(value, null, 2)
const toString = value =>
value === undefined ? 'undefined' : JSON.stringify(value, null, 2)
// This component does not handle changes in its `promise` property.
class DebugAsync extends Component {
static propTypes = {
promise: PropTypes.object.isRequired
promise: PropTypes.object.isRequired,
}
constructor (props) {
super()
this.state = {
status: 'pending'
status: 'pending',
}
props.promise.then(
@@ -33,21 +35,26 @@ class DebugAsync extends Component {
return <pre>{'Promise { <pending> }'}</pre>
}
return <pre>
{'Promise { '}
{status === 'rejected' && '<rejected> '}
{toString(value)}
{' }'}
</pre>
return (
<pre>
{'Promise { '}
{status === 'rejected' && '<rejected> '}
{toString(value)}
{' }'}
</pre>
)
}
}
const Debug = ({ value }) => isPromise(value)
? <DebugAsync promise={value} />
: <pre>{toString(value)}</pre>
const Debug = ({ value }) =>
isPromise(value) ? (
<DebugAsync promise={value} />
) : (
<pre>{toString(value)}</pre>
)
Debug.propTypes = {
value: PropTypes.any.isRequired
value: PropTypes.any.isRequired,
}
export { Debug as default }

View File

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

View File

@@ -11,7 +11,7 @@ import Component from '../base-component'
import getEventValue from '../get-event-value'
import Icon from '../icon'
import logError from '../log-error'
import propTypes from '../prop-types'
import propTypes from '../prop-types-decorator'
import Tooltip from '../tooltip'
import { formatSize } from '../utils'
import { SizeInput } from '../form'
@@ -25,8 +25,9 @@ import {
SelectSr,
SelectSubject,
SelectTag,
SelectVgpuType,
SelectVm,
SelectVmTemplate
SelectVmTemplate,
} from '../select-objects'
import styles from './index.css'
@@ -34,14 +35,14 @@ import styles from './index.css'
const LONG_CLICK = 400
@propTypes({
alt: propTypes.node.isRequired
alt: propTypes.node.isRequired,
})
class Hover extends Component {
constructor () {
super()
this.state = {
hover: false
hover: false,
}
this._onMouseEnter = () => this.setState({ hover: true })
@@ -50,25 +51,18 @@ class Hover extends Component {
render () {
if (this.state.hover) {
return <span onMouseLeave={this._onMouseLeave}>
{this.props.alt}
</span>
return <span onMouseLeave={this._onMouseLeave}>{this.props.alt}</span>
}
return <span onMouseEnter={this._onMouseEnter}>
{this.props.children}
</span>
return <span onMouseEnter={this._onMouseEnter}>{this.props.children}</span>
}
}
@propTypes({
onChange: propTypes.func.isRequired,
onUndo: propTypes.oneOfType([
propTypes.bool,
propTypes.func
]),
onUndo: propTypes.oneOfType([propTypes.bool, propTypes.func]),
useLongClick: propTypes.bool,
value: propTypes.any.isRequired
value: propTypes.any.isRequired,
})
class Editable extends Component {
get value () {
@@ -94,7 +88,7 @@ class Editable extends Component {
this.setState({
editing: true,
error: null,
saving: false
saving: false,
})
}
@@ -112,10 +106,7 @@ class Editable extends Component {
}
_save () {
return this.__save(
() => this.value,
this.props.onChange
)
return this.__save(() => this.value, this.props.onChange)
}
async __save (getValue, saveValue) {
@@ -138,7 +129,7 @@ class Editable extends Component {
this.setState({
// `error` may be undefined if the action has been cancelled
error: error !== undefined && (isString(error) ? error : error.message),
saving: false
saving: false,
})
logError(error)
}
@@ -161,34 +152,59 @@ class Editable extends Component {
const { useLongClick } = props
const success = <Icon icon='success' />
return <span className={classNames(styles.clickToEdit, !useLongClick && styles.shortClick)}>
return (
<span
onClick={!useLongClick && this._openEdition}
onMouseDown={useLongClick && this.__startTimer}
onMouseUp={useLongClick && this.__stopTimer}
className={classNames(
styles.clickToEdit,
!useLongClick && styles.shortClick
)}
>
{this._renderDisplay()}
</span>
{previous != null && (onUndo !== false
? <Hover
alt={<a onClick={this._undo}><Icon icon='undo' /></a>}
<span
onClick={!useLongClick && this._openEdition}
onMouseDown={useLongClick && this.__startTimer}
onMouseUp={useLongClick && this.__stopTimer}
>
{success}
</Hover>
: success
)}
</span>
{this._renderDisplay()}
</span>
{previous != null &&
(onUndo !== false ? (
<Hover
alt={
<a onClick={this._undo}>
<Icon icon='undo' />
</a>
}
>
{success}
</Hover>
) : (
success
))}
</span>
)
}
const { error, saving } = state
return <span>
{this._renderEdition()}
{saving && <span>{' '}<Icon icon='loading' /></span>}
{error != null && <span>
{' '}<Tooltip content={error}><Icon icon='error' /></Tooltip>
</span>}
</span>
return (
<span>
{this._renderEdition()}
{saving && (
<span>
{' '}
<Icon icon='loading' />
</span>
)}
{error != null && (
<span>
{' '}
<Tooltip content={error}>
<Icon icon='error' />
</Tooltip>
</span>
)}
</span>
)
}
}
@@ -197,7 +213,7 @@ class Editable extends Component {
maxLength: propTypes.number,
minLength: propTypes.number,
pattern: propTypes.string,
value: propTypes.string.isRequired
value: propTypes.string.isRequired,
})
export class Text extends Editable {
get value () {
@@ -217,25 +233,22 @@ export class Text extends Editable {
}
_renderDisplay () {
const {
children,
value
} = this.props
const { children, value } = this.props
if (children || value) {
return <span> {children || value} </span>
}
const {
placeholder,
useLongClick
} = this.props
const { placeholder, useLongClick } = this.props
return <span className='text-muted'>
{placeholder ||
(useLongClick ? _('editableLongClickPlaceholder') : _('editableClickPlaceholder'))
}
</span>
return (
<span className='text-muted'>
{placeholder ||
(useLongClick
? _('editableLongClickPlaceholder')
: _('editableClickPlaceholder'))}
</span>
)
}
_renderEdition () {
@@ -247,25 +260,26 @@ export class Text extends Editable {
'autoComplete',
'maxLength',
'minLength',
'pattern'
'pattern',
])
return <input
{...extraProps}
autoFocus
defaultValue={value}
onBlur={this._closeEdition}
onInput={this._onInput}
onKeyDown={this._onKeyDown}
readOnly={saving}
ref='input'
style={{
width: `${value.length + 1}ex`,
maxWidth: '50ex'
}}
type={this._isPassword ? 'password' : 'text'}
/>
return (
<input
{...extraProps}
autoFocus
defaultValue={value}
onBlur={this._closeEdition}
onInput={this._onInput}
onKeyDown={this._onKeyDown}
readOnly={saving}
ref='input'
style={{
width: `${value.length + 1}ex`,
maxWidth: '50ex',
}}
type={this._isPassword ? 'password' : 'text'}
/>
)
}
}
@@ -277,7 +291,7 @@ export class Password extends Text {
@propTypes({
nullable: propTypes.bool,
value: propTypes.number
value: propTypes.number,
})
export class Number extends Component {
get value () {
@@ -300,20 +314,19 @@ export class Number extends Component {
render () {
const { value } = this.props
return <Text
{...this.props}
onChange={this._onChange}
value={value === null ? '' : String(value)}
/>
return (
<Text
{...this.props}
onChange={this._onChange}
value={value === null ? '' : String(value)}
/>
)
}
}
@propTypes({
options: propTypes.oneOfType([
propTypes.array,
propTypes.object
]).isRequired,
renderer: propTypes.func
options: propTypes.oneOfType([propTypes.array, propTypes.object]).isRequired,
renderer: propTypes.func,
})
export class Select extends Editable {
componentWillReceiveProps (props) {
@@ -321,7 +334,9 @@ export class Select extends Editable {
props.value !== this.props.value ||
props.options !== this.props.options
) {
this.setState({ valueKey: findKey(props.options, option => option === props.value) })
this.setState({
valueKey: findKey(props.options, option => option === props.value),
})
}
}
@@ -336,12 +351,11 @@ export class Select extends Editable {
_optionToJsx = (option, key) => {
const { renderer } = this.props
return <option
key={key}
value={key}
>
{renderer ? renderer(option) : option}
</option>
return (
<option key={key} value={key}>
{renderer ? renderer(option) : option}
</option>
)
}
_onEditionMount = ref => {
@@ -352,26 +366,27 @@ export class Select extends Editable {
_renderDisplay () {
const { children, renderer, value } = this.props
return children ||
<span>{renderer ? renderer(value) : value}</span>
return children || <span>{renderer ? renderer(value) : value}</span>
}
_renderEdition () {
const { saving, valueKey } = this.state
const { options } = this.props
return <select
autoFocus
className={classNames('form-control', styles.select)}
onBlur={this._closeEdition}
onChange={this._onChange}
onKeyDown={this._onKeyDown}
readOnly={saving}
ref={this._onEditionMount}
value={valueKey}
>
{map(options, this._optionToJsx)}
</select>
return (
<select
autoFocus
className={classNames('form-control', styles.select)}
onBlur={this._closeEdition}
onChange={this._onChange}
onKeyDown={this._onKeyDown}
readOnly={saving}
ref={this._onEditionMount}
value={valueKey}
>
{map(options, this._optionToJsx)}
</select>
)
}
}
@@ -385,37 +400,31 @@ const MAP_TYPE_SELECT = {
SR: SelectSr,
subject: SelectSubject,
tag: SelectTag,
vgpuType: SelectVgpuType,
VM: SelectVm,
'VM-template': SelectVmTemplate
'VM-template': SelectVmTemplate,
}
@propTypes({
labelProp: propTypes.string.isRequired,
value: propTypes.oneOfType([
propTypes.string,
propTypes.object
]).isRequired
value: propTypes.oneOfType([propTypes.string, propTypes.object]),
})
export class XoSelect extends Editable {
get value () {
return this.refs.select.value
return this.state.value
}
_renderDisplay () {
return this.props.children ||
<span>{this.props.value[this.props.labelProp]}</span>
return (
this.props.children || (
<span>{this.props.value[this.props.labelProp]}</span>
)
)
}
_onChange = object => {
object ? this._save() : this._closeEdition()
}
_onChange = object => this.setState({ value: object }, object && this._save)
_renderEdition () {
const {
saving,
xoType,
...props
} = this.props
const { saving, xoType, ...props } = this.props
const Select = MAP_TYPE_SELECT[xoType]
if (process.env.NODE_ENV !== 'production') {
@@ -426,20 +435,21 @@ export class XoSelect extends Editable {
// Anchor is needed so that the BlockLink does not trigger a redirection
// when this element is clicked.
return <a onBlur={this._closeEdition}>
<Select
{...props}
autoFocus
disabled={saving}
onChange={this._onChange}
ref='select'
/>
</a>
return (
<a onBlur={this._closeEdition}>
<Select
{...props}
autoFocus
disabled={saving}
onChange={this._onChange}
/>
</a>
)
}
}
@propTypes({
value: propTypes.number.isRequired
value: propTypes.number.isRequired,
})
export class Size extends Editable {
get value () {
@@ -457,27 +467,31 @@ export class Size extends Editable {
}, 10)
}
_focus = () => { this._focused = true }
_focus = () => {
this._focused = true
}
_renderEdition () {
const { saving } = this.state
const { value } = this.props
return <span
// SizeInput uses `input-group` which makes it behave as a block element (display: table).
// `form-inline` to use it as an inline element
className='form-inline'
onBlur={this._closeEditionIfUnfocused}
onFocus={this._focus}
onKeyDown={this._onKeyDown}
>
<SizeInput
autoFocus
className={styles.size}
ref='input'
readOnly={saving}
defaultValue={value}
/>
</span>
return (
<span
// SizeInput uses `input-group` which makes it behave as a block element (display: table).
// `form-inline` to use it as an inline element
className='form-inline'
onBlur={this._closeEditionIfUnfocused}
onFocus={this._focus}
onKeyDown={this._onKeyDown}
>
<SizeInput
autoFocus
className={styles.size}
ref='input'
readOnly={saving}
defaultValue={value}
/>
</span>
)
}
}

View File

@@ -3,24 +3,22 @@ import React from 'react'
const ellipsisStyle = {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
whiteSpace: 'nowrap',
}
const ellipsisContainerStyle = {
display: 'flex'
display: 'flex',
}
const Ellipsis = ({ children }) => (
<span style={ellipsisStyle}>
{children}
</span>
)
const Ellipsis = ({ children }) => <span style={ellipsisStyle}>{children}</span>
export { Ellipsis as default }
export const EllipsisContainer = ({ children }) => (
<div style={ellipsisContainerStyle}>
{React.Children.map(children, child =>
child == null || child.type === Ellipsis ? child : <span>{child}</span>
{React.Children.map(
children,
child =>
child == null || child.type === Ellipsis ? child : <span>{child}</span>
)}
</div>
)

11
src/common/fetch.js Normal file
View File

@@ -0,0 +1,11 @@
import 'whatwg-fetch'
const { fetch } = window
export { fetch as default }
export const post = (url, body, opts) =>
fetch(url, {
...opts,
body,
method: 'POST',
})

View File

@@ -11,14 +11,8 @@ import identity from 'lodash/identity'
const filterReduce = (array, predicate, reducer, initial) => {
const { length } = array
let i
if (
!length ||
!predicate ||
(i = findIndex(array, predicate)) === -1
) {
return initial == null
? array.slice(0)
: array.concat(initial)
if (!length || !predicate || (i = findIndex(array, predicate)) === -1) {
return initial == null ? array.slice(0) : array.concat(initial)
}
if (reducer == null) {
@@ -26,9 +20,7 @@ const filterReduce = (array, predicate, reducer, initial) => {
}
const result = array.slice(0, i)
let value = initial == null
? array[i]
: reducer(initial, array[i], i, array)
let value = initial == null ? array[i] : reducer(initial, array[i], i, array)
for (i = i + 1; i < length; ++i) {
const current = array[i]

View File

@@ -1,28 +1,19 @@
import test from 'ava'
/* eslint-env jest */
import filterReduce from './filter-reduce'
const add = (a, b) => a + b
const data = [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
const data = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
const isEven = x => !(x & 1)
test('filterReduce', t => {
it('filterReduce', () => {
// Returns all elements not matching the predicate and the result of
// a reduction over those who do.
t.deepEqual(
filterReduce(data, isEven, add),
[ 1, 3, 5, 7, 9, 20 ]
)
expect(filterReduce(data, isEven, add)).toEqual([1, 3, 5, 7, 9, 20])
// The default reducer is the identity.
t.deepEqual(
filterReduce(data, isEven),
[ 1, 3, 5, 7, 9, 0 ]
)
expect(filterReduce(data, isEven)).toEqual([1, 3, 5, 7, 9, 0])
// If an initial value is passed it is used.
t.deepEqual(
filterReduce(data, isEven, add, 22),
[ 1, 3, 5, 7, 9, 42 ]
)
expect(filterReduce(data, isEven, add, 22)).toEqual([1, 3, 5, 7, 9, 42])
})

View File

@@ -1,24 +1,18 @@
import React from 'react'
import * as Grid from './grid'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
export const LabelCol = propTypes({
children: propTypes.any.isRequired
children: propTypes.any.isRequired,
})(({ children }) => (
<label className='col-md-2 form-control-label'>{children}</label>
))
export const InputCol = propTypes({
children: propTypes.any.isRequired
})(({ children }) => (
<Grid.Col mediumSize={10}>{children}</Grid.Col>
))
children: propTypes.any.isRequired,
})(({ children }) => <Grid.Col mediumSize={10}>{children}</Grid.Col>)
export const Row = propTypes({
children: propTypes.arrayOf(propTypes.element).isRequired
})(({ children }) => (
<Grid.Row className='form-group'>
{children}
</Grid.Row>
))
children: propTypes.arrayOf(propTypes.element).isRequired,
})(({ children }) => <Grid.Row className='form-group'>{children}</Grid.Row>)

View File

@@ -5,18 +5,16 @@ import map from 'lodash/map'
import randomPassword from 'random-password'
import React from 'react'
import round from 'lodash/round'
import {
DropdownButton,
MenuItem
} from 'react-bootstrap-4/lib'
import SingleLineRow from 'single-line-row'
import { Container, Col } from 'grid'
import { DropdownButton, MenuItem } from 'react-bootstrap-4/lib'
import Button from '../button'
import Component from '../base-component'
import propTypes from '../prop-types'
import {
firstDefined,
formatSizeRaw,
parseSize
} from '../utils'
import defined from '../xo-defined'
import getEventValue from '../get-event-value'
import propTypes from '../prop-types-decorator'
import { formatSizeRaw, parseSize } from '../utils'
export Select from './select'
export SelectPlainObject from './select-plain-object'
@@ -24,7 +22,7 @@ export SelectPlainObject from './select-plain-object'
// ===================================================================
@propTypes({
enableGenerator: propTypes.bool
enableGenerator: propTypes.bool,
})
export class Password extends Component {
get value () {
@@ -47,109 +45,87 @@ export class Password extends Component {
// FIXME: in controlled mode, visibility should only be updated
// when the value prop is changed according to the emitted value.
this.setState({
visible: true
visible: true,
})
}
_toggleVisibility = () => {
this.setState({
visible: !this.state.visible
visible: !this.state.visible,
})
}
render () {
const {
className,
enableGenerator = false,
...props
} = this.props
const { className, enableGenerator = false, ...props } = this.props
const { visible } = this.state
return <div className='input-group'>
{enableGenerator && <span className='input-group-btn'>
<button type='button' className='btn btn-secondary' onClick={this._generate}>
<Icon icon='password' />
</button>
</span>}
<input
{...props}
className={classNames(className, 'form-control')}
ref='field'
type={visible ? 'text' : 'password'}
/>
<span className='input-group-btn'>
<button type='button' className='btn btn-secondary' onClick={this._toggleVisibility}>
<Icon icon={visible ? 'shown' : 'hidden'} />
</button>
</span>
</div>
return (
<div className='input-group'>
{enableGenerator && (
<span className='input-group-btn'>
<Button onClick={this._generate}>
<Icon icon='password' />
</Button>
</span>
)}
<input
{...props}
className={classNames(className, 'form-control')}
ref='field'
type={visible ? 'text' : 'password'}
/>
<span className='input-group-btn'>
<Button onClick={this._toggleVisibility}>
<Icon icon={visible ? 'shown' : 'hidden'} />
</Button>
</span>
</div>
)
}
}
// ===================================================================
@propTypes({
defaultValue: propTypes.number,
max: propTypes.number.isRequired,
min: propTypes.number.isRequired,
onChange: propTypes.func,
step: propTypes.number,
onChange: propTypes.func
value: propTypes.number,
})
export class Range extends Component {
constructor (props) {
super()
this.state = {
value: props.defaultValue || props.min
componentDidMount () {
const { min, onChange, value } = this.props
if (!value) {
onChange && onChange(min)
}
}
get value () {
return this.state.value
}
set value (value) {
this.setState({
value: +value
})
}
_handleChange = event => {
const { onChange } = this.props
const { value } = event.target
if (value === this.state.value) {
return
}
this.setState({
value
}, onChange && (() => onChange(value)))
}
_onChange = value => this.props.onChange(getEventValue(value))
render () {
const {
props
} = this
const step = props.step || 1
const { value } = this.state
const { max, min, step, value } = this.props
return (
<div className='form-group row'>
<label className='col-sm-2 control-label'>
{value}
</label>
<div className='col-sm-10'>
<input
className='form-control'
type='range'
min={props.min}
max={props.max}
step={step}
value={value}
onChange={this._handleChange}
/>
</div>
</div>
<Container>
<SingleLineRow>
<Col size={2}>
<span className='pull-right'>{value}</span>
</Col>
<Col size={10}>
<input
className='form-control'
max={max}
min={min}
onChange={this._onChange}
step={step}
type='range'
value={value}
/>
</Col>
</SingleLineRow>
</Container>
)
}
}
@@ -168,16 +144,15 @@ const DEFAULT_UNIT = 'GiB'
readOnly: propTypes.bool,
required: propTypes.bool,
style: propTypes.object,
value: propTypes.oneOfType([
propTypes.number,
propTypes.oneOf([ null ])
])
value: propTypes.oneOfType([propTypes.number, propTypes.oneOf([null])]),
})
export class SizeInput extends BaseComponent {
constructor (props) {
super(props)
this.state = this._createStateFromBytes(firstDefined(props.value, props.defaultValue, null))
this.state = this._createStateFromBytes(
defined(props.value, props.defaultValue, null)
)
}
componentWillReceiveProps (props) {
@@ -191,21 +166,21 @@ export class SizeInput extends BaseComponent {
if (bytes === this._bytes) {
return {
input: this._input,
unit: this._unit
unit: this._unit,
}
}
if (bytes === null) {
return {
input: '',
unit: this.props.defaultUnit || DEFAULT_UNIT
unit: this.props.defaultUnit || DEFAULT_UNIT,
}
}
const { prefix, value } = formatSizeRaw(bytes)
return {
input: String(round(value, 2)),
unit: `${prefix}B`
unit: `${prefix}B`,
}
}
@@ -233,9 +208,7 @@ export class SizeInput extends BaseComponent {
const { onChange } = this.props
// Empty input equals null.
const bytes = input
? parseSize(`${+input} ${unit}`)
: null
const bytes = input ? parseSize(`${+input} ${unit}`) : null
const isControlled = this.props.value !== undefined
if (isControlled) {
@@ -265,8 +238,7 @@ export class SizeInput extends BaseComponent {
const number = +input
// NaN: do not ack this change.
if (number !== number) { // eslint-disable-line no-self-compare
if (Number.isNaN(number)) {
return
}
@@ -297,38 +269,37 @@ export class SizeInput extends BaseComponent {
readOnly,
placeholder,
required,
style
style,
} = this.props
return <span className={classNames('input-group', className)} style={style}>
<input
autoFocus={autoFocus}
className='form-control'
disabled={readOnly}
onChange={this._updateNumber}
placeholder={placeholder}
required={required}
type='text'
value={this.state.input}
/>
<span className='input-group-btn'>
<DropdownButton
bsStyle='secondary'
id='size'
pullRight
return (
<span className={classNames('input-group', className)} style={style}>
<input
autoFocus={autoFocus}
className='form-control'
disabled={readOnly}
title={this.state.unit}
>
{map(UNITS, unit =>
<MenuItem
key={unit}
onClick={() => this._updateUnit(unit)}
>
{unit}
</MenuItem>
)}
</DropdownButton>
onChange={this._updateNumber}
placeholder={placeholder}
required={required}
type='text'
value={this.state.input}
/>
<span className='input-group-btn'>
<DropdownButton
bsStyle='secondary'
id='size'
pullRight
disabled={readOnly}
title={this.state.unit}
>
{map(UNITS, unit => (
<MenuItem key={unit} onClick={() => this._updateUnit(unit)}>
{unit}
</MenuItem>
))}
</DropdownButton>
</span>
</span>
</span>
)
}
}

View File

@@ -1,76 +1,82 @@
import uncontrollableInput from 'uncontrollable-input'
import Component from 'base-component'
import find from 'lodash/find'
import map from 'lodash/map'
import React, { Component } from 'react'
import React from 'react'
import propTypes from '../prop-types'
import propTypes from '../prop-types-decorator'
import Select from './select'
@propTypes({
autoFocus: propTypes.bool,
defaultValue: propTypes.any,
disabled: propTypes.bool,
optionRenderer: propTypes.func,
multi: propTypes.bool,
onChange: propTypes.func,
options: propTypes.array,
placeholder: propTypes.string,
placeholder: propTypes.node,
predicate: propTypes.func,
required: propTypes.bool
required: propTypes.bool,
value: propTypes.any,
})
@uncontrollableInput()
export default class SelectPlainObject extends Component {
constructor (props) {
super(props)
this.state = {
value: this._computeValue(props.defaultValue, props)
componentDidMount () {
const { options, value } = this.props
this.setState({
options: this._computeOptions(options),
value: this._computeValue(value, this.props),
})
}
componentWillReceiveProps (newProps) {
if (newProps !== this.props) {
this.setState({
options: this._computeOptions(newProps.options),
value: this._computeValue(newProps.value, newProps),
})
}
}
_computeValue (value, props = this.props) {
let { optionKey } = props
optionKey || (optionKey = 'id')
const reduceValue = value => value != null ? (value[optionKey] || value) : ''
const reduceValue = value =>
value != null ? value[optionKey] || value : ''
if (props.multi) {
if (!Array.isArray(value)) {
value = [value]
}
return map(value, reduceValue)
}
return reduceValue(value)
}
componentWillMount () {
const { options } = this.props
this.setState({
options: this._computeOptions(options)
})
}
componentWillReceiveProps (newProps) {
const { options } = newProps
this.setState({
options: this._computeOptions(options)
})
}
_computeOptions (options) {
const { optionKey = 'id' } = this.props
const { optionRenderer = o => o.label || o[optionKey] || o } = this.props
return map(options, option => ({
value: option[optionKey] || option,
label: optionRenderer(option)
label: optionRenderer(option),
}))
}
get value () {
const { optionKey = 'id' } = this.props
const { value } = this.state
const { options } = this.props
_getObject (value) {
if (value == null) {
return undefined
}
const { optionKey = 'id', options } = this.props
const pickValue = value => {
value = value.value || value
return find(options, option => option[optionKey] === value || option === value)
return find(
options,
option => option[optionKey] === value || option === value
)
}
if (this.props.multi) {
@@ -80,18 +86,12 @@ export default class SelectPlainObject extends Component {
return pickValue(value)
}
set value (value) {
this.setState({
value: this._computeValue(value)
})
}
_handleChange = value => {
const { onChange } = this.props
this.setState({
value: this._computeValue(value)
}, onChange && (() => { onChange(this.value) }))
if (onChange) {
onChange(this._getObject(value))
}
}
_renderOption = option => option.label
@@ -111,7 +111,8 @@ export default class SelectPlainObject extends Component {
placeholder={props.placeholder}
required={props.required}
value={state.value}
valueRenderer={this._renderOption} />
valueRenderer={this._renderOption}
/>
)
}
}

View File

@@ -2,39 +2,35 @@ import map from 'lodash/map'
import React, { Component } from 'react'
import ReactSelect from 'react-select'
import sum from 'lodash/sum'
import {
AutoSizer,
CellMeasurer,
List
} from 'react-virtualized'
import { AutoSizer, CellMeasurer, List } from 'react-virtualized'
import propTypes from '../prop-types'
import propTypes from '../prop-types-decorator'
const SELECT_MENU_STYLE = {
overflow: 'hidden'
overflow: 'hidden',
}
const SELECT_STYLE = {
minWidth: '10em'
minWidth: '10em',
}
const LIST_STYLE = {
whiteSpace: 'normal',
}
const MAX_OPTIONS = 5
// See: https://github.com/bvaughn/react-virtualized-select/blob/master/source/VirtualizedSelect/VirtualizedSelect.js
@propTypes({
maxHeight: propTypes.number
maxHeight: propTypes.number,
})
export default class Select extends Component {
static defaultProps = {
maxHeight: 200,
optionRenderer: (option, labelKey) => option[labelKey]
optionRenderer: (option, labelKey) => option[labelKey],
}
_renderMenu = ({
focusedOption,
options,
...otherOptions
}) => {
_renderMenu = ({ focusedOption, options, ...otherOptions }) => {
const { maxHeight } = this.props
const focusedOptionIndex = options.indexOf(focusedOption)
@@ -48,15 +44,17 @@ export default class Select extends Component {
key,
option: options[index],
options,
style
style,
})
return (
<AutoSizer disableHeight>
{({ width }) => (
{({ width }) =>
width ? (
<CellMeasurer
cellRenderer={({ rowIndex }) => wrappedRowRenderer({ index: rowIndex })}
cellRenderer={({ rowIndex }) =>
wrappedRowRenderer({ index: rowIndex })
}
columnCount={1}
rowCount={options.length}
// FIXME: 16 px: ugly workaround to take into account the scrollbar
@@ -66,21 +64,26 @@ export default class Select extends Component {
>
{({ getRowHeight }) => {
if (options.length <= MAX_OPTIONS) {
height = sum(map(options, (_, index) => getRowHeight({ index })))
height = sum(
map(options, (_, index) => getRowHeight({ index }))
)
}
return <List
height={height}
rowCount={options.length}
rowHeight={getRowHeight}
rowRenderer={wrappedRowRenderer}
scrollToIndex={focusedOptionIndex}
width={width}
/>
return (
<List
height={height}
rowCount={options.length}
rowHeight={getRowHeight}
rowRenderer={wrappedRowRenderer}
scrollToIndex={focusedOptionIndex}
style={LIST_STYLE}
width={width}
/>
)
}}
</CellMeasurer>
) : null
)}
}
</AutoSizer>
)
}
@@ -92,7 +95,7 @@ export default class Select extends Component {
labelKey,
option,
style,
selectValue
selectValue,
}) => {
let className = 'Select-option'
@@ -124,6 +127,7 @@ export default class Select extends Component {
render () {
return (
<ReactSelect
closeOnSelect={!this.props.multi}
{...this.props}
backspaceToRemoveMessage=''
menuRenderer={this._renderMenu}

46
src/common/form/toggle.js Normal file
View File

@@ -0,0 +1,46 @@
import React from 'react'
import classNames from 'classnames'
import uncontrollableInput from 'uncontrollable-input'
import Component from '../base-component'
import Icon from '../icon'
import propTypes from '../prop-types-decorator'
@uncontrollableInput()
@propTypes({
className: propTypes.string,
onChange: propTypes.func,
icon: propTypes.string,
iconOn: propTypes.string,
iconOff: propTypes.string,
iconSize: propTypes.number,
value: propTypes.bool,
})
export default class Toggle extends Component {
static defaultProps = {
iconOn: 'toggle-on',
iconOff: 'toggle-off',
iconSize: 2,
}
_toggle = () => {
const { props } = this
props.onChange(!props.value)
}
render () {
const { props } = this
return (
<Icon
className={classNames(
props.disabled ? 'text-muted' : props.value ? 'text-success' : null,
props.className
)}
icon={props.icon || (props.value ? props.iconOn : props.iconOff)}
onClick={this._toggle}
size={props.iconSize}
/>
)
}
}

View File

@@ -1,3 +0,0 @@
.checkbox {
display: none;
}

View File

@@ -1,90 +0,0 @@
import React from 'react'
import classNames from 'classnames'
import Component from '../../base-component'
import Icon from '../../icon'
import propTypes from '../../prop-types'
import styles from './index.css'
@propTypes({
className: propTypes.string,
defaultValue: propTypes.bool,
onChange: propTypes.func,
icon: propTypes.string,
iconOn: propTypes.string,
iconOff: propTypes.string,
iconSize: propTypes.number,
value: propTypes.bool
})
export default class Toggle extends Component {
static defaultProps = {
iconOn: 'toggle-on',
iconOff: 'toggle-off',
iconSize: 2
}
get value () {
const { props } = this
const { value } = props
if (value != null) {
return value
}
const { input } = this.refs
if (input) {
return input.checked
}
return props.defaultValue || false
}
set value (value) {
if (
process.env.NODE_ENV !== 'production' &&
this.props.value != null
) {
throw new Error('cannot set value of controlled Toggle')
}
this.refs.input.checked = Boolean(value)
this.forceUpdate()
}
_onChange = event => {
if (this.props.value == null) {
this.forceUpdate()
}
const { onChange } = this.props
onChange && onChange(event.target.checked)
}
render () {
const { props, value } = this
return (
<label
className={classNames(
props.disabled ? 'text-muted' : value ? 'text-success' : null,
props.className
)}
>
<Icon
icon={props.icon || (value ? props.iconOn : props.iconOff)}
size={props.iconSize}
/>
<input
checked={props.value}
className={styles.checkbox}
defaultChecked={props.defaultValue}
disabled={props.disabled}
onChange={this._onChange}
ref='input'
type='checkbox'
/>
</label>
)
}
}

View File

@@ -6,10 +6,8 @@ const getEventValue = event => {
return event
}
return (
target.nodeName.toLowerCase() === 'input' &&
return target.nodeName.toLowerCase() === 'input' &&
target.type.toLowerCase() === 'checkbox'
)
? target.checked
: target.value
}

View File

@@ -1,8 +1,9 @@
import classNames from 'classnames'
import React from 'react'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
// A column can contain content or a row.
export const Col = propTypes({
className: propTypes.string,
size: propTypes.number,
@@ -12,47 +13,49 @@ export const Col = propTypes({
offset: propTypes.number,
smallOffset: propTypes.number,
mediumOffset: propTypes.number,
largeOffset: propTypes.number
})(({
children,
className,
size = 12,
smallSize = size,
mediumSize,
largeSize,
offset,
smallOffset = offset,
mediumOffset,
largeOffset,
style
}) => <div className={classNames(
className,
smallSize && `col-xs-${smallSize}`,
mediumSize && `col-md-${mediumSize}`,
largeSize && `col-lg-${largeSize}`,
smallOffset && `offset-xs-${smallOffset}`,
mediumOffset && `offset-md-${mediumOffset}`,
largeOffset && `offset-lg-${largeOffset}`
)}
style={style}
>
{children}
</div>)
largeOffset: propTypes.number,
})(
({
children,
className,
size = 12,
smallSize = size,
mediumSize,
largeSize,
offset,
smallOffset = offset,
mediumOffset,
largeOffset,
style,
}) => (
<div
className={classNames(
className,
smallSize && `col-xs-${smallSize}`,
mediumSize && `col-md-${mediumSize}`,
largeSize && `col-lg-${largeSize}`,
smallOffset && `offset-xs-${smallOffset}`,
mediumOffset && `offset-md-${mediumOffset}`,
largeOffset && `offset-lg-${largeOffset}`
)}
style={style}
>
{children}
</div>
)
)
// This is the root component of the grid layout, containers should not be
// nested.
export const Container = propTypes({
className: propTypes.string
})(({
children,
className
}) => <div className={classNames(className, 'container-fluid')}>
{children}
</div>)
className: propTypes.string,
})(({ children, className }) => (
<div className={classNames(className, 'container-fluid')}>{children}</div>
))
// Only columns can be children of a row.
export const Row = propTypes({
className: propTypes.string
})(({
children,
className
}) => <div className={`${className || ''} row`}>
{children}
</div>)
className: propTypes.string,
})(({ children, className }) => (
<div className={`${className || ''} row`}>{children}</div>
))

13
src/common/grid.spec.js Normal file
View File

@@ -0,0 +1,13 @@
/* eslint-env jest */
import React from 'react'
import { forEach } from 'lodash'
import { shallow } from 'enzyme'
import * as grid from './grid'
forEach(grid, (Component, name) => {
it(name, () => {
expect(shallow(<Component />)).toMatchSnapshot()
})
})

View File

@@ -1,20 +1,33 @@
const common = {
homeFilterNone: '',
}
export const VM = {
...common,
homeFilterPendingVms: 'current_operations:"" ',
homeFilterNonRunningVms: '!power_state:running ',
homeFilterHvmGuests: 'virtualizationMode:hvm ',
homeFilterRunningVms: 'power_state:running ',
homeFilterTags: 'tags:'
homeFilterTags: 'tags:',
}
export const host = {
...common,
homeFilterRunningHosts: 'power_state:running ',
homeFilterTags: 'tags:'
homeFilterTags: 'tags:',
}
export const pool = {
homeFilterTags: 'tags:'
...common,
homeFilterTags: 'tags:',
}
export const vmTemplate = {
homeFilterTags: 'tags:'
...common,
homeFilterTags: 'tags:',
}
export const SR = {
...common,
homeFilterTags: 'tags:',
}

40
src/common/home-tags.js Normal file
View File

@@ -0,0 +1,40 @@
import * as CM from 'complex-matcher'
import React from 'react'
import Component from './base-component'
import propTypes from './prop-types-decorator'
import Tags from './tags'
@propTypes({
labels: propTypes.arrayOf(React.PropTypes.string).isRequired,
onAdd: propTypes.func,
onChange: propTypes.func,
onDelete: propTypes.func,
type: propTypes.string,
})
export default class HomeTags extends Component {
static contextTypes = {
router: React.PropTypes.object,
}
_onClick = label => {
const s = encodeURIComponent(
new CM.Property('tags', new CM.String(label)).toString()
)
const t = encodeURIComponent(this.props.type)
this.context.router.push(`/home?t=${t}&s=${s}`)
}
render () {
return (
<Tags
labels={this.props.labels}
onAdd={this.props.onAdd}
onChange={this.props.onChange}
onClick={this._onClick}
onDelete={this.props.onDelete}
/>
)
}
}

View File

@@ -1,27 +1,24 @@
import isEmpty from 'lodash/isEmpty'
import keys from 'lodash/keys'
import map from 'lodash/map'
import React from 'react'
import { Portal } from 'react-overlays'
import { forEach, isEmpty, keys, map, noop } from 'lodash'
import _ from './intl'
import ActionButton from './action-button'
import Component from './base-component'
import forEach from 'lodash/forEach'
import Link from './link'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
import SortedTable from './sorted-table'
import TabButton from './tab-button'
import { connectStore } from './utils'
import {
createGetObjectsOfType,
createFilter,
createSelector
createSelector,
} from './selectors'
import {
getHostMissingPatches,
installAllHostPatches,
installAllPatchesOnPool
installAllPatchesOnPool,
subscribeHostMissingPatches,
} from './xo'
// ===================================================================
@@ -29,18 +26,22 @@ import {
const MISSING_PATCHES_COLUMNS = [
{
name: _('srHost'),
itemRenderer: host => <Link to={`/hosts/${host.id}`}>{host.name_label}</Link>,
sortCriteria: host => host.name_label
itemRenderer: host => (
<Link to={`/hosts/${host.id}`}>{host.name_label}</Link>
),
sortCriteria: host => host.name_label,
},
{
name: _('hostDescription'),
itemRenderer: host => host.name_description,
sortCriteria: host => host.name_description
sortCriteria: host => host.name_description,
},
{
name: _('hostMissingPatches'),
itemRenderer: (host, { missingPatches }) => <Link to={`/hosts/${host.id}/patches`}>{missingPatches[host.id]}</Link>,
sortCriteria: (host, { missingPatches }) => missingPatches[host.id]
itemRenderer: (host, { missingPatches }) => (
<Link to={`/hosts/${host.id}/patches`}>{missingPatches[host.id]}</Link>
),
sortCriteria: (host, { missingPatches }) => missingPatches[host.id],
},
{
name: _('patchUpdateButton'),
@@ -51,21 +52,33 @@ const MISSING_PATCHES_COLUMNS = [
handlerParam={host}
icon='host-patch-update'
/>
)
}
),
},
]
const POOLS_MISSING_PATCHES_COLUMNS = [{
name: _('srPool'),
itemRenderer: (host, { pools }) => {
const pool = pools[host.$pool]
return <Link to={`/pools/${pool.id}`}>{pool.name_label}</Link>
const POOLS_MISSING_PATCHES_COLUMNS = [
{
name: _('srPool'),
itemRenderer: (host, { pools }) => {
const pool = pools[host.$pool]
return <Link to={`/pools/${pool.id}`}>{pool.name_label}</Link>
},
sortCriteria: (host, { pools }) => pools[host.$pool].name_label,
},
sortCriteria: (host, { pools }) => pools[host.$pool].name_label
}].concat(MISSING_PATCHES_COLUMNS)
].concat(MISSING_PATCHES_COLUMNS)
// Small component to homogenize Button usage in HostsPatchesTable
const ActionButton_ = ({ children, labelId, ...props }) => (
<ActionButton {...props} tooltip={_(labelId)}>
{children}
</ActionButton>
)
// ===================================================================
@connectStore({
hostsById: createGetObjectsOfType('host').groupBy('id'),
})
class HostsPatchesTable extends Component {
constructor (props) {
super(props)
@@ -80,11 +93,30 @@ class HostsPatchesTable extends Component {
)
)
_refreshMissingPatches = () => (
Promise.all(
map(this.props.hosts, this._refreshHostMissingPatches)
_subscribeMissingPatches = (hosts = this.props.hosts) => {
const { hostsById } = this.props
const unsubs = map(
hosts,
host =>
hostsById
? subscribeHostMissingPatches(hostsById[host.id][0], patches =>
this.setState({
missingPatches: {
...this.state.missingPatches,
[host.id]: patches.length,
},
})
)
: noop
)
)
if (this.unsubscribeMissingPatches !== undefined) {
this.unsubscribeMissingPatches()
}
this.unsubscribeMissingPatches = () => forEach(unsubs, unsub => unsub())
}
_installAllMissingPatches = () => {
const pools = {}
@@ -92,101 +124,71 @@ class HostsPatchesTable extends Component {
pools[host.$pool] = true
})
return Promise.all(map(
keys(pools),
installAllPatchesOnPool
)).then(this._refreshMissingPatches)
}
_refreshHostMissingPatches = host => (
getHostMissingPatches(host).then(patches => {
this.setState({
missingPatches: {
...this.state.missingPatches,
[host.id]: patches.length
}
})
})
)
_installAllHostPatches = host => (
installAllHostPatches(host).then(() =>
this._refreshHostMissingPatches(host)
)
)
componentWillMount () {
this._refreshMissingPatches()
return Promise.all(map(keys(pools), installAllPatchesOnPool))
}
componentDidMount () {
// Force one Portal refresh.
// Because Portal cannot see the container reference at first rendering.
this.forceUpdate()
this._subscribeMissingPatches()
}
componentWillReceiveProps (nextProps) {
forEach(nextProps.hosts, host => {
const { id } = host
if (nextProps.hosts !== this.props.hosts) {
this._subscribeMissingPatches(nextProps.hosts)
}
}
if (this.state.missingPatches[id] !== undefined) {
return
}
this.setState({
missingPatches: {
...this.state.missingPatches,
[id]: 0
}
})
this._refreshHostMissingPatches(host)
})
componentWillUnmount () {
this.unsubscribeMissingPatches()
}
render () {
const {
buttonsGroupContainer,
container,
displayPools,
pools,
useTabButton,
} = this.props
const hosts = this._getHosts()
const noPatches = isEmpty(hosts)
const { props } = this
const Container = props.container || 'div'
const Button = props.useTabButton ? TabButton : ActionButton
const Container = container || 'div'
const Buttons = (
<Container>
<Button
btnStyle='secondary'
handler={this._refreshMissingPatches}
icon='refresh'
labelId='refreshPatches'
/>
<Button
btnStyle='primary'
disabled={noPatches}
handler={this._installAllMissingPatches}
icon='host-patch-update'
labelId='installPoolPatches'
/>
</Container>
)
const Button = useTabButton ? TabButton : ActionButton_
return (
<div>
{!noPatches
? (
<SortedTable
collection={hosts}
columns={props.displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
userData={{
installAllHostPatches: this._installAllHostPatches,
missingPatches: this.state.missingPatches,
pools: props.pools
}}
{!noPatches ? (
<SortedTable
collection={hosts}
columns={
displayPools
? POOLS_MISSING_PATCHES_COLUMNS
: MISSING_PATCHES_COLUMNS
}
userData={{
installAllHostPatches,
missingPatches: this.state.missingPatches,
pools,
}}
/>
) : (
<p>{_('patchNothing')}</p>
)}
<Portal container={() => buttonsGroupContainer()}>
<Container>
<Button
btnStyle='primary'
disabled={noPatches}
handler={this._installAllMissingPatches}
icon='host-patch-update'
labelId='installPoolPatches'
/>
) : <p>{_('patchNothing')}</p>
}
<Portal container={() => props.buttonsGroupContainer()}>
{Buttons}
</Container>
</Portal>
</div>
)
@@ -199,7 +201,7 @@ class HostsPatchesTable extends Component {
const getPools = createGetObjectsOfType('pool')
return {
pools: getPools
pools: getPools,
}
})
class HostsPatchesTableByPool extends Component {
@@ -217,10 +219,14 @@ export default propTypes({
displayPools: propTypes.bool,
hosts: propTypes.oneOfType([
propTypes.arrayOf(propTypes.object),
propTypes.objectOf(propTypes.object)
propTypes.objectOf(propTypes.object),
]).isRequired,
useTabButton: propTypes.bool
})(props => props.displayPools
? <HostsPatchesTableByPool {...props} />
: <HostsPatchesTable {...props} />
useTabButton: propTypes.bool,
})(
props =>
props.displayPools ? (
<HostsPatchesTableByPool {...props} />
) : (
<HostsPatchesTable {...props} />
)
)

View File

@@ -1,21 +1,24 @@
import classNames from 'classnames'
import isInteger from 'lodash/isInteger'
import React, { PropTypes } from 'react'
import React from 'react'
const Icon = ({ className, icon, size = 1, fixedWidth }) => (
<i className={classNames(
className,
icon ? `xo-icon-${icon}` : 'fa', // Without icon prop, is a placeholder.
isInteger(size) ? `fa-${size}x` : `fa-${size}`,
fixedWidth && 'fa-fw'
)} />
)
Icon.propTypes = {
fixedWidth: PropTypes.bool,
icon: PropTypes.string,
size: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
])
import propTypes from './prop-types-decorator'
const Icon = ({ icon, size = 1, color, fixedWidth, ...props }) => {
props.className = classNames(
props.className,
icon !== undefined ? `xo-icon-${icon}` : 'fa', // Without icon prop, is a placeholder.
isInteger(size) ? `fa-${size}x` : `fa-${size}`,
color,
fixedWidth && 'fa-fw'
)
return <i {...props} />
}
propTypes(Icon)({
color: propTypes.string,
fixedWidth: propTypes.bool,
icon: propTypes.string,
size: propTypes.oneOfType([propTypes.string, propTypes.number]),
})
export default Icon

View File

@@ -1,18 +1,10 @@
import isFunction from 'lodash/isFunction'
import isString from 'lodash/isString'
import moment from 'moment'
import React, {
Component,
PropTypes
} from 'react'
import {
connect
} from 'react-redux'
import {
FormattedMessage,
IntlProvider as IntlProvider_
} from 'react-intl'
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { FormattedMessage, IntlProvider as IntlProvider_ } from 'react-intl'
import messages from './messages'
import locales from './locales'
@@ -44,10 +36,17 @@ const getMessage = (props, messageId, values, render) => {
values = undefined
}
return <FormattedMessage {...props} {...message} values={values}>
{render}
</FormattedMessage>
return (
<FormattedMessage {...props} {...message} values={values}>
{render}
</FormattedMessage>
)
}
getMessage.keyValue = (key, value) =>
getMessage('keyValue', {
key: <strong>{key}</strong>,
value,
})
export { getMessage as default }
@@ -57,27 +56,35 @@ export { messages }
export class IntlProvider extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
lang: PropTypes.string.isRequired
};
lang: PropTypes.string.isRequired,
}
render () {
const { lang, children } = this.props
return <IntlProvider_
locale={lang}
messages={locales[lang]}
>
{children}
</IntlProvider_>
// Adding a key prop is a work-around suggested by react-intl documentation
// to make sure changes to the locale trigger a re-render of the child components
// https://github.com/yahoo/react-intl/wiki/Components#dynamic-language-selection
//
// FIXME: remove the key prop when React context propagation is fixed (https://github.com/facebook/react/issues/2517)
return (
<IntlProvider_ key={lang} locale={lang} messages={locales[lang]}>
{children}
</IntlProvider_>
)
}
}
@connect(({ lang }) => ({ lang }))
export class FormattedDuration extends Component {
render () {
const {
duration,
lang
} = this.props
return <span>{moment.duration(duration).locale(lang).humanize()}</span>
const { duration, lang } = this.props
return (
<span>
{moment
.duration(duration)
.locale(lang)
.humanize()}
</span>
)
}
}

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

@@ -204,7 +204,7 @@ export default {
editUserProfile: undefined,
// Original text: "Fetching data…"
homeFetchingData: 'מקבל נתונים, נא להמתין...',
homeFetchingData: 'מקבל נתונים, נא להמתין',
// Original text: "Welcome on Xen Orchestra!"
homeWelcome: 'ברוכים הבאים',
@@ -228,7 +228,7 @@ export default {
homeNoVms: 'אין מכונות',
// Original text: "Or…"
homeNoVmsOr: 'או...',
homeNoVmsOr: 'או',
// Original text: "Import VM"
homeImportVm: 'ההלעה של מכונה',
@@ -330,7 +330,7 @@ export default {
homeMore: 'עוד',
// Original text: "Migrate to…"
homeMigrateTo: 'העבר ל...',
homeMigrateTo: 'העבר ל',
// Original text: 'Missing patches'
homeMissingPaths: undefined,
@@ -627,7 +627,7 @@ export default {
editBackupReportTitle: undefined,
// Original text: 'Enable immediately after creation'
editBackupReportEnable: undefined,
editBackupScheduleEnabled: undefined,
// Original text: 'Depth'
editBackupDepthTitle: undefined,
@@ -1172,7 +1172,7 @@ export default {
// Original text: 'Reboot'
rebootHostLabel: undefined,
// Original text: 'Reboot for applying updates'
// Original text: 'Reboot to apply updates'
rebootUpdateHostLabel: undefined,
// Original text: 'Emergency mode'
@@ -1605,10 +1605,10 @@ export default {
vdiRemove: undefined,
// Original text: 'Boot flag'
vdbBootableStatus: undefined,
vbdBootableStatus: undefined,
// Original text: 'Status'
vdbStatus: undefined,
vbdStatus: undefined,
// Original text: 'Connected'
vbdStatusConnected: undefined,
@@ -1626,19 +1626,19 @@ export default {
vbdDisconnect: undefined,
// Original text: 'Bootable'
vdbBootable: undefined,
vbdBootable: undefined,
// Original text: 'Readonly'
vdbReadonly: undefined,
vbdReadonly: undefined,
// Original text: 'Create'
vdbCreate: undefined,
vbdCreate: undefined,
// Original text: 'Disk name'
vdbNamePlaceHolder: undefined,
vbdNamePlaceHolder: undefined,
// Original text: 'Size'
vdbSizePlaceHolder: undefined,
vbdSizePlaceHolder: undefined,
// Original text: 'Save'
saveBootOption: undefined,
@@ -3132,5 +3132,5 @@ export default {
settingsAclsButtonTooltipSR: undefined,
// Original text: 'Network'
settingsAclsButtonTooltipnetwork: undefined
settingsAclsButtonTooltipnetwork: undefined,
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -204,7 +204,7 @@ export default {
editUserProfile: undefined,
// Original text: "Fetching data…"
homeFetchingData: 'Obtendo dados...',
homeFetchingData: 'Obtendo dados',
// Original text: "Welcome on Xen Orchestra!"
homeWelcome: 'Bem-vindo ao Xen Orchestra',
@@ -228,7 +228,7 @@ export default {
homeNoVms: 'Não foram encontradas VMs!',
// Original text: "Or…"
homeNoVmsOr: 'Ou...',
homeNoVmsOr: 'Ou',
// Original text: "Import VM"
homeImportVm: 'Importar VM',
@@ -330,7 +330,7 @@ export default {
homeMore: 'Mais',
// Original text: "Migrate to…"
homeMigrateTo: 'Migrar para...',
homeMigrateTo: 'Migrar para',
// Original text: 'Missing patches'
homeMissingPaths: undefined,
@@ -360,28 +360,28 @@ export default {
selectSubjects: 'Escolha um usuário(s) e/ou grupo(s)',
// Original text: "Select Object(s)…"
selectObjects: 'Selecionar Objeto(s)...',
selectObjects: 'Selecionar Objeto(s)',
// Original text: "Choose a role"
selectRole: 'Escolha uma função',
// Original text: "Select Host(s)…"
selectHosts: 'Selecionar Host(s)...',
selectHosts: 'Selecionar Host(s)',
// Original text: "Select object(s)…"
selectHostsVms: 'Selecionar Objeto(s)...',
selectHostsVms: 'Selecionar Objeto(s)',
// Original text: "Select Network(s)…"
selectNetworks: 'Selecionar Rede(s)...',
selectNetworks: 'Selecionar Rede(s)',
// Original text: "Select PIF(s)…"
selectPifs: 'Selecionar PIF(s)...',
selectPifs: 'Selecionar PIF(s)',
// Original text: "Select Pool(s)…"
selectPools: 'Selecionar Pool(s)...',
selectPools: 'Selecionar Pool(s)',
// Original text: "Select Remote(s)…"
selectRemotes: 'Selecionar Remote(s)...',
selectRemotes: 'Selecionar Remote(s)',
// Original text: 'Select resource set(s)…'
selectResourceSets: undefined,
@@ -402,19 +402,19 @@ export default {
selectSshKey: undefined,
// Original text: "Select SR(s)…"
selectSrs: 'Selecionar SR(s)...',
selectSrs: 'Selecionar SR(s)',
// Original text: "Select VM(s)…"
selectVms: 'Selecionar VM(s)...',
selectVms: 'Selecionar VM(s)',
// Original text: "Select VM template(s)…"
selectVmTemplates: 'Selecionar VM(s) modelo(s)...',
selectVmTemplates: 'Selecionar VM(s) modelo(s)',
// Original text: "Select tag(s)…"
selectTags: 'Selecionar etiqueta(s)...',
selectTags: 'Selecionar etiqueta(s)',
// Original text: "Select disk(s)…"
selectVdis: 'Selecionar disco(s)...',
selectVdis: 'Selecionar disco(s)',
// Original text: 'Select timezone…'
selectTimezone: undefined,
@@ -543,7 +543,8 @@ export default {
runJob: 'Iniciar tarefa',
// Original text: "One shot running started. See overview for logs."
runJobVerbose: 'O backup manual foi executado. Clique em Visão Geral para ver os Logs',
runJobVerbose:
'O backup manual foi executado. Clique em Visão Geral para ver os Logs',
// Original text: "Started"
jobStarted: 'Iniciado',
@@ -558,16 +559,19 @@ export default {
deleteBackupSchedule: 'Remover tarefa de backup',
// Original text: "Are you sure you want to delete this backup job?"
deleteBackupScheduleQuestion: 'Você tem certeza que você quer deletar esta tarefa de backup?',
deleteBackupScheduleQuestion:
'Você tem certeza que você quer deletar esta tarefa de backup?',
// Original text: "Enable immediately after creation"
scheduleEnableAfterCreation: 'Ativar imediatamente após criação',
// Original text: "You are editing Schedule {name} ({id}). Saving will override previous schedule state."
scheduleEditMessage: 'Você esta editando o Agendamento {name} ({id}). Este procedimento irá substituir o agendamento atual.',
scheduleEditMessage:
'Você esta editando o Agendamento {name} ({id}). Este procedimento irá substituir o agendamento atual.',
// Original text: "You are editing job {name} ({id}). Saving will override previous job state."
jobEditMessage: 'Você esta editando a Tarefa {name} ({id}). Este procedimento irá substituir a tarefa atual.',
jobEditMessage:
'Você esta editando a Tarefa {name} ({id}). Este procedimento irá substituir a tarefa atual.',
// Original text: "No scheduled jobs."
noScheduledJobs: 'Sem agendamentos',
@@ -627,7 +631,7 @@ export default {
editBackupReportTitle: undefined,
// Original text: 'Enable immediately after creation'
editBackupReportEnable: undefined,
editBackupScheduleEnabled: undefined,
// Original text: 'Depth'
editBackupDepthTitle: undefined,
@@ -942,7 +946,8 @@ export default {
purgePluginConfiguration: 'Configuração de limpeza do plugin',
// Original text: "Are you sure you want to purge this configuration ?"
purgePluginConfigurationQuestion: 'Você tem certeza que deseja executar esta configuração?',
purgePluginConfigurationQuestion:
'Você tem certeza que deseja executar esta configuração?',
// Original text: "Edit"
editPluginConfiguration: 'Editar',
@@ -954,7 +959,8 @@ export default {
pluginConfigurationSuccess: 'Configuração do Plugin',
// Original text: "Plugin configuration successfully saved!"
pluginConfigurationChanges: 'Configuração do plugin foi efetuada com sucesso!',
pluginConfigurationChanges:
'Configuração do plugin foi efetuada com sucesso!',
// Original text: 'Predefined configuration'
pluginConfigurationPresetTitle: undefined,
@@ -1172,7 +1178,7 @@ export default {
// Original text: "Reboot"
rebootHostLabel: 'Reinicializar',
// Original text: 'Reboot for applying updates'
// Original text: 'Reboot to apply updates'
rebootUpdateHostLabel: undefined,
// Original text: "Emergency mode"
@@ -1512,7 +1518,8 @@ export default {
tipLabel: 'Dica',
// Original text: "non-US keyboard could have issues with console: switch your own layout to US."
tipConsoleLabel: 'Teclados fora do padrão US-Keyboard podem apresentar problemas com o console: Altere seu teclado e verifique!',
tipConsoleLabel:
'Teclados fora do padrão US-Keyboard podem apresentar problemas com o console: Altere seu teclado e verifique!',
// Original text: 'Hide infos'
hideHeaderTooltip: undefined,
@@ -1605,10 +1612,10 @@ export default {
vdiRemove: undefined,
// Original text: "Boot flag"
vdbBootableStatus: 'Indicador de inicialização',
vbdBootableStatus: 'Indicador de inicialização',
// Original text: "Status"
vdbStatus: 'Status',
vbdStatus: 'Status',
// Original text: "Connected"
vbdStatusConnected: 'Conectado',
@@ -1626,19 +1633,19 @@ export default {
vbdDisconnect: undefined,
// Original text: 'Bootable'
vdbBootable: undefined,
vbdBootable: undefined,
// Original text: 'Readonly'
vdbReadonly: undefined,
vbdReadonly: undefined,
// Original text: 'Create'
vdbCreate: undefined,
vbdCreate: undefined,
// Original text: 'Disk name'
vdbNamePlaceHolder: undefined,
vbdNamePlaceHolder: undefined,
// Original text: 'Size'
vdbSizePlaceHolder: undefined,
vbdSizePlaceHolder: undefined,
// Original text: 'Save'
saveBootOption: undefined,
@@ -1842,7 +1849,8 @@ export default {
vmHomeNamePlaceholder: 'Faça um longo clique para adicionar um nome',
// Original text: "Long click to add a description"
vmHomeDescriptionPlaceholder: 'Faça um longo clique para adicionar uma descrição',
vmHomeDescriptionPlaceholder:
'Faça um longo clique para adicionar uma descrição',
// Original text: "Click to add a name"
vmViewNamePlaceholder: 'Clique para adicionar um nome',
@@ -1968,7 +1976,7 @@ export default {
statsDashboardSelectObjects: 'Selecionar',
// Original text: "Loading…"
metricsLoading: 'Carregando...',
metricsLoading: 'Carregando',
// Original text: "Coming soon!"
comingSoon: 'Em breve!',
@@ -2235,7 +2243,8 @@ export default {
noHostsAvailable: 'Sem hosts disponiveis',
// Original text: "VMs created from this resource set shall run on the following hosts."
availableHostsDescription: 'VMs criadas a partir desse conjunto de recursos deve ser executado nos hosts indicados.',
availableHostsDescription:
'VMs criadas a partir desse conjunto de recursos deve ser executado nos hosts indicados.',
// Original text: "Maximum CPUs"
maxCpus: 'Limite de CPUs',
@@ -2268,7 +2277,8 @@ export default {
resourceSetNew: undefined,
// Original text: "Try dropping some VMs files here, or click to select VMs to upload. Accept only .xva/.ova files."
importVmsList: 'Tente soltar alguns backups aqui, ou clique para selecionar os backups para que seja feito o upload. Apenas arquivos .xva são aceitos.',
importVmsList:
'Tente soltar alguns backups aqui, ou clique para selecionar os backups para que seja feito o upload. Apenas arquivos .xva são aceitos.',
// Original text: "No selected VMs."
noSelectedVms: 'Nenhuma VM selecionada',
@@ -2292,10 +2302,10 @@ export default {
vmImportFailed: 'Falha na importação',
// Original text: "Import starting…"
startVmImport: 'Iniciando importação...',
startVmImport: 'Iniciando importação',
// Original text: "Export starting…"
startVmExport: 'Iniciando exportação...',
startVmExport: 'Iniciando exportação',
// Original text: 'N CPUs'
nCpus: undefined,
@@ -2406,7 +2416,8 @@ export default {
stopHostModalTitle: 'Desligar host',
// Original text: "This will shutdown your host. Do you want to continue? If it's the pool master, your connection to the pool will be lost"
stopHostModalMessage: 'O host será desligado. Você tem certeza que deseja continuar?',
stopHostModalMessage:
'O host será desligado. Você tem certeza que deseja continuar?',
// Original text: 'Add host'
addHostModalTitle: undefined,
@@ -2418,7 +2429,8 @@ export default {
restartHostModalTitle: 'Reiniciar host',
// Original text: "This will restart your host. Do you want to continue?"
restartHostModalMessage: 'O host será reiniciado. Você tem certeza que deseja continuar?',
restartHostModalMessage:
'O host será reiniciado. Você tem certeza que deseja continuar?',
// Original text: 'Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}'
restartHostsAgentsModalTitle: undefined,
@@ -2436,7 +2448,8 @@ export default {
startVmsModalTitle: 'Iniciar VM{vms, plural, one {} other {s}}',
// Original text: "Are you sure you want to start {vms} VM{vms, plural, one {} other {s}}?"
startVmsModalMessage: 'Você tem certeza que deseja iniciar {vms} VM{vms, plural, one {} other {s}}?',
startVmsModalMessage:
'Você tem certeza que deseja iniciar {vms} VM{vms, plural, one {} other {s}}?',
// Original text: 'Stop Host{nHosts, plural, one {} other {s}}'
stopHostsModalTitle: undefined,
@@ -2448,7 +2461,8 @@ export default {
stopVmsModalTitle: 'Parar VM{vms, plural, one {} other {s}}',
// Original text: "Are you sure you want to stop {vms} VM{vms, plural, one {} other {s}}?"
stopVmsModalMessage: 'Você tem certeza que deseja parar {vms} VM{vms, plural, one {} other {s}}?',
stopVmsModalMessage:
'Você tem certeza que deseja parar {vms} VM{vms, plural, one {} other {s}}?',
// Original text: "Restart VM"
restartVmModalTitle: 'Reiniciar VM',
@@ -2466,25 +2480,29 @@ export default {
restartVmsModalTitle: 'Reiniciar VM{vms, plural, one {} other {s}}',
// Original text: "Are you sure you want to restart {vms} VM{vms, plural, one {} other {s}}?"
restartVmsModalMessage: 'Você tem certeza que deseja reiniciar {vms} VM{vms, plural, one {} other {s}}?',
restartVmsModalMessage:
'Você tem certeza que deseja reiniciar {vms} VM{vms, plural, one {} other {s}}?',
// Original text: "Snapshot VM{vms, plural, one {} other {s}}"
snapshotVmsModalTitle: 'Snapshot VM{vms, plural, one {} other {s}}',
// Original text: "Are you sure you want to snapshot {vms} VM{vms, plural, one {} other {s}}?"
snapshotVmsModalMessage: 'Você tem certeza que deseja executar snapshop para {vms} VM{vms, plural, one {} other {s}}?',
snapshotVmsModalMessage:
'Você tem certeza que deseja executar snapshop para {vms} VM{vms, plural, one {} other {s}}?',
// Original text: "Delete VM{vms, plural, one {} other {s}}"
deleteVmsModalTitle: 'Deletar VM{vms, plural, one {} other {s}}',
// Original text: "Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED"
deleteVmsModalMessage: 'Você tem certeza que deseja deletar {vms} VM{vms, plural, one {} other {s}}? Todos os discos de VM serão removidos',
deleteVmsModalMessage:
'Você tem certeza que deseja deletar {vms} VM{vms, plural, one {} other {s}}? Todos os discos de VM serão removidos',
// Original text: "Delete VM"
deleteVmModalTitle: 'Deletar VM',
// Original text: "Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED"
deleteVmModalMessage: 'Você tem certeza que deseja deletar esta VM? Todos os discos de VM serão removidos',
deleteVmModalMessage:
'Você tem certeza que deseja deletar esta VM? Todos os discos de VM serão removidos',
// Original text: "Migrate VM"
migrateVmModalTitle: 'Migrar VM',
@@ -2559,16 +2577,18 @@ export default {
importBackupModalStart: 'Iniciar VM após restauração',
// Original text: "Select your backup…"
importBackupModalSelectBackup: 'Selecionar backup...',
importBackupModalSelectBackup: 'Selecionar backup',
// Original text: "Are you sure you want to remove all orphaned snapshot VDIs?"
removeAllOrphanedModalWarning: 'Você tem certeza que deseja remover todos as VDIs orfãs?',
removeAllOrphanedModalWarning:
'Você tem certeza que deseja remover todos as VDIs orfãs?',
// Original text: "Remove all logs"
removeAllLogsModalTitle: 'Remover todos os logs',
// Original text: "Are you sure you want to remove all logs?"
removeAllLogsModalWarning: 'Você tem certeza que deseja remover todos os logs?',
removeAllLogsModalWarning:
'Você tem certeza que deseja remover todos os logs?',
// Original text: "This operation is definitive."
definitiveMessageModal: 'Esta operação é definitiva.',
@@ -2577,25 +2597,29 @@ export default {
existingSrModalTitle: 'Uso anterior SR',
// Original text: "This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation."
existingSrModalText: 'Este caminho foi previamente utilizado como um dispositivo de armazenamento por um host XenServer. Todos os dados serão perdidos se você optar por continuar a criação do SR.',
existingSrModalText:
'Este caminho foi previamente utilizado como um dispositivo de armazenamento por um host XenServer. Todos os dados serão perdidos se você optar por continuar a criação do SR.',
// Original text: "Previous LUN Usage"
existingLunModalTitle: 'Uso anterior LUN',
// Original text: "This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation."
existingLunModalText: 'Este LUN foi previamente utilizado como um dispositivo de armazenamento por um host XenServer. Todos os dados serão perdidos se você optar por continuar a criação do SR.',
existingLunModalText:
'Este LUN foi previamente utilizado como um dispositivo de armazenamento por um host XenServer. Todos os dados serão perdidos se você optar por continuar a criação do SR.',
// Original text: "Replace current registration?"
alreadyRegisteredModal: 'Deseja substituir o registro atual?',
// Original text: "Your XO appliance is already registered to {email}, do you want to forget and replace this registration ?"
alreadyRegisteredModalText: 'O seu XO appliance já foi registrado com o e-mail {email}, você tem certeza que gostaria de substituir este registro?',
alreadyRegisteredModalText:
'O seu XO appliance já foi registrado com o e-mail {email}, você tem certeza que gostaria de substituir este registro?',
// Original text: "Ready for trial?"
trialReadyModal: 'Pronto para iniciar o teste (trial)?',
// Original text: "During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!"
trialReadyModalText: 'Durante o período experimental, XOA precisa de uma conexão internet. Esta limitação não se aplica em nossos planos pagos!',
trialReadyModalText:
'Durante o período experimental, XOA precisa de uma conexão internet. Esta limitação não se aplica em nossos planos pagos!',
// Original text: "Host"
serverHost: 'Host',
@@ -2844,13 +2868,16 @@ export default {
upgrade: 'Atualização (Upgrade)',
// Original text: "No updater available for Community Edition"
noUpdaterCommunity: 'Nenhuma atualização disponível para a versão Community Edition',
noUpdaterCommunity:
'Nenhuma atualização disponível para a versão Community Edition',
// Original text: "Please consider subscribe and try it with all features for free during 15 days on"
noUpdaterSubscribe: 'Oi, inscreva-se e venha testar todos nossos recursos e serviços gratuitamente por 15 dias!',
noUpdaterSubscribe:
'Oi, inscreva-se e venha testar todos nossos recursos e serviços gratuitamente por 15 dias!',
// Original text: "Manual update could break your current installation due to dependencies issues, do it with caution"
noUpdaterWarning: 'Atualização feita de forma manual pode corromper sua instalação atual devido a problema de dependências, tenha cuidado!',
noUpdaterWarning:
'Atualização feita de forma manual pode corromper sua instalação atual devido a problema de dependências, tenha cuidado!',
// Original text: "Current version:"
currentVersion: 'Versão atual:',
@@ -2862,19 +2889,23 @@ export default {
editRegistration: undefined,
// Original text: "Please, take time to register in order to enjoy your trial."
trialRegistration: 'Por favor, tome seu tempo para se registrar a fim de desfrutar do seu período de teste (trial)',
trialRegistration:
'Por favor, tome seu tempo para se registrar a fim de desfrutar do seu período de teste (trial)',
// Original text: "Start trial"
trialStartButton: 'Iniciar teste (trial)',
// Original text: "You can use a trial version until {date, date, medium}. Upgrade your appliance to get it."
trialAvailableUntil: 'Sua versao de teste é válida até {date, date, medium}. Após esta data escolha um de nossos planos e continue a desfrutar de nosso software e serviços!',
trialAvailableUntil:
'Sua versao de teste é válida até {date, date, medium}. Após esta data escolha um de nossos planos e continue a desfrutar de nosso software e serviços!',
// Original text: "Your trial has been ended. Contact us or downgrade to Free version"
trialConsumed: 'Seu período de teste chegou ao fim. Entre em contato conosco ou faça o downgrade para a versão grátis',
trialConsumed:
'Seu período de teste chegou ao fim. Entre em contato conosco ou faça o downgrade para a versão grátis',
// Original text: "Your xoa-updater service appears to be down. Your XOA cannot run fully without reaching this service."
trialLocked: 'Seu serviço de atualização XOA parece não funcionar. Seu XOA não pode funcionar corretamente sem este serviço.',
trialLocked:
'Seu serviço de atualização XOA parece não funcionar. Seu XOA não pode funcionar corretamente sem este serviço.',
// Original text: 'No update information available'
noUpdateInfo: undefined,
@@ -2904,13 +2935,16 @@ export default {
disclaimerTitle: 'Xen Orchestra versão Open-Source',
// Original text: "You are using XO from the sources! That's great for a personal/non-profit usage."
disclaimerText1: 'Você está usando XO Open-Source! Isso é ótimo para um uso pessoal / sem fins lucrativos.',
disclaimerText1:
'Você está usando XO Open-Source! Isso é ótimo para um uso pessoal / sem fins lucrativos.',
// Original text: "If you are a company, it's better to use it with our appliance + pro support included:"
disclaimerText2: 'Se você é uma empresa, é melhor usá-lo com o nosso sistema appliance + suporte pro inclusos:',
disclaimerText2:
'Se você é uma empresa, é melhor usá-lo com o nosso sistema appliance + suporte pro inclusos:',
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3: 'Esta versão não está vinculada a qualquer tipo de suporte nem atualizações. Use-a com cuidado em se tratando de tarefas críticas.',
disclaimerText3:
'Esta versão não está vinculada a qualquer tipo de suporte nem atualizações. Use-a com cuidado em se tratando de tarefas críticas.',
// Original text: "Connect PIF"
connectPif: 'Conectar PIF',
@@ -3132,5 +3166,5 @@ export default {
settingsAclsButtonTooltipSR: undefined,
// Original text: 'Network'
settingsAclsButtonTooltipnetwork: undefined
settingsAclsButtonTooltipnetwork: undefined,
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -13,22 +13,24 @@ export const isIpV6 = isIp.v6
const ipv4 = /^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(?:\.(?!$)|$)){4}$/
function ip2hex (ip) {
let parts = ip.split('.').map(str => parseInt(str, 10))
const parts = ip.split('.').map(str => parseInt(str, 10))
let n = 0
n += parts[3]
n += parts[2] * 256 // 2^8
n += parts[1] * 65536 // 2^16
n += parts[2] * 256 // 2^8
n += parts[1] * 65536 // 2^16
n += parts[0] * 16777216 // 2^24
return n
}
function assertIpv4 (str, msg) {
if (!ipv4.test(str)) { throw new Error(msg) }
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')
@@ -36,13 +38,14 @@ function *range (ip1, ip2) {
let hex2 = ip2hex(ip2)
if (hex > hex2) {
let tmp = hex
const tmp = hex
hex = hex2
hex2 = tmp
}
for (let i = hex; i <= hex2; i++) {
yield `${(i >> 24) & 0xff}.${(i >> 16) & 0xff}.${(i >> 8) & 0xff}.${i & 0xff}`
yield `${(i >> 24) & 0xff}.${(i >> 16) & 0xff}.${(i >> 8) & 0xff}.${i &
0xff}`
}
}
@@ -50,7 +53,10 @@ function *range (ip1, ip2) {
export const getNextIpV4 = ip => {
const splitIp = ip.split('.')
if (splitIp.length !== 4 || some(splitIp, value => value < 0 || value > 255)) {
if (
splitIp.length !== 4 ||
some(splitIp, value => value < 0 || value > 255)
) {
return
}
let index
@@ -85,10 +91,13 @@ export const formatIps = ips => {
if (splitIp2.length !== 4) {
return -1
}
return splitIp1[3] - splitIp2[3] +
return (
splitIp1[3] -
splitIp2[3] +
(splitIp1[2] - splitIp2[2]) * 256 +
(splitIp1[1] - splitIp2[1]) * 256 * 256 +
(splitIp1[0] - splitIp2[0]) * 256 * 256 * 256
)
})
const range = { first: '', last: '' }
const formattedIps = []
@@ -96,7 +105,8 @@ export const formatIps = ips => {
forEach(sortedIps, ip => {
if (ip !== getNextIpV4(range.last)) {
if (range.first) {
formattedIps[index] = range.first === range.last ? range.first : { ...range }
formattedIps[index] =
range.first === range.last ? range.first : { ...range }
index++
}
range.first = range.last = ip

View File

@@ -1,50 +1,58 @@
import React from 'react'
import _ from 'intl'
import ActionButton from './action-button'
import Component from './base-component'
import propTypes from './prop-types'
import Icon from 'icon'
import propTypes from './prop-types-decorator'
import Tooltip from 'tooltip'
import { alert } from 'modal'
import { connectStore } from './utils'
import { SelectVdi } from './select-objects'
import {
createGetObjectsOfType,
createFinder,
createGetObject,
createSelector
createSelector,
} from './selectors'
import {
ejectCd,
insertCd
} from './xo'
import { ejectCd, insertCd } from './xo'
@propTypes({
vm: propTypes.object.isRequired
vm: propTypes.object.isRequired,
})
@connectStore(() => {
const getCdDrive = createFinder(
createGetObjectsOfType('VBD').pick(
(_, { vm }) => vm.$VBDs
),
[ vbd => vbd.is_cd_drive ]
createGetObjectsOfType('VBD').pick((_, { vm }) => vm.$VBDs),
[vbd => vbd.is_cd_drive]
)
const getMountedIso = createGetObject(
(state, props) => {
const cdDrive = getCdDrive(state, props)
if (cdDrive) {
return cdDrive.VDI
}
const getMountedIso = createGetObject((state, props) => {
const cdDrive = getCdDrive(state, props)
if (cdDrive) {
return cdDrive.VDI
}
)
})
return {
cdDrive: getCdDrive,
mountedIso: getMountedIso
mountedIso: getMountedIso,
}
})
export default class IsoDevice extends Component {
_getPredicate = createSelector(
() => this.props.vm.$pool,
poolId => sr => sr.$pool === poolId && sr.SR_type === 'iso'
() => this.props.vm.$container,
(vmPool, vmContainer) => sr => {
const vmRunning = vmContainer !== vmPool
const sameHost = vmContainer === sr.$container
const samePool = vmPool === sr.$pool
return (
samePool &&
(vmRunning ? sr.shared || sameHost : true) &&
(sr.SR_type === 'iso' || (sr.SR_type === 'udev' && sr.size))
)
}
)
_handleInsert = iso => {
@@ -59,25 +67,36 @@ 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'>
<SelectVdi
srPredicate={this._getPredicate()}
onChange={this._handleInsert}
ref='selectIso'
value={mountedIso}
/>
<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,45 +1,15 @@
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 propTypes from '../prop-types'
import { propsEqual } from '../utils'
import Button from '../button'
import Component from '../base-component'
import propTypes from '../prop-types-decorator'
import { EMPTY_ARRAY } from '../utils'
import GenericInput from './generic-input'
import {
descriptionRender,
forceDisplayOptionalAttr
} from './helpers'
// ===================================================================
class ArrayItem extends Component {
get value () {
return this.refs.input.value
}
set value (value) {
this.refs.input.value = value
}
render () {
const { children } = this.props
return (
<li className='list-group-item clearfix'>
{cloneElement(children, {
ref: 'input'
})}
<button disabled={children.props.disabled} className='btn btn-danger pull-right' type='button' onClick={this.props.onDelete}>
{_('remove')}
</button>
</li>
)
}
}
// ===================================================================
import { descriptionRender, forceDisplayOptionalAttr } from './helpers'
@propTypes({
depth: propTypes.number,
@@ -48,135 +18,108 @@ class ArrayItem extends Component {
required: propTypes.bool,
schema: propTypes.object.isRequired,
uiSchema: propTypes.object,
defaultValue: propTypes.array
})
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>
<div style={{ paddingLeft: `${depth}em` }}>
<legend>{label}</legend>
{descriptionRender(schema.description)}
<hr />
{!props.required &&
{!required && (
<div className='checkbox'>
<label>
<input
checked={use}
disabled={disabled}
onChange={this._handleOptionalChange}
onChange={this.linkState('use')}
type='checkbox'
/> {_('fillOptionalInformations')}
/>{' '}
{_('fillOptionalInformations')}
</label>
</div>
}
{use &&
<div className={'card-block'}>
<ul style={{'paddingLeft': 0}} >
{map(this.state.children, (child, index) =>
cloneElement(child, { ref: index })
)}
)}
{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 disabled={disabled} className='btn btn-primary pull-right mt-1 mr-1' type='button' onClick={this._handleAdd}>
<Button
btnStyle='primary'
className='pull-right mt-1 mr-1'
disabled={disabled}
onClick={this._onAddItem}
>
{_('add')}
</button>
</Button>
</div>
}
)}
</div>
)
}

View File

@@ -1,33 +1,22 @@
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'
/>
<Toggle disabled={disabled} onChange={onChange} value={value} />
</div>
</PrimitiveInputWrapper>
)

View File

@@ -1,34 +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,
required
} = props
disabled,
schema: { enum: enumValues, enumNames = enumValues },
required,
} = 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 propTypes from '../prop-types'
import getEventValue from '../get-event-value'
import propTypes from '../prop-types-decorator'
import uncontrollableInput from 'uncontrollable-input'
import { EMPTY_OBJECT } from '../utils'
import ArrayInput from './array-input'
@@ -21,7 +23,7 @@ const InputByType = {
integer: IntegerInput,
number: NumberInput,
object: ObjectInput,
string: StringInput
string: StringInput,
}
// ===================================================================
@@ -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
})
@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

@@ -38,12 +38,21 @@ export const getXoType = schema => {
// ===================================================================
export const descriptionRender = description =>
<span className='text-muted' dangerouslySetInnerHTML={{__html: marked(description || '')}} />
export const descriptionRender = description => (
<span
className='text-muted'
dangerouslySetInnerHTML={{ __html: marked(description || '') }}
/>
)
// ===================================================================
export const PrimitiveInputWrapper = ({ label, required = false, schema, children }) => (
export const PrimitiveInputWrapper = ({
label,
required = false,
schema,
children,
}) => (
<Row>
<Col mediumSize={6}>
<div className='input-group'>
@@ -54,27 +63,25 @@ export const PrimitiveInputWrapper = ({ label, required = false, schema, childre
{children}
</div>
</Col>
<Col mediumSize={6}>
{descriptionRender(schema.description)}
</Col>
<Col mediumSize={6}>{descriptionRender(schema.description)}</Col>
</Row>
)
// ===================================================================
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,38 +1,42 @@
import React from 'react'
import AbstractInput from './abstract-input'
import uncontrollableInput from 'uncontrollable-input'
import Combobox from '../combobox'
import Component from '../base-component'
import getEventValue from '../get-event-value'
import { PrimitiveInputWrapper } from './helpers'
// ===================================================================
export default class IntegerInput extends AbstractInput {
get value () {
const { value } = this.refs.input
return !value ? undefined : +value
}
set value (value) {
// Getter/Setter are always inherited together.
// `get value` is defined in the subclass, so `set value`
// must be defined too.
super.value = value
@uncontrollableInput()
export default class IntegerInput extends Component {
_onChange = event => {
const value = getEventValue(event)
this.props.onChange(value ? +value : undefined)
}
render () {
const { props } = this
const { schema } = props
const { required, schema } = this.props
const {
disabled,
onChange, // eslint-disable-line no-unused-vars
placeholder = schema.default,
value,
...props
} = this.props
return (
<PrimitiveInputWrapper {...props}>
<Combobox
defaultValue={props.defaultValue}
disabled={props.disabled}
onChange={props.onChange}
value={value === undefined ? '' : String(value)}
disabled={disabled}
max={schema.max}
min={schema.min}
onChange={this._onChange}
options={schema.defaults}
placeholder={props.placeholder || schema.default}
ref='input'
required={props.required}
placeholder={placeholder}
required={required}
step={1}
type='number'
/>

View File

@@ -1,38 +1,42 @@
import React from 'react'
import AbstractInput from './abstract-input'
import uncontrollableInput from 'uncontrollable-input'
import Combobox from '../combobox'
import Component from '../base-component'
import getEventValue from '../get-event-value'
import { PrimitiveInputWrapper } from './helpers'
// ===================================================================
export default class NumberInput extends AbstractInput {
get value () {
const { value } = this.refs.input
return !value ? undefined : +value
}
set value (value) {
// Getter/Setter are always inherited together.
// `get value` is defined in the subclass, so `set value`
// must be defined too.
super.value = value
@uncontrollableInput()
export default class NumberInput extends Component {
_onChange = event => {
const value = getEventValue(event)
this.props.onChange(value ? +value : undefined)
}
render () {
const { props } = this
const { schema } = props
const { required, schema } = this.props
const {
disabled,
onChange, // eslint-disable-line no-unused-vars
placeholder = schema.default,
value,
...props
} = this.props
return (
<PrimitiveInputWrapper {...props}>
<Combobox
defaultValue={props.defaultValue}
disabled={props.disabled}
onChange={props.onChange}
value={value === undefined ? '' : String(value)}
disabled={disabled}
max={schema.max}
min={schema.min}
onChange={this._onChange}
options={schema.defaults}
placeholder={props.placeholder || schema.default}
ref='input'
required={props.required}
placeholder={placeholder}
required={required}
step='any'
type='number'
/>

View File

@@ -1,44 +1,15 @@
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 propTypes from '../prop-types'
import { propsEqual } from '../utils'
import _ from '../intl'
import Component from '../base-component'
import propTypes from '../prop-types-decorator'
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>
)
}
}
// ===================================================================
import { descriptionRender, forceDisplayOptionalAttr } from './helpers'
@propTypes({
depth: propTypes.number,
@@ -47,117 +18,80 @@ class ObjectItem extends Component {
required: propTypes.bool,
schema: propTypes.object.isRequired,
uiSchema: propTypes.object,
defaultValue: propTypes.object
})
@uncontrollableInput()
export default class ObjectInput extends Component {
constructor (props) {
super(props)
this.state = {
use: Boolean(props.required) || forceDisplayOptionalAttr(props),
children: this._makeChildren(props)
}
state = {
use: this.props.required || forceDisplayOptionalAttr(this.props),
}
get value () {
if (!this.state.use) {
return
}
const obj = {}
forEach(this.refs, (instance, key) => {
obj[key] = instance.value
})
return obj
}
set value (value = {}) {
forEach(this.refs, (instance, id) => {
instance.value = value[id]
_onChildChange = (value, key) => {
this.props.onChange({
...this.props.value,
[key]: value,
})
}
_handleOptionalChange = event => {
const { checked } = event.target
this.setState({
use: checked
})
}
_makeChildren (props) {
const {
depth = 0,
schema,
uiSchema = {},
defaultValue = {}
} = props
const obj = {}
const { properties } = uiSchema
forEach(schema.properties, (childSchema, key) => {
obj[key] = (
<ObjectItem key={key}>
<GenericInput
depth={depth + 2}
disabled={props.disabled}
label={childSchema.title || key}
required={includes(schema.required, key)}
schema={childSchema}
uiSchema={properties && properties[key]}
defaultValue={defaultValue[key]}
/>
</ObjectItem>
)
})
return obj
}
componentWillReceiveProps (props) {
if (
!propsEqual(
this.props,
props,
[ 'depth', 'disabled', 'label', 'required', 'schema', 'uiSchema' ]
)
) {
this.setState({
children: this._makeChildren(props)
})
}
}
_getRequiredProps = createSelector(
() => this.props.schema.required,
required => (required ? keyBy(required) : EMPTY_OBJECT)
)
render () {
const { props, state } = this
const { use } = state
const depth = props.depth || 0
const {
props: {
depth = 0,
disabled,
label,
required,
schema,
uiSchema,
value = EMPTY_OBJECT,
},
state: { use },
} = this
const childDepth = depth + 2
const properties = (uiSchema != null && uiSchema.properties) || EMPTY_OBJECT
const requiredProps = this._getRequiredProps()
return (
<div style={{'paddingLeft': `${depth}em`}}>
<legend>{props.label}</legend>
{descriptionRender(props.schema.description)}
<div style={{ paddingLeft: `${depth}em` }}>
<legend>{label}</legend>
{descriptionRender(schema.description)}
<hr />
{!props.required &&
{!required && (
<div className='checkbox'>
<label>
<input
checked={use}
disabled={props.disabled}
onChange={this._handleOptionalChange}
disabled={disabled}
onChange={this.linkState('use')}
type='checkbox'
/> {_('fillOptionalInformations')}
/>{' '}
{_('fillOptionalInformations')}
</label>
</div>
}
{use &&
)}
{use && (
<div className='card-block'>
{map(state.children, (child, index) =>
cloneElement(child, { ref: index })
)}
{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,31 +1,48 @@
import React from 'react'
import uncontrollableInput from 'uncontrollable-input'
import AbstractInput from './abstract-input'
import Combobox from '../combobox'
import propTypes from '../prop-types'
import Component from '../base-component'
import getEventValue from '../get-event-value'
import propTypes from '../prop-types-decorator'
import { PrimitiveInputWrapper } from './helpers'
// ===================================================================
@propTypes({
password: propTypes.bool
password: propTypes.bool,
})
export default class StringInput extends AbstractInput {
@uncontrollableInput()
export default class StringInput extends Component {
// the value of this input is undefined not '' when empty to make
// it homogenous with when the user has never touched this input
_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,
password,
placeholder = schema.default,
value,
...props
} = this.props
delete props.onChange
return (
<PrimitiveInputWrapper {...props}>
<Combobox
defaultValue={props.defaultValue}
disabled={props.disabled}
onChange={props.onChange}
value={value !== undefined ? value : ''}
disabled={disabled}
onChange={this._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

@@ -3,7 +3,7 @@ import React from 'react'
import { routerShape } from 'react-router/lib/PropTypes'
import Component from './base-component'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
// ===================================================================
@@ -15,15 +15,16 @@ const _IGNORED_TAGNAMES = {
A: true,
BUTTON: true,
INPUT: true,
SELECT: true
SELECT: true,
}
@propTypes({
tagName: propTypes.string
className: propTypes.string,
tagName: propTypes.string,
})
export class BlockLink extends Component {
static contextTypes = {
router: routerShape
router: routerShape,
}
_style = { cursor: 'pointer' }
@@ -44,11 +45,22 @@ export class BlockLink extends Component {
}
}
_addAuxClickListener = ref => {
// FIXME: when https://github.com/facebook/react/issues/8529 is fixed,
// remove and use onAuxClickCapture.
// In Chrome ^55, middle-clicking triggers auxclick event instead of click
if (ref !== null) {
ref.addEventListener('auxclick', this._onClickCapture)
}
}
render () {
const { children, tagName = 'div' } = this.props
const { children, tagName = 'div', className } = this.props
const Component = tagName
return (
<Component
className={className}
ref={this._addAuxClickListener}
style={this._style}
onClickCapture={this._onClickCapture}
>

View File

@@ -1,14 +1,17 @@
import _ from 'intl'
import Icon from 'icon'
import isArray from 'lodash/isArray'
import isString from 'lodash/isString'
import map from 'lodash/map'
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 propTypes from './prop-types'
import _ from './intl'
import Button from './button'
import Icon from './icon'
import propTypes from './prop-types-decorator'
import Tooltip from './tooltip'
import {
disable as disableShortcuts,
enable as enableShortcuts
enable as enableShortcuts,
} from './shortcuts'
let instance
@@ -22,28 +25,97 @@ const modal = (content, onClose) => {
instance.setState({ content, onClose, showModal: true }, disableShortcuts)
}
export const alert = (title, body) => {
return new Promise(resolve => {
const { Body, Footer, Header, Title } = ReactModal
modal(
@propTypes({
buttons: propTypes.arrayOf(
propTypes.shape({
btnStyle: propTypes.string,
icon: propTypes.string,
label: propTypes.node.isRequired,
tooltip: propTypes.node,
value: propTypes.any,
})
).isRequired,
children: propTypes.node.isRequired,
icon: propTypes.string,
title: propTypes.node.isRequired,
})
class GenericModal extends Component {
_getBodyValue = () => {
const { body } = this.refs
if (body !== undefined) {
return body.getWrappedInstance === undefined
? body.value
: body.getWrappedInstance().value
}
}
_resolve = (value = this._getBodyValue()) => {
this.props.resolve(value)
instance.close()
}
_reject = () => {
this.props.reject()
instance.close()
}
render () {
const { buttons, icon, title } = this.props
const body = _addRef(this.props.children, 'body')
return (
<div>
<Header closeButton>
<Title>{title}</Title>
</Header>
<Body>{body}</Body>
<Footer>
<Button bsStyle='primary' onClick={() => {
resolve()
instance.close()
}}>
{_('alertOk')}
</Button>
</Footer>
</div>,
<ReactModal.Header closeButton>
<ReactModal.Title>
{icon ? (
<span>
<Icon icon={icon} /> {title}
</span>
) : (
title
)}
</ReactModal.Title>
</ReactModal.Header>
<ReactModal.Body>{body}</ReactModal.Body>
<ReactModal.Footer>
{map(buttons, ({ label, tooltip, value, icon, ...props }, key) => {
const button = (
<Button onClick={() => this._resolve(value)} {...props}>
{icon !== undefined && <Icon icon={icon} fixedWidth />}
{label}
</Button>
)
return (
<span key={key}>
{tooltip !== undefined ? (
<Tooltip content={tooltip}>{button}</Tooltip>
) : (
button
)}{' '}
</span>
)
})}
{this.props.reject !== undefined && (
<Button onClick={this._reject}>{_('genericCancel')}</Button>
)}
</ReactModal.Footer>
</div>
)
}
}
const ALERT_BUTTONS = [{ label: _('alertOk'), value: 'ok' }]
export const alert = (title, body) =>
new Promise(resolve => {
modal(
<GenericModal buttons={ALERT_BUTTONS} resolve={resolve} title={title}>
{body}
</GenericModal>,
resolve
)
})
}
const _addRef = (component, ref) => {
if (isString(component) || isArray(component)) {
@@ -56,79 +128,28 @@ const _addRef = (component, ref) => {
return component
}
@propTypes({
children: propTypes.node.isRequired,
title: propTypes.node.isRequired,
icon: propTypes.string
})
class Confirm extends Component {
_resolve = () => {
const { body } = this.refs
this.props.resolve(body && (body.getWrappedInstance
? body.getWrappedInstance().value
: body.value
))
instance.close()
}
_reject = () => {
this.props.reject()
instance.close()
}
const CONFIRM_BUTTONS = [{ btnStyle: 'primary', label: _('confirmOk') }]
_style = { marginRight: '0.5em' }
export const confirm = ({ body, icon = 'alarm', title }) =>
chooseAction({
body,
buttons: CONFIRM_BUTTONS,
icon,
title,
})
render () {
const { Body, Footer, Header, Title } = ReactModal
const { title, icon } = this.props
const body = _addRef(this.props.children, 'body')
return <div>
<Header closeButton>
<Title>
{icon
? <span><Icon icon={icon} /> {title}</span>
: title
}
</Title>
</Header>
<Body>
{body}
</Body>
<Footer>
<Button
bsStyle='primary'
onClick={this._resolve}
style={this._style}
>
{_('confirmOk')}
</Button>
<Button
bsStyle='secondary'
onClick={this._reject}
>
{_('confirmCancel')}
</Button>
</Footer>
</div>
}
}
export const confirm = ({
body,
title,
icon = 'alarm'
}) => {
export const chooseAction = ({ body, buttons, icon, title }) => {
return new Promise((resolve, reject) => {
modal(
<Confirm
title={title}
resolve={resolve}
reject={reject}
<GenericModal
buttons={buttons}
icon={icon}
reject={reject}
resolve={resolve}
title={title}
>
{body}
</Confirm>,
</GenericModal>,
reject
)
})
@@ -138,14 +159,18 @@ export default class Modal extends Component {
constructor () {
super()
this.state = { showModal: false }
}
componentDidMount () {
if (instance) {
throw new Error('Modal is a singleton!')
}
instance = this
}
componentWillMount () {
this.setState({ showModal: false })
componentWillUnmount () {
instance = undefined
}
close () {
@@ -160,14 +185,8 @@ export default class Modal extends Component {
}
render () {
const { showModal } = this.state
/* TODO: remove this work-around and use
* ReactModal.Body, ReactModal.Header, ...
* after this issue has been fixed:
* https://phabricator.babeljs.io/T6976
*/
return (
<ReactModal show={showModal} onHide={this._onHide}>
<ReactModal show={this.state.showModal} onHide={this._onHide}>
{this.state.content}
</ReactModal>
)

31
src/common/no-objects.js Normal file
View File

@@ -0,0 +1,31 @@
import React from 'react'
import { isEmpty } from 'lodash'
import propTypes from './prop-types-decorator'
// This component returns :
// - A loading icon when the objects are not fetched
// - A default message if the objects are fetched and the collection is empty
// - The children if the objects are fetched and the collection is not empty
//
// ```js
// <NoObjects collection={collection} emptyMessage={message}>
// {children}
// </NoObjects>
// ````
const NoObjects = ({ children, collection, emptyMessage }) =>
collection == null ? (
<img src='assets/loading.svg' alt='loading' />
) : isEmpty(collection) ? (
<p>{emptyMessage}</p>
) : (
<div>{children}</div>
)
propTypes(NoObjects)({
children: propTypes.node.isRequired,
collection: propTypes.oneOfType([propTypes.array, propTypes.object])
.isRequired,
emptyMessage: propTypes.node.isRequired,
})
export default NoObjects

View File

@@ -1,5 +1,10 @@
import _ from 'intl'
import ButtonLink from 'button-link'
import Icon from 'icon'
import React, { Component } from 'react'
import ReactNotify from 'react-notify'
import { connectStore } from 'utils'
import { isAdmin } from 'selectors'
let instance
@@ -7,31 +12,59 @@ export let error
export let info
export let success
@connectStore({
isAdmin,
})
export class Notification extends Component {
constructor () {
super()
componentDidMount () {
if (instance) {
throw new Error('Notification is a singleton!')
}
instance = this
}
componentWillUnmount () {
instance = undefined
}
// This special component never have to rerender!
shouldComponentUpdate () {
return false
}
render () {
return <ReactNotify ref={notification => {
if (!notification) {
return
}
return (
<ReactNotify
ref={notification => {
if (!notification) {
return
}
error = (title, body) => notification.error(title, body, 3e3)
info = (title, body) => notification.info(title, body, 3e3)
success = (title, body) => notification.success(title, body, 3e3)
}} />
error = (title, body) =>
notification.error(
title,
this.props.isAdmin ? (
<div>
<div>{body}</div>
<ButtonLink
btnStyle='danger'
className='mt-1'
size='small'
to='/settings/logs'
>
<Icon icon='logs' /> {_('showLogs')}
</ButtonLink>
</div>
) : (
body
),
6e3
)
info = (title, body) => notification.info(title, body, 3e3)
success = (title, body) => notification.success(title, body, 3e3)
}}
/>
)
}
}

View File

@@ -5,7 +5,7 @@ import React, { Component } from 'react'
@connectStore(() => {
const object = createGetObject()
return (state, props) => ({object: object(state, props)})
return (state, props) => ({ object: object(state, props) })
})
export default class ObjectName extends Component {
render () {

View File

@@ -0,0 +1,33 @@
import assign from 'lodash/assign'
import PropTypes from 'prop-types'
// Decorators to help declaring properties and context types on React
// components without using the tedious static properties syntax.
//
// ```js
// @propTypes({
// children: propTypes.node.isRequired
// }, {
// store: propTypes.object.isRequired
// })
// class MyComponent extends React.Component {}
// ```
const propTypes = (propTypes, contextTypes) => target => {
if (propTypes !== undefined) {
target.propTypes = {
...target.propTypes,
...propTypes,
}
}
if (contextTypes !== undefined) {
target.contextTypes = {
...target.contextTypes,
...contextTypes,
}
}
return target
}
assign(propTypes, PropTypes)
export { propTypes as default }

View File

@@ -1,22 +0,0 @@
import assign from 'lodash/assign'
import { PropTypes } from 'react'
// Decorators to help declaring on React components without using the
// tedious static properties syntax.
//
// ```js
// @propTypes({
// children: propTypes.node.isRequired
// })
// class MyComponent extends React.Component {}
// ```
const propTypes = types => target => {
target.propTypes = {
...target.propTypes,
...types
}
return target
}
assign(propTypes, PropTypes)
export { propTypes as default }

View File

@@ -1,20 +1,17 @@
import React, { Component } from 'react'
import RFB from '@nraynaud/novnc/lib/rfb'
import URL from 'url-parse'
import { createBackoff } from 'jsonrpc-websocket-client'
import { RFB } from 'novnc-node'
import {
format as formatUrl,
parse as parseUrl,
resolve as resolveUrl
} from 'url'
import { enable as enableShortcuts, disable as disableShortcuts } from 'shortcuts'
enable as enableShortcuts,
disable as disableShortcuts,
} from 'shortcuts'
import propTypes from './prop-types'
const parseRelativeUrl = url => parseUrl(resolveUrl(String(window.location), url))
import propTypes from './prop-types-decorator'
const PROTOCOL_ALIASES = {
'http:': 'ws:',
'https:': 'wss:'
'https:': 'wss:',
}
const fixProtocol = url => {
const protocol = PROTOCOL_ALIASES[url.protocol]
@@ -25,7 +22,7 @@ const fixProtocol = url => {
@propTypes({
onClipboardChange: propTypes.func,
url: propTypes.string.isRequired
url: propTypes.string.isRequired,
})
export default class NoVnc extends Component {
constructor (props) {
@@ -42,12 +39,15 @@ export default class NoVnc extends Component {
}
}
if (state !== 'disconnected') {
if (state !== 'disconnected' || this.refs.canvas == null) {
return
}
clearTimeout(this._retryTimeout)
this._retryTimeout = setTimeout(this._connect, this._retryGen.next().value)
this._retryTimeout = setTimeout(
this._connect,
this._retryGen.next().value
)
}
}
@@ -77,23 +77,39 @@ export default class NoVnc extends Component {
_connect = () => {
this._clean()
const url = parseRelativeUrl(this.props.url)
const { canvas } = this.refs
if (!canvas) {
return
}
const url = new URL(this.props.url)
fixProtocol(url)
const isSecure = url.protocol === 'wss:'
const { onClipboardChange } = this.props
const rfb = this._rfb = new RFB({
const rfb = (this._rfb = new RFB({
encrypt: isSecure,
target: this.refs.canvas,
wsProtocols: [ 'chat' ],
onClipboard: onClipboardChange && ((_, text) => {
onClipboardChange(text)
}),
onUpdateState: this._onUpdateState
})
onClipboard:
onClipboardChange &&
((_, text) => {
onClipboardChange(text)
}),
onUpdateState: this._onUpdateState,
}))
rfb.connect(formatUrl(url))
// remove leading slashes from the path
//
// a leading slassh will be added by noVNC
const clippedPath = url.pathname.replace(/^\/+/, '')
// a port is required
//
// if not available from the URL, use the default ones
const port = url.port || (isSecure ? 443 : 80)
rfb.connect(url.hostname, port, null, clippedPath)
disableShortcuts()
}
@@ -139,13 +155,15 @@ export default class NoVnc extends Component {
}
render () {
return <canvas
className='center-block'
height='480'
onMouseEnter={this._focus}
onMouseLeave={this._unfocus}
ref='canvas'
width='640'
/>
return (
<canvas
className='center-block'
height='480'
onMouseEnter={this._focus}
onMouseLeave={this._unfocus}
ref='canvas'
width='640'
/>
)
}
}

View File

@@ -1,88 +1,95 @@
import _ from 'intl'
import React from 'react'
import { startsWith } from 'lodash'
import Icon from './icon'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
import { createGetObject } from './selectors'
import { isSrWritable } from './xo'
import {
connectStore,
formatSize
} from './utils'
import { connectStore, formatSize } from './utils'
// ===================================================================
const OBJECT_TYPE_TO_ICON = {
'VM-template': 'vm',
host: 'host',
network: 'network'
network: 'network',
}
// Host, Network, VM-template.
export const PoolObjectItem = propTypes({
object: propTypes.object.isRequired
})(connectStore(() => {
const getPool = createGetObject(
(_, props) => props.object.$pool
)
const PoolObjectItem = propTypes({
object: propTypes.object.isRequired,
})(
connectStore(() => {
const getPool = createGetObject((_, props) => props.object.$pool)
return (state, props) => ({
pool: getPool(state, props)
return (state, props) => ({
pool: getPool(state, props),
})
})(({ object, pool }) => {
const icon = OBJECT_TYPE_TO_ICON[object.type]
const { id } = object
return (
<span>
<Icon icon={icon} /> {`${object.name_label || id} `}
{pool && `(${pool.name_label || pool.id})`}
</span>
)
})
})(({ object, pool }) => {
const icon = OBJECT_TYPE_TO_ICON[object.type]
const { id } = object
return (
<span>
<Icon icon={icon} /> {`${object.name_label || id} `}
{pool && `(${pool.name_label || pool.id})`}
</span>
)
}))
)
// SR.
export const SrItem = propTypes({
sr: propTypes.object.isRequired
})(connectStore(() => {
const getContainer = createGetObject(
(_, props) => props.sr.$container
)
const SrItem = propTypes({
sr: propTypes.object.isRequired,
})(
connectStore(() => {
const getContainer = createGetObject((_, props) => props.sr.$container)
return (state, props) => ({
container: getContainer(state, props)
return (state, props) => ({
container: getContainer(state, props),
})
})(({ sr, container }) => {
let label = `${sr.name_label || sr.id}`
if (isSrWritable(sr)) {
label += ` (${formatSize(sr.size - sr.physical_usage)} free)`
}
return (
<span>
<Icon icon='sr' /> {label}
</span>
)
})
})(({ sr, container }) => {
let label = `${sr.name_label || sr.id}`
if (isSrWritable(sr)) {
label += ` (${formatSize(sr.size - sr.physical_usage)} free)`
}
return (
<span>
<Icon icon='sr' /> {label}
</span>
)
}))
)
// VM.
export const VmItem = propTypes({
vm: propTypes.object.isRequired
})(connectStore(() => {
const getContainer = createGetObject(
(_, props) => props.vm.$container
)
const VmItem = propTypes({
vm: propTypes.object.isRequired,
})(
connectStore(() => {
const getContainer = createGetObject((_, props) => props.vm.$container)
return (state, props) => ({
container: getContainer(state, props)
})
})(({ vm, container }) => (
return (state, props) => ({
container: getContainer(state, props),
})
})(({ vm, container }) => (
<span>
<Icon icon={`vm-${vm.power_state.toLowerCase()}`} />{' '}
{vm.name_label || vm.id}
{container && ` (${container.name_label || container.id})`}
</span>
))
)
const VgpuItem = connectStore(() => ({
vgpuType: createGetObject((_, props) => props.vgpu.vgpuType),
}))(({ vgpu, vgpuType }) => (
<span>
<Icon icon={`vm-${vm.power_state.toLowerCase()}`} /> {vm.name_label || vm.id}
{container && ` (${container.name_label || container.id})`}
<Icon icon='vgpu' /> {vgpuType.modelName}
</span>
)))
))
// ===================================================================
@@ -98,11 +105,7 @@ const xoItemToRender = {
<Icon icon='remote' /> {remote.value.name}
</span>
),
role: role => (
<span>
{role.name}
</span>
),
role: role => <span>{role.name}</span>,
user: user => (
<span>
<Icon icon='user' /> {user.email}
@@ -123,7 +126,7 @@ const xoItemToRender = {
<Icon icon='ip' /> {ipPool.name}
</span>
),
ipAddress: ({label, used}) => {
ipAddress: ({ label, used }) => {
if (used) {
return <strong className='text-warning'>{label}</strong>
}
@@ -139,7 +142,8 @@ const xoItemToRender = {
VDI: vdi => (
<span>
<Icon icon='disk' /> {vdi.name_label} {vdi.name_description && <span> ({vdi.name_description})</span>}
<Icon icon='disk' /> {vdi.name_label}{' '}
{vdi.name_description && <span> ({vdi.name_description})</span>}
</span>
),
@@ -156,16 +160,18 @@ const xoItemToRender = {
'VM-snapshot': vm => <VmItem vm={vm} />,
'VM-controller': vm => (
<span>
<Icon icon='host' />
{' '}
<VmItem vm={vm} />
<Icon icon='host' /> <VmItem vm={vm} />
</span>
),
// PIF.
PIF: pif => (
<span>
<Icon icon='network' /> {pif.device}
<Icon
icon='network'
color={pif.carrier ? 'text-success' : 'text-danger'}
/>{' '}
{pif.device} ({pif.deviceName})
</span>
),
@@ -174,14 +180,40 @@ const xoItemToRender = {
<span>
<Icon icon='tag' /> {tag.value}
</span>
)
),
// GPUs
vgpu: vgpu => <VgpuItem vgpu={vgpu} />,
vgpuType: type => (
<span>
<Icon icon='gpu' /> {type.modelName} ({type.vendorName}){' '}
{type.maxResolutionX}x{type.maxResolutionY}
</span>
),
gpuGroup: group => (
<span>
{startsWith(group.name_label, 'Group of ')
? group.name_label.slice(9)
: group.name_label}
</span>
),
}
const renderXoItem = (item, {
className
} = {}) => {
const renderXoItem = (item, { className } = {}) => {
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`)
@@ -214,11 +246,17 @@ const GenericXoItem = connectStore(() => {
const getObject = createGetObject()
return (state, props) => ({
xoItem: getObject(state, props)
xoItem: getObject(state, props),
})
})(({ xoItem, ...props }) => xoItem
? renderXoItem(xoItem, props)
: <span className='text-muted'>{_('errorNoSuchItem')}</span>
})(
({ xoItem, ...props }) =>
xoItem ? renderXoItem(xoItem, props) : renderXoUnknownItem()
)
export const renderXoItemFromId = (id, props) => <GenericXoItem {...props} id={id} />
export const renderXoItemFromId = (id, props) => (
<GenericXoItem {...props} id={id} />
)
export const renderXoUnknownItem = () => (
<span className='text-muted'>{_('errorNoSuchItem')}</span>
)

View File

@@ -1,42 +1,43 @@
import includes from 'lodash/includes'
import join from 'lodash/join'
import classNames from 'classnames'
import later from 'later'
import map from 'lodash/map'
import React from 'react'
import sortedIndex from 'lodash/sortedIndex'
import { FormattedDate, FormattedTime } from 'react-intl'
import {
Tab,
Tabs
} from 'react-bootstrap-4/lib'
import { forEach, includes, isArray, map, sortedIndex } from 'lodash'
import _ from './intl'
import Button from './button'
import Component from './base-component'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
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'
// ===================================================================
// By default later use UTC but we use this line for futures versions.
// By default, later uses UTC but we use this line for future versions.
later.date.UTC()
// ===================================================================
const NAV_EACH_SELECTED = 1
const NAV_EVERY_N = 2
const CLICKABLE = { cursor: 'pointer' }
const PREVIEW_SLIDER_STYLE = { width: '400px' }
// ===================================================================
const UNITS = ['minute', 'hour', 'monthDay', 'month', 'weekDay']
const MINUTES_RANGE = [2, 30]
const HOURS_RANGE = [2, 12]
const MONTH_DAYS_RANGE = [2, 15]
const MONTHS_RANGE = [2, 6]
const MIN_PREVIEWS = 5
const MAX_PREVIEWS = 20
const MONTHS = [
[ 0, 1, 2 ],
[ 3, 4, 5 ],
[ 6, 7, 8 ],
[ 9, 10, 11 ]
]
const MONTHS = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11]]
const DAYS = (() => {
const days = []
@@ -54,20 +55,16 @@ const DAYS = (() => {
return days
})()
const WEEK_DAYS = [
[ 0, 1, 2 ],
[ 3, 4, 5 ],
[ 6 ]
]
const WEEK_DAYS = [[0, 1, 2], [3, 4, 5], [6]]
const HOURS = (() => {
const hours = []
for (let i = 0; i < 3; i++) {
for (let i = 0; i < 4; i++) {
hours[i] = []
for (let j = 0; j < 8; j++) {
hours[i].push(8 * i + j)
for (let j = 0; j < 6; j++) {
hours[i].push(6 * i + j)
}
}
@@ -93,7 +90,7 @@ const PICKTIME_TO_ID = {
hour: 1,
monthDay: 2,
month: 3,
weekDay: 4
weekDay: 4,
}
const TIME_FORMAT = {
@@ -110,44 +107,58 @@ const TIME_FORMAT = {
// Therefore we can use UTC everywhere and say to the user that the
// previews are in the configured timezone.
timeZone: 'UTC'
timeZone: 'UTC',
}
// ===================================================================
// monthNum: [ 0 : 11 ]
const getMonthName = (monthNum) =>
const getMonthName = monthNum => (
<FormattedDate value={Date.UTC(1970, monthNum)} month='long' timeZone='UTC' />
)
// dayNum: [ 0 : 6 ]
const getDayName = (dayNum) =>
const getDayName = dayNum => (
// January, 1970, 5th => Monday
<FormattedDate value={Date.UTC(1970, 0, 4 + dayNum)} weekday='long' timeZone='UTC' />
<FormattedDate
value={Date.UTC(1970, 0, 4 + dayNum)}
weekday='long'
timeZone='UTC'
/>
)
// ===================================================================
@propTypes({
cronPattern: propTypes.string.isRequired
cronPattern: propTypes.string.isRequired,
})
export class SchedulePreview extends Component {
_handleChange = value => {
this.setState({
value
})
}
render () {
const { cronPattern } = this.props
const { value } = this.state
const cronSched = later.parse.cron(cronPattern)
const dates = later.schedule(cronSched).next(this.state.value || MIN_PREVIEWS)
// Due to implementation, the range used for months is 0-11
// instead of 1-12
forEach(cronSched.schedules[0].M, (v, i, a) => {
a[i] = v + 1
})
const dates = later.schedule(cronSched).next(value)
return (
<div>
<div className='alert alert-info' role='alert'>
{_('cronPattern')} <strong>{cronPattern}</strong>
</div>
<div className='form-inline pb-1'>
<Range min={MIN_PREVIEWS} max={MAX_PREVIEWS} onChange={this._handleChange} />
<div className='mb-1' style={PREVIEW_SLIDER_STYLE}>
<Range
min={MIN_PREVIEWS}
max={MAX_PREVIEWS}
onChange={this.linkState('value')}
value={+value}
/>
</div>
<ul className='list-group'>
{map(dates, (date, id) => (
@@ -168,7 +179,7 @@ export class SchedulePreview extends Component {
children: propTypes.any.isRequired,
onChange: propTypes.func.isRequired,
tdId: propTypes.number.isRequired,
value: propTypes.bool.isRequired
value: propTypes.bool.isRequired,
})
class ToggleTd extends Component {
_onClick = () => {
@@ -179,7 +190,11 @@ class ToggleTd extends Component {
render () {
const { props } = this
return (
<td style={{ cursor: 'pointer' }} className={props.value ? 'table-success' : ''} onClick={this._onClick}>
<td
className={classNames('text-xs-center', props.value && 'table-success')}
onClick={this._onClick}
style={CLICKABLE}
>
{props.children}
</td>
)
@@ -189,14 +204,15 @@ class ToggleTd extends Component {
// ===================================================================
@propTypes({
labelId: propTypes.string.isRequired,
options: propTypes.array.isRequired,
optionsRenderer: propTypes.func,
optionRenderer: propTypes.func,
onChange: propTypes.func.isRequired,
value: propTypes.array.isRequired
value: propTypes.array.isRequired,
})
class TableSelect extends Component {
static defaultProps = {
optionsRenderer: value => value
optionRenderer: value => value,
}
_reset = () => {
@@ -225,12 +241,7 @@ class TableSelect extends Component {
}
render () {
const {
options,
optionsRenderer,
value
} = this.props
const { length } = options[0]
const { labelId, options, optionRenderer, value } = this.props
return (
<div>
@@ -238,25 +249,23 @@ class TableSelect extends Component {
<tbody>
{map(options, (line, i) => (
<tr key={i}>
{map(line, (tdOption, j) => {
const tdId = length * i + j
return (
<ToggleTd
children={optionsRenderer(tdOption)}
tdId={tdId}
key={tdId}
onChange={this._handleChange}
value={includes(value, tdId)}
/>
)
})}
{map(line, tdOption => (
<ToggleTd
children={optionRenderer(tdOption)}
tdId={tdOption}
key={tdOption}
onChange={this._handleChange}
value={includes(value, tdOption)}
/>
))}
</tr>
))}
</tbody>
</table>
<button className='btn btn-secondary pull-right' onClick={this._reset}>
{_('selectTableReset')}
</button>
<Button className='pull-right' onClick={this._reset}>
{_(`selectTableAll${labelId}`)}{' '}
{value && !value.length && <Icon icon='success' />}
</Button>
</div>
)
}
@@ -264,217 +273,296 @@ class TableSelect extends Component {
// ===================================================================
// "2,7" => [2,7] "*/2" => 2 "*" => []
const cronToValue = (cron, range) => {
if (cron.indexOf('/') === 1) {
return +cron.split('/')[1]
}
if (cron === '*') {
return []
}
return map(cron.split(','), Number)
}
// [2,7] => "2,7" 2 => "*/2" [] => "*"
const valueToCron = value => {
if (!isArray(value)) {
return `*/${value}`
}
if (!value.length) {
return '*'
}
return value.join(',')
}
@propTypes({
optionsRenderer: propTypes.func,
headerAddon: propTypes.node,
optionRenderer: propTypes.func,
onChange: propTypes.func.isRequired,
range: propTypes.array,
labelId: propTypes.string.isRequired,
value: propTypes.any.isRequired,
valueRenderer: propTypes.func
})
class TimePicker extends Component {
static defaultProps = {
valueRenderer: e => +e
}
_update = cron => {
const { tableValue, rangeValue } = this.state
constructor () {
super()
this.state = {
activeKey: NAV_EACH_SELECTED,
tableValue: []
}
}
const newValue = cronToValue(cron)
const periodic = !isArray(newValue)
_update (props) {
const { value, valueRenderer } = props
if (value.indexOf('/') === 1) {
this.setState({
activeKey: NAV_EVERY_N
}, () => { this.refs.range.value = value.split('/')[1] })
} else {
this.setState({
activeKey: NAV_EACH_SELECTED,
tableValue: value === '*'
? []
: map(value.split(','), valueRenderer)
})
}
}
componentWillMount () {
this._update(this.props)
}
componentWillReceiveProps (props) {
this._update(props)
}
_selectTab = activeKey => {
this.setState({
activeKey
}, () => {
const { activeKey, tableValue } = this.state
const { onChange } = this.props
const { refs } = this
if (activeKey === NAV_EACH_SELECTED) {
onChange(tableValue)
} else {
onChange(refs.range.value)
}
periodic,
tableValue: periodic ? tableValue : newValue,
rangeValue: periodic ? newValue : rangeValue,
})
}
_handleTableValue = tableValue => {
this.setState({
tableValue
}, () => this.props.onChange(tableValue))
componentWillReceiveProps (props) {
if (props.value !== this.props.value) {
this._update(props.value)
}
}
render () {
const {
onChange,
options,
optionsRenderer,
range,
labelId
} = this.props
const { tableValue } = this.state
componentDidMount () {
this._update(this.props.value)
}
const tableSelect = (
<TableSelect
onChange={this._handleTableValue}
options={options}
optionsRenderer={optionsRenderer}
value={tableValue}
/>
)
_onChange = value => {
this.props.onChange(valueToCron(value))
}
_tableTab = () => this._onChange(this.state.tableValue || [])
_periodicTab = () =>
this._onChange(this.state.rangeValue || this.props.range[0])
render () {
const { headerAddon, labelId, options, optionRenderer, range } = this.props
const { periodic, tableValue, rangeValue } = this.state
return (
<Card>
<CardHeader>
{_(`scheduling${labelId}`)}
{headerAddon}
</CardHeader>
<CardBlock>
{range
? (
<Tabs bsStyle='tabs' activeKey={this.state.activeKey} onSelect={this._selectTab}>
<Tab tabClassName='nav-item' eventKey={NAV_EACH_SELECTED} title={_(`schedulingEachSelected${labelId}`)}>
{tableSelect}
</Tab>
<Tab tabClassName='nav-item' eventKey={NAV_EVERY_N} title={_(`schedulingEveryN${labelId}`)}>
<Range ref='range' min={range[0]} max={range[1]} onChange={onChange} />
</Tab>
</Tabs>
) : tableSelect
}
{range && (
<ul className='nav nav-tabs mb-1'>
<li className='nav-item'>
<a
onClick={this._tableTab}
className={classNames('nav-link', !periodic && 'active')}
style={CLICKABLE}
>
{_(`schedulingEachSelected${labelId}`)}
</a>
</li>
<li className='nav-item'>
<a
onClick={this._periodicTab}
className={classNames('nav-link', periodic && 'active')}
style={CLICKABLE}
>
{_(`schedulingEveryN${labelId}`)}
</a>
</li>
</ul>
)}
{periodic ? (
<Range
ref='range'
min={range[0]}
max={range[1]}
onChange={this._onChange}
value={rangeValue}
/>
) : (
<TableSelect
labelId={labelId}
onChange={this._onChange}
options={options}
optionRenderer={optionRenderer}
value={tableValue || []}
/>
)}
</CardBlock>
</Card>
)
}
}
// ===================================================================
const HOURS_RANGE = [2, 12]
const MINUTES_RANGE = [2, 30]
const decrement = e => e - 1
@propTypes({
cronPattern: propTypes.string.isRequired,
onChange: propTypes.func,
timezone: propTypes.string
})
export default class Scheduler extends Component {
_update (type, value) {
if (Array.isArray(value)) {
if (!value.length) {
value = '*'
} else {
value = join(
(type === 'monthDay' || type === 'month')
? map(value, n => n + 1)
: value,
','
)
}
} else {
value = `*/${value}`
}
const { props } = this
const cronPattern = props.cronPattern.split(' ')
cronPattern[PICKTIME_TO_ID[type]] = value
this.props.onChange({
cronPattern: cronPattern.join(' '),
timezone: props.timezone
})
const isWeekDayMode = ({ monthDayPattern, weekDayPattern }) => {
if (monthDayPattern === '*' && weekDayPattern === '*') {
return
}
_onHourChange = value => this._update('hour', value)
_onMinuteChange = value => this._update('minute', value)
_onMonthChange = value => this._update('month', value)
_onMonthDayChange = value => this._update('monthDay', value)
_onWeekDayChange = value => this._update('weekDay', value)
return weekDayPattern !== '*'
}
_onTimezoneChange = timezone => {
const { props } = this
props.onChange({
cronPattern: props.cronPattern,
timezone
})
@propTypes({
monthDayPattern: propTypes.string.isRequired,
weekDayPattern: propTypes.string.isRequired,
})
class DayPicker extends Component {
state = {
weekDayMode: isWeekDayMode(this.props),
}
componentWillReceiveProps (props) {
const weekDayMode = isWeekDayMode(props)
if (weekDayMode !== undefined) {
this.setState({ weekDayMode })
}
}
_setWeekDayMode = weekDayMode => {
this.props.onChange(['*', '*'])
this.setState({ weekDayMode })
}
_onChange = cron => {
const isMonthDayPattern = !this.state.weekDayMode || includes(cron, '/')
this.props.onChange([
isMonthDayPattern ? cron : '*',
isMonthDayPattern ? '*' : cron,
])
}
render () {
const {
cronPattern,
timezone
} = this.props
const cronPatternArr = cronPattern.split(' ')
const { monthDayPattern, weekDayPattern } = this.props
const { weekDayMode } = this.state
const dayModeToggle = (
<Tooltip
content={_(
weekDayMode ? 'schedulingSetMonthDayMode' : 'schedulingSetWeekDayMode'
)}
>
<span className='pull-right'>
<Toggle
onChange={this._setWeekDayMode}
iconSize={1}
value={weekDayMode}
/>
</span>
</Tooltip>
)
return (
<TimePicker
headerAddon={dayModeToggle}
key={weekDayMode ? 'week' : 'month'}
labelId='Day'
optionRenderer={weekDayMode ? getDayName : undefined}
options={weekDayMode ? WEEK_DAYS : DAYS}
onChange={this._onChange}
range={MONTH_DAYS_RANGE}
setWeekDayMode={this._setWeekDayMode}
value={weekDayMode ? weekDayPattern : monthDayPattern}
/>
)
}
}
// ===================================================================
@propTypes({
cronPattern: propTypes.string,
onChange: propTypes.func,
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._getCronPattern().split(' ')
forEach(newCrons, (cron, unit) => {
cronPattern[PICKTIME_TO_ID[unit]] = cron
})
this.props.onChange({
cronPattern: cronPattern.join(' '),
timezone: this._getTimezone(),
})
}
forEach(UNITS, unit => {
this[`_${unit}Change`] = cron => this._onCronChange({ [unit]: cron })
})
this._dayChange = ([monthDay, weekDay]) =>
this._onCronChange({ monthDay, weekDay })
}
_onTimezoneChange = timezone => {
this.props.onChange({
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 cronPatternArr = this._getCronPattern().split(' ')
const timezone = this._getTimezone()
return (
<div className='card-block'>
<Row>
<Col mediumSize={6}>
<Col largeSize={6}>
<TimePicker
labelId='Month'
optionsRenderer={getMonthName}
optionRenderer={getMonthName}
options={MONTHS}
onChange={this._onMonthChange}
onChange={this._monthChange}
range={MONTHS_RANGE}
value={cronPatternArr[PICKTIME_TO_ID['month']]}
valueRenderer={decrement}
/>
<TimePicker
labelId='MonthDay'
options={DAYS}
onChange={this._onMonthDayChange}
value={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
valueRenderer={decrement}
/>
<TimePicker
labelId='WeekDay'
optionsRenderer={getDayName}
options={WEEK_DAYS}
onChange={this._onWeekDayChange}
value={cronPatternArr[PICKTIME_TO_ID['weekDay']]}
/>
</Col>
<Col mediumSize={6}>
<Col largeSize={6}>
<DayPicker
onChange={this._dayChange}
monthDayPattern={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
weekDayPattern={cronPatternArr[PICKTIME_TO_ID['weekDay']]}
/>
</Col>
</Row>
<Row>
<Col largeSize={6}>
<TimePicker
labelId='Hour'
options={HOURS}
range={HOURS_RANGE}
onChange={this._onHourChange}
onChange={this._hourChange}
value={cronPatternArr[PICKTIME_TO_ID['hour']]}
/>
</Col>
<Col largeSize={6}>
<TimePicker
labelId='Minute'
options={MINS}
range={MINUTES_RANGE}
onChange={this._onMinuteChange}
onChange={this._minuteChange}
value={cronPatternArr[PICKTIME_TO_ID['minute']]}
/>
</Col>
@@ -482,7 +570,10 @@ export default class Scheduler extends Component {
<Row>
<Col>
<hr />
<TimezonePicker value={timezone} onChange={this._onTimezoneChange} />
<TimezonePicker
value={timezone}
onChange={this._onTimezoneChange}
/>
</Col>
</Row>
</div>

View File

@@ -0,0 +1,34 @@
import _ from 'intl'
import Component from 'base-component'
import Icon from 'icon'
import propTypes from 'prop-types-decorator'
import React from 'react'
import { omit } from 'lodash'
@propTypes({
multi: propTypes.bool,
label: propTypes.node,
onChange: propTypes.func.isRequired,
})
export default class SelectFiles extends Component {
_onChange = e => {
const { multi, onChange } = this.props
const { files } = e.target
onChange(multi ? files : files[0])
}
render () {
return (
<label className='btn btn-secondary btn-file hidden'>
<Icon icon='file' /> {this.props.label || _('browseFiles')}
<input
{...omit(this.props, ['hidden', 'label', 'onChange', 'multi'])}
hidden
onChange={this._onChange}
type='file'
/>
</label>
)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,21 @@
import add from 'lodash/add'
import checkPermissions from 'xo-acl-resolver'
import filter from 'lodash/filter'
import find from 'lodash/find'
import forEach from 'lodash/forEach'
import groupBy from 'lodash/groupBy'
import isArray from 'lodash/isArray'
import isArrayLike from 'lodash/isArrayLike'
import isFunction from 'lodash/isFunction'
import keys from 'lodash/keys'
import map from 'lodash/map'
import orderBy from 'lodash/orderBy'
import pickBy from 'lodash/pickBy'
import size from 'lodash/size'
import slice from 'lodash/slice'
import { createSelector as create } from 'reselect'
import {
filter,
find,
forEach,
groupBy,
isArray,
isArrayLike,
isFunction,
keys,
map,
orderBy,
pickBy,
size,
slice,
} from 'lodash'
import invoke from './invoke'
import shallowEqual from './shallow-equal'
@@ -23,9 +26,8 @@ import { EMPTY_ARRAY, EMPTY_OBJECT } from './utils'
export {
// That's usually the name we want to import.
createSelector,
// But selectors.create is nice too :)
createSelector as create
createSelector as create,
} from 'reselect'
// -------------------------------------------------------------------
@@ -37,12 +39,16 @@ export {
// Use case: in connect, to avoid rerendering a component where the
// objects are still the same.
const _createCollectionWrapper = selector => {
let cache
let cache, previous
return (...args) => {
const value = selector(...args)
if (!shallowEqual(value, cache)) {
cache = value
if (value !== previous) {
previous = value
if (!shallowEqual(value, cache)) {
cache = value
}
}
return cache
}
@@ -86,9 +92,7 @@ const _create2 = (...inputs) => {
const args = new Array(n)
for (let i = 0, j = 0; i < n; ++i) {
const input = inputs[i]
args[i] = input === _SELECTOR_PLACEHOLDER
? arguments[j++]
: input
args[i] = input === _SELECTOR_PLACEHOLDER ? arguments[j++] : input
}
return resultFn.apply(this, args)
@@ -99,107 +103,94 @@ const _create2 = (...inputs) => {
// Generic selector creators.
export const createCounter = (collection, predicate) =>
_create2(
collection,
predicate,
(collection, predicate) => {
if (!predicate) {
return size(collection)
}
let count = 0
forEach(collection, item => {
if (predicate(item)) {
++count
}
})
return count
_create2(collection, predicate, (collection, predicate) => {
if (!predicate) {
return size(collection)
}
)
let count = 0
forEach(collection, item => {
if (predicate(item)) {
++count
}
})
return count
})
// Creates an object selector from an object selector and a properties
// selector.
//
// Should only be used with a reasonable number of properties.
export const createPicker = (object, props) =>
_createCollectionWrapper(
_create2(
object, props,
(object, props) => {
const values = {}
forEach(props, prop => {
const value = object[prop]
if (value) {
values[prop] = value
}
})
return values
}
)
_create2(
object,
props,
_createCollectionWrapper((object, props) => {
const values = {}
forEach(props, prop => {
const value = object[prop]
if (value) {
values[prop] = value
}
})
return values
})
)
// Special cases:
// - predicate == null → no filtering
// - predicate === false → everything is filtered out
export const createFilter = (collection, predicate) =>
_createCollectionWrapper(
_create2(
collection,
predicate,
(collection, predicate) => predicate === false
? (isArrayLike(collection) ? EMPTY_ARRAY : EMPTY_OBJECT)
: predicate
? (isArrayLike(collection) ? filter : pickBy)(collection, predicate)
: collection
_create2(
collection,
predicate,
_createCollectionWrapper(
(collection, predicate) =>
predicate === false
? isArrayLike(collection) ? EMPTY_ARRAY : EMPTY_OBJECT
: predicate
? (isArrayLike(collection) ? filter : pickBy)(collection, predicate)
: collection
)
)
export const createFinder = (collection, predicate) =>
_create2(
collection,
predicate,
find
)
_create2(collection, predicate, find)
export const createGroupBy = (collection, getter) =>
_create2(
collection,
getter,
groupBy
)
_create2(collection, getter, groupBy)
export const createPager = (array, page, n = 25) => _createCollectionWrapper(
export const createPager = (array, page, n = 25) =>
_create2(
array,
page,
n,
(array, page, n) => {
_createCollectionWrapper((array, page, n) => {
const start = (page - 1) * n
return slice(array, start, start + n)
}
})
)
)
export const createSort = (
collection,
getter = 'name_label',
order = 'asc'
) => _create2(collection, getter, order, orderBy)
export const createSort = (collection, getter = 'name_label', order = 'asc') =>
_create2(collection, getter, order, orderBy)
export const createSumBy = (itemsSelector, iterateeSelector) =>
_create2(itemsSelector, iterateeSelector, (items, iteratee) =>
map(items, iteratee).reduce(add, 0)
)
export const createTop = (collection, iteratee, n) =>
_createCollectionWrapper(
_create2(
collection,
iteratee,
n,
(objects, iteratee, n) => {
let results = orderBy(objects, iteratee, 'desc')
if (n < results.length) {
results.length = n
}
return results
_create2(
collection,
iteratee,
n,
_createCollectionWrapper((objects, iteratee, n) => {
const results = orderBy(objects, iteratee, 'desc')
if (n < results.length) {
results.length = n
}
)
return results
})
)
// ===================================================================
@@ -207,9 +198,8 @@ export const createTop = (collection, iteratee, n) =>
export const areObjectsFetched = state => state.objects.fetched
const _getId = (state, { routeParams, id }) => routeParams
? routeParams.id
: id
const _getId = (state, { routeParams, id }) =>
routeParams ? routeParams.id : id
export const getLang = state => state.lang
@@ -217,13 +207,44 @@ export const getStatus = state => state.status
export const getUser = state => state.user
export const getCheckPermissions = invoke(() => {
const getPredicate = create(
state => state.permissions,
state => state.objects,
(permissions, objects) => {
objects = objects.all
const getObject = id => objects[id] || EMPTY_OBJECT
return (id, permission) =>
checkPermissions(permissions, getObject, id, permission)
}
)
const isTrue = () => true
const isFalse = () => false
return state => {
const user = getUser(state)
if (!user) {
return isFalse
}
if (user.permission === 'admin') {
return isTrue
}
return getPredicate(state)
}
})
const _getPermissionsPredicate = invoke(() => {
const getPredicate = create(
state => state.permissions,
state => state.objects,
(permissions, objects) => {
objects = objects.all
const getObject = id => (objects[id] || EMPTY_OBJECT)
const getObject = id => objects[id] || EMPTY_OBJECT
return id => checkPermissions(permissions, getObject, id.id || id, 'view')
}
@@ -253,33 +274,36 @@ export const isAdmin = (...args) => {
// Common selector creators.
// Creates an object selector from an id selector.
export const createGetObject = (idSelector = _getId) =>
(state, props, useResourceSet) => {
const object = state.objects.all[idSelector(state, props)]
if (!object) {
return
}
if (useResourceSet) {
return object
}
const predicate = _getPermissionsPredicate(state)
if (!predicate) {
if (predicate == null) {
return object // no filtering
}
// predicate is false.
return
}
if (predicate(object)) {
return object
}
export const createGetObject = (idSelector = _getId) => (
state,
props,
useResourceSet
) => {
const object = state.objects.all[idSelector(state, props)]
if (!object) {
return
}
if (useResourceSet) {
return object
}
const predicate = _getPermissionsPredicate(state)
if (!predicate) {
if (predicate == null) {
return object // no filtering
}
// predicate is false.
return
}
if (predicate(object)) {
return object
}
}
// Specialized createSort() configured for a given type.
export const createSortForType = invoke(() => {
const iterateesByType = {
@@ -290,30 +314,27 @@ export const createSortForType = invoke(() => {
tag: tag => tag,
VBD: vbd => vbd.position,
'VDI-snapshot': snapshot => snapshot.snapshot_time,
'VM-snapshot': snapshot => snapshot.snapshot_time
'VM-snapshot': snapshot => snapshot.snapshot_time,
}
const defaultIteratees = [
object => object.$pool,
object => object.name_label
]
const defaultIteratees = [object => object.$pool, object => object.name_label]
const getIteratees = type => iterateesByType[type] || defaultIteratees
const ordersByType = {
message: 'desc',
'VDI-snapshot': 'desc',
'VM-snapshot': 'desc'
'VM-snapshot': 'desc',
}
const getOrders = type => ordersByType[type]
const autoSelector = (type, fn) => isFunction(type)
? (state, props) => fn(type(state, props))
: [ fn(type) ]
const autoSelector = (type, fn) =>
isFunction(type) ? (state, props) => fn(type(state, props)) : [fn(type)]
return (type, collection) => createSort(
collection,
autoSelector(type, getIteratees),
autoSelector(type, getOrders),
)
return (type, collection) =>
createSort(
collection,
autoSelector(type, getIteratees),
autoSelector(type, getOrders)
)
})
// Add utility methods to a collection selector.
@@ -345,17 +366,17 @@ const _extendCollectionSelector = (selector, objectsType) => {
// count, groupBy and sort can be chained.
const _addFilter = selector => {
selector.filter = predicate => _addCount(_addGroupBy(_addSort(
createFilter(selector, predicate)
)))
selector.filter = predicate =>
_addCount(_addGroupBy(_addSort(createFilter(selector, predicate))))
return selector
}
_addFilter(selector)
// filter, groupBy and sort can be chained.
selector.pick = idsSelector => _addFind(_addFilter(_addGroupBy(_addSort(
createPicker(selector, idsSelector)
))))
selector.pick = idsSelector =>
_addFind(
_addFilter(_addGroupBy(_addSort(createPicker(selector, idsSelector))))
)
return selector
}
@@ -381,10 +402,10 @@ export const createGetObjectsOfType = type => {
? (state, props) => state.objects.byType[type(state, props)] || EMPTY_OBJECT
: state => state.objects.byType[type] || EMPTY_OBJECT
return _extendCollectionSelector(createFilter(
getObjects,
_getPermissionsPredicate
), type)
return _extendCollectionSelector(
createFilter(getObjects, _getPermissionsPredicate),
type
)
}
export const createGetTags = collectionSelectors => {
@@ -392,34 +413,56 @@ export const createGetTags = collectionSelectors => {
collectionSelectors = [
createGetObjectsOfType('host'),
createGetObjectsOfType('pool'),
createGetObjectsOfType('VM')
createGetObjectsOfType('VM'),
]
}
const getTags = create(
collectionSelectors,
(...collections) => {
const tags = {}
const getTags = create(collectionSelectors, (...collections) => {
const tags = {}
const addTag = tag => { tags[tag] = null }
const addItemTags = item => { forEach(item.tags, addTag) }
const addCollectionTags = collection => { forEach(collection, addItemTags) }
forEach(collections, addCollectionTags)
return keys(tags)
const addTag = tag => {
tags[tag] = null
}
)
const addItemTags = item => {
forEach(item.tags, addTag)
}
const addCollectionTags = collection => {
forEach(collection, addItemTags)
}
forEach(collections, addCollectionTags)
return keys(tags)
})
return _extendCollectionSelector(getTags, 'tag')
}
export const createGetVmLastShutdownTime = (
getVmId = (_, { vm }) => (vm != null ? vm.id : undefined)
) =>
create(getVmId, createGetObjectsOfType('message'), (vmId, messages) => {
let max = null
forEach(messages, message => {
if (
message.$object === vmId &&
message.name === 'VM_SHUTDOWN' &&
(max === null || message.time > max)
) {
max = message.time
}
})
return max
})
export const createGetObjectMessages = objectSelector =>
createGetObjectsOfType('message').filter(
create(
(...args) => objectSelector(...args).id,
id => message => message.$object === id
createGetObjectsOfType('message')
.filter(
create(
(...args) => objectSelector(...args).id,
id => message => message.$object === id
)
)
).sort()
.sort()
// Example of use:
// import store from 'store'
@@ -428,40 +471,53 @@ export const createGetObjectMessages = objectSelector =>
export const getObject = createGetObject((_, id) => id)
export const createDoesHostNeedRestart = hostSelector => {
// Returns the first patch of the host which requires it to be
// restarted.
const restartPoolPatch = createGetObjectsOfType('pool_patch').pick(
create(
createGetObjectsOfType('host_patch').pick(
(state, props) => {
const host = hostSelector(state, props)
return host && host.patches
}
).filter(create(
(state, props) => {
const host = hostSelector(state, props)
return host && host.startTime
},
startTime => patch => patch.time > startTime
)),
hostPatches => map(hostPatches, hostPatch => hostPatch.pool_patch)
// XS < 7.1
const patchRequiresReboot = createGetObjectsOfType('pool_patch')
.pick(
// Returns the first patch of the host which requires it to be
// restarted.
create(
createGetObjectsOfType('host_patch')
.pick((state, props) => {
const host = hostSelector(state, props)
return host && host.patches
})
.filter(
create(
(state, props) => {
const host = hostSelector(state, props)
return host && host.startTime
},
startTime => patch => patch.time > startTime
)
),
hostPatches => map(hostPatches, hostPatch => hostPatch.pool_patch)
)
)
).find([ ({ guidance }) => find(guidance, action =>
action === 'restartHost' || action === 'restartXapi'
) ])
.find([
({ guidance }) =>
find(
guidance,
action => action === 'restartHost' || action === 'restartXapi'
),
])
return (state, props) => restartPoolPatch(state, props) !== undefined
return create(
hostSelector,
(...args) => args,
(host, args) => host.rebootRequired || !!patchRequiresReboot(...args)
)
}
export const createGetHostMetrics = hostSelector => _createCollectionWrapper(
export const createGetHostMetrics = hostSelector =>
create(
hostSelector,
hosts => {
_createCollectionWrapper(hosts => {
const metrics = {
count: 0,
cpus: 0,
memoryTotal: 0,
memoryUsage: 0
memoryUsage: 0,
}
forEach(hosts, host => {
metrics.count++
@@ -470,6 +526,17 @@ export const createGetHostMetrics = hostSelector => _createCollectionWrapper(
metrics.memoryUsage += host.memory.usage
})
return metrics
}
})
)
export const createGetVmDisks = vmSelector =>
createGetObjectsOfType('VDI').pick(
create(
createGetObjectsOfType('VBD').pick(
(state, props) => vmSelector(state, props).$VBDs
),
_createCollectionWrapper(vbds =>
map(vbds, vbd => (vbd.is_cd_drive ? undefined : vbd.VDI))
)
)
)
)

View File

@@ -1,3 +1,5 @@
import kindOf from 'kindof'
// Tests that two collections (arrays or objects) have strictly equals
// values (items or properties)
const shallowEqual = (c1, c2) => {
@@ -5,8 +7,8 @@ const shallowEqual = (c1, c2) => {
return true
}
const type = typeof c1
if (type !== typeof c2) {
const type = kindOf(c1)
if (type !== kindOf(c2)) {
return false
}
@@ -25,8 +27,13 @@ const shallowEqual = (c1, c2) => {
return true
}
if (type !== 'object') {
return false
}
let n = 0
for (const _ in c2) { // eslint-disable-line no-unused-vars
// eslint-disable-next-line no-unused-vars
for (const _ in c2) {
++n
}

View File

@@ -1,19 +1,18 @@
import React, { cloneElement } from 'react'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
const SINGLE_LINE_STYLE = { display: 'flex' }
const COL_STYLE = { marginTop: 'auto', marginBottom: 'auto' }
const SingleLineRow = propTypes({
className: propTypes.string
})(({
children,
className
}) => <div
className={`${className || ''} row`}
style={SINGLE_LINE_STYLE}
>
{React.Children.map(children, child => child && cloneElement(child, { style: COL_STYLE }))}
</div>)
className: propTypes.string,
})(({ children, className }) => (
<div className={`${className || ''} row`} style={SINGLE_LINE_STYLE}>
{React.Children.map(
children,
child => child && cloneElement(child, { style: COL_STYLE })
)}
</div>
))
export { SingleLineRow as default }

View File

@@ -10,3 +10,8 @@
.clickableRow {
cursor: pointer;
}
.highlight {
outline: 2px solid #366e98;
outline-offset: -2px;
}

View File

@@ -1,30 +1,42 @@
import * as CM from 'complex-matcher'
import _ from 'intl'
import ceil from 'lodash/ceil'
import classNames from 'classnames'
import debounce from 'lodash/debounce'
import findIndex from 'lodash/findIndex'
import isEmpty from 'lodash/isEmpty'
import isFunction from 'lodash/isFunction'
import map from 'lodash/map'
import React from 'react'
import { Dropdown, MenuItem, Pagination } from 'react-bootstrap-4/lib'
import DropdownMenu from 'react-bootstrap-4/lib/DropdownMenu' // https://phabricator.babeljs.io/T6662 so Dropdown.Menu won't work like https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
import DropdownToggle from 'react-bootstrap-4/lib/DropdownToggle' // https://phabricator.babeljs.io/T6662 so Dropdown.Toggle won't work https://react-bootstrap.github.io/components.html#btn-dropdowns-custom
import React from 'react'
import Shortcuts from 'shortcuts'
import { Portal } from 'react-overlays'
import { routerShape } from 'react-router/lib/PropTypes'
import { Set } from 'immutable'
import { Dropdown, MenuItem, Pagination } from 'react-bootstrap-4/lib'
import {
ceil,
filter,
findIndex,
forEach,
isEmpty,
isFunction,
map,
} from 'lodash'
import ActionRowButton from '../action-row-button'
import Button from '../button'
import ButtonGroup from '../button-group'
import Component from '../base-component'
import defined, { get } from '../xo-defined'
import Icon from '../icon'
import propTypes from '../prop-types'
import propTypes from '../prop-types-decorator'
import SingleLineRow from '../single-line-row'
import Tooltip from '../tooltip'
import { BlockLink } from '../link'
import { Container, Col } from '../grid'
import { create as createMatcher } from '../complex-matcher'
import { Input as DebouncedInput } from '../debounce-component-decorator'
import {
createCounter,
createFilter,
createPager,
createSelector,
createSort
createSort,
} from '../selectors'
import styles from './index.css'
@@ -33,15 +45,14 @@ import styles from './index.css'
@propTypes({
filters: propTypes.object,
nFilteredItems: propTypes.number.isRequired,
nItems: propTypes.number.isRequired,
onChange: propTypes.func.isRequired
onChange: propTypes.func.isRequired,
value: propTypes.string.isRequired,
})
class TableFilter extends Component {
_cleanFilter = () => this._setFilter('')
_setFilter = filterValue => {
const { filter } = this.refs
const filter = this.refs.filter.getWrappedInstance()
filter.value = filterValue
filter.focus()
this.props.onChange(filterValue)
@@ -51,39 +62,55 @@ class TableFilter extends Component {
this.props.onChange(event.target.value)
}
focus () {
this.refs.filter.getWrappedInstance().focus()
}
render () {
const { props } = this
return (
<div className='input-group'>
<span className='input-group-addon'>{props.nFilteredItems} / {props.nItems}</span>
{isEmpty(props.filters)
? <span className='input-group-addon'><Icon icon='search' /></span>
: <div className='input-group-btn'>
{isEmpty(props.filters) ? (
<span className='input-group-addon'>
<Icon icon='search' />
</span>
) : (
<span className='input-group-btn'>
<Dropdown id='filter'>
<DropdownToggle bsStyle='info'>
<Icon icon='search' />
</DropdownToggle>
<DropdownMenu>
{map(props.filters, (filter, label) =>
{map(props.filters, (filter, label) => (
<MenuItem key={label} onClick={() => this._setFilter(filter)}>
{_(label)}
</MenuItem>
)}
))}
</DropdownMenu>
</Dropdown>
</div>}
<input
type='text'
ref='filter'
onChange={this._onChange}
</span>
)}
<DebouncedInput
className='form-control'
onChange={this._onChange}
ref='filter'
value={props.value}
/>
<div className='input-group-btn'>
<button className='btn btn-secondary' onClick={this._cleanFilter}>
<Tooltip content={_('filterSyntaxLinkTooltip')}>
<a
className='input-group-addon'
href='https://xen-orchestra.com/docs/search.html#filter-syntax'
target='_blank'
>
<Icon icon='info' />
</a>
</Tooltip>
<span className='input-group-btn'>
<Button onClick={this._cleanFilter}>
<Icon icon='clear-search' />
</button>
</div>
</Button>
</span>
</div>
)
}
@@ -93,9 +120,9 @@ class TableFilter extends Component {
@propTypes({
columnId: propTypes.number.isRequired,
name: propTypes.any.isRequired,
name: propTypes.node,
sort: propTypes.func,
sortIcon: propTypes.string
sortIcon: propTypes.string,
})
class ColumnHead extends Component {
_sort = () => {
@@ -104,10 +131,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 +142,7 @@ class ColumnHead extends Component {
return (
<th
className={classNames(
textAlign && `text-xs-${textAlign}`,
styles.clickableColumn,
isSelected && classNames('text-white', 'bg-info')
)}
@@ -131,38 +159,136 @@ class ColumnHead extends Component {
// ===================================================================
const DEFAULT_ITEMS_PER_PAGE = 10
@propTypes({
defaultColumn: propTypes.number,
collection: propTypes.oneOfType([
propTypes.array,
propTypes.object
]).isRequired,
columns: propTypes.arrayOf(propTypes.shape({
default: propTypes.bool,
name: propTypes.node.isRequired,
itemRenderer: propTypes.func.isRequired,
sortCriteria: propTypes.oneOfType([
propTypes.func,
propTypes.string
]),
sortOrder: propTypes.string
})).isRequired,
filterContainer: propTypes.func,
filters: propTypes.object,
itemsPerPage: propTypes.number,
paginationContainer: propTypes.func,
rowAction: propTypes.func,
rowLink: propTypes.oneOfType([
propTypes.func,
propTypes.string
]),
userData: propTypes.any
indeterminate: propTypes.bool.isRequired,
})
class Checkbox extends Component {
componentDidUpdate () {
const { props: { indeterminate }, ref } = this
if (ref !== null) {
ref.indeterminate = indeterminate
}
}
_ref = ref => {
this.ref = ref
this.componentDidUpdate()
}
render () {
const { indeterminate, ...props } = this.props
props.ref = this._ref
props.type = 'checkbox'
return <input {...props} />
}
}
// ===================================================================
const actionsShape = propTypes.arrayOf(
propTypes.shape({
// groupedActions: the function will be called with an array of the selected items in parameters
// individualActions: the function will be called with the related item in parameters
disabled: propTypes.oneOfType([propTypes.bool, propTypes.func]),
handler: propTypes.func.isRequired,
icon: propTypes.string.isRequired,
label: propTypes.node.isRequired,
level: propTypes.oneOf(['primary', 'warning', 'danger']),
})
)
class IndividualAction extends Component {
_getIsDisabled = createSelector(
() => this.props.disabled,
() => this.props.item,
() => this.props.userData,
(disabled, item, userData) =>
isFunction(disabled) ? disabled(item, userData) : disabled
)
render () {
const { icon, label, level, handler, item } = this.props
return (
<ActionRowButton
btnStyle={level}
disabled={this._getIsDisabled()}
handler={handler}
handlerParam={item}
icon={icon}
tooltip={label}
/>
)
}
}
class GroupedAction extends Component {
_getIsDisabled = createSelector(
() => this.props.disabled,
() => this.props.selectedItems,
() => this.props.userData,
(disabled, selectedItems, userData) =>
isFunction(disabled) ? disabled(selectedItems, userData) : disabled
)
render () {
const { icon, label, level, handler, selectedItems } = this.props
return (
<ActionRowButton
btnStyle={level}
disabled={this._getIsDisabled()}
handler={handler}
handlerParam={selectedItems}
icon={icon}
tooltip={label}
/>
)
}
}
@propTypes(
{
defaultColumn: propTypes.number,
defaultFilter: propTypes.string,
collection: propTypes.oneOfType([propTypes.array, propTypes.object])
.isRequired,
columns: propTypes.arrayOf(
propTypes.shape({
component: propTypes.func,
default: propTypes.bool,
name: propTypes.node,
itemRenderer: propTypes.func,
sortCriteria: propTypes.oneOfType([propTypes.func, propTypes.string]),
sortOrder: propTypes.string,
textAlign: propTypes.string,
})
).isRequired,
filterContainer: propTypes.func,
filters: propTypes.object,
groupedActions: actionsShape,
individualActions: actionsShape,
itemsPerPage: propTypes.number,
paginationContainer: propTypes.func,
rowAction: propTypes.func,
rowLink: propTypes.oneOfType([propTypes.func, propTypes.string]),
// DOM node selector like body or .my-class
// The shortcuts will be enabled when the node is focused
shortcutsTarget: propTypes.string,
stateUrlParam: propTypes.string,
userData: propTypes.any,
},
{
router: routerShape,
}
)
export default class SortedTable extends Component {
constructor (props) {
super(props)
static defaultProps = {
itemsPerPage: 10,
}
constructor (props, context) {
super(props, context)
let selectedColumn = props.defaultColumn
if (selectedColumn == null) {
@@ -173,53 +299,144 @@ export default class SortedTable extends Component {
}
}
this.state = {
const state = (this.state = {
all: false, // whether all items are selected (accross pages)
filter: defined(() => props.filters[props.defaultFilter], ''),
page: 1,
selectedColumn,
itemsPerPage: props.itemsPerPage || DEFAULT_ITEMS_PER_PAGE
sortOrder:
props.columns[selectedColumn].sortOrder === 'desc' ? 'desc' : 'asc',
})
const urlState = get(
() => context.router.location.query[props.stateUrlParam]
)
if (urlState !== undefined) {
const i = urlState.indexOf('-')
if (i === -1) {
state.filter = urlState
} else {
state.filter = urlState.slice(i + 1)
state.page = +urlState.slice(0, i)
}
}
this._getSelectedColumn = () =>
this.props.columns[this.state.selectedColumn]
this._getTotalNumberOfItems = createCounter(
() => this.props.collection
)
this._getTotalNumberOfItems = createCounter(() => this.props.collection)
this._getAllItems = createSort(
const createMatcher = str => CM.parse(str).createPredicate()
this._getItems = createSort(
createFilter(
() => this.props.collection,
createSelector(
() => this.state.filter || '',
createMatcher
)
createSelector(() => this.state.filter, createMatcher)
),
createSelector(
() => this._getSelectedColumn().sortCriteria,
() => this.props.userData,
(sortCriteria, userData) =>
(typeof sortCriteria === 'function')
typeof sortCriteria === 'function'
? object => sortCriteria(object, userData)
: sortCriteria
),
() => this.state.sortOrder
)
this.state.activePage = 1
this._getVisibleItems = createPager(
this._getAllItems,
() => this.state.activePage,
this.state.itemsPerPage
this._getItems,
() => this.state.page,
() => this.props.itemsPerPage
)
state.selectedItemsIds = new Set()
this._getSelectedItems = createSelector(
() => this.state.all,
() => this.state.selectedItemsIds,
this._getItems,
(all, selectedItemsIds, items) =>
all ? items : filter(items, item => selectedItemsIds.has(item.id))
)
this._hasGroupedActions = createSelector(
() => this.props.groupedActions,
actions => !isEmpty(actions)
)
this._getShortcutsHandler = createSelector(
this._getVisibleItems,
this._hasGroupedActions,
() => this.state.highlighted,
() => this.props.rowLink,
() => this.props.rowAction,
() => this.props.userData,
(
visibleItems,
hasGroupedActions,
itemIndex,
rowLink,
rowAction,
userData
) => (command, event) => {
event.preventDefault()
const item =
itemIndex !== undefined ? visibleItems[itemIndex] : undefined
switch (command) {
case 'SEARCH':
this.refs.filterInput.focus()
break
case 'NAV_DOWN':
if (
hasGroupedActions ||
rowAction !== undefined ||
rowLink !== undefined
) {
this.setState({
highlighted:
(itemIndex + visibleItems.length + 1) % visibleItems.length ||
0,
})
}
break
case 'NAV_UP':
if (
hasGroupedActions ||
rowAction !== undefined ||
rowLink !== undefined
) {
this.setState({
highlighted:
(itemIndex + visibleItems.length - 1) % visibleItems.length ||
0,
})
}
break
case 'SELECT':
if (itemIndex !== undefined && hasGroupedActions) {
this._selectItem(itemIndex)
}
break
case 'ROW_ACTION':
if (item !== undefined) {
if (rowLink !== undefined) {
this.context.router.push(
isFunction(rowLink) ? rowLink(item, userData) : rowLink
)
} else if (rowAction !== undefined) {
rowAction(item, userData)
}
}
break
}
}
)
}
componentWillMount () {
this.setState({
sortOrder: this.props.columns[this.state.selectedColumn].sortOrder === 'desc' ? 'desc' : 'asc'
})
}
componentDidMount () {
this._checkUpdatePage()
// Force one Portal refresh.
// Because Portal cannot see the container reference at first rendering.
if (this.props.paginationContainer) {
@@ -232,132 +449,419 @@ export default class SortedTable extends Component {
let sortOrder
if (state.selectedColumn === columnId) {
sortOrder = state.sortOrder === 'desc'
? 'asc'
: 'desc'
sortOrder = state.sortOrder === 'desc' ? 'asc' : 'desc'
} else {
sortOrder = this.props.columns[columnId].sortOrder === 'desc'
? 'desc'
: 'asc'
sortOrder =
this.props.columns[columnId].sortOrder === 'desc' ? 'desc' : 'asc'
}
this.setState({
selectedColumn: columnId,
sortOrder
sortOrder,
})
}
_onPageSelection = (_, event) => this.setState({
activePage: event.eventKey
})
componentDidUpdate () {
const { selectedItemsIds } = this.state
_onFilterChange = debounce(filter => {
// Unselect items that are no longer visible
if (
(this._visibleItemsRecomputations || 0) <
(this._visibleItemsRecomputations = this._getVisibleItems.recomputations())
) {
const newSelectedItems = selectedItemsIds.intersect(
map(this._getVisibleItems(), 'id')
)
if (newSelectedItems.size < selectedItemsIds.size) {
this.setState({ selectedItemsIds: newSelectedItems })
}
}
this._checkUpdatePage()
}
_saveUrlState (filter, page) {
const { stateUrlParam } = this.props
if (stateUrlParam === undefined) {
return
}
const { router } = this.context
const { location } = router
router.replace({
...location,
query: {
...location.query,
[stateUrlParam]: `${page}-${filter}`,
},
})
}
_setFilter = filter => {
this._saveUrlState(filter, 1)
this.setState({
filter,
activePage: 1
page: 1,
highlighted: undefined,
})
}, 500)
}
_checkUpdatePage () {
const { page } = this.state
if (page === 1) {
return
}
const n = this._getItems().length
const { itemsPerPage } = this.props
if (n < itemsPerPage) {
return this._setPage(1)
}
if (page * itemsPerPage > n) {
return this._setPage(ceil(n / itemsPerPage))
}
}
_setPage (page) {
this._saveUrlState(this.state.filter, page)
this.setState({ page })
}
_onPageSelection = (_, event) => this._setPage(event.eventKey)
_selectAllVisibleItems = event => {
this.setState({
all: false,
selectedItemsIds: event.target.checked
? this.state.selectedItemsIds.union(map(this._getVisibleItems(), 'id'))
: this.state.selectedItemsIds.clear(),
})
}
// TODO: figure out why it's necessary
_toggleNestedCheckboxGuard = false
_toggleNestedCheckbox = event => {
const child = event.target.firstElementChild
if (child != null && child.tagName === 'INPUT') {
if (this._toggleNestedCheckboxGuard) {
return
}
this._toggleNestedCheckboxGuard = true
child.dispatchEvent(new window.MouseEvent('click', event.nativeEvent))
this._toggleNestedCheckboxGuard = false
}
}
_selectAll = () => this.setState({ all: true })
_selectItem (current, selected, range = false) {
const { all, selectedItemsIds } = this.state
const visibleItems = this._getVisibleItems()
const item = visibleItems[current]
if (all) {
return this.setState({
all: false,
selectedItemsIds: new Set().withMutations(selectedItemsIds => {
forEach(visibleItems, item => {
selectedItemsIds.add(item.id)
})
selectedItemsIds.delete(item.id)
}),
})
}
const method = (selected === undefined
? !selectedItemsIds.has(item.id)
: selected)
? 'add'
: 'delete'
let previous
this.setState({
selectedItemsIds:
range && (previous = this._previous) !== undefined
? selectedItemsIds.withMutations(selectedItemsIds => {
let i = previous
let end = current
if (previous > current) {
i = current
end = previous
}
for (; i <= end; ++i) {
selectedItemsIds[method](visibleItems[i].id)
}
})
: selectedItemsIds[method](item.id),
})
this._previous = current
}
_onSelectItemCheckbox = event => {
const { target } = event
this._selectItem(+target.name, target.checked, event.nativeEvent.shiftKey)
}
_renderItem = (item, i) => {
const { props, state } = this
const { individualActions, rowAction, rowLink, userData } = props
const hasGroupedActions = this._hasGroupedActions()
const hasIndividualActions = !isEmpty(individualActions)
const columns = map(
props.columns,
({ component: Component, itemRenderer, textAlign }, key) => (
<td className={textAlign && `text-xs-${textAlign}`} key={key}>
{Component !== undefined ? (
<Component item={item} userData={userData} />
) : (
itemRenderer(item, userData)
)}
</td>
)
)
const { id = i } = item
const selectionColumn = hasGroupedActions && (
<td className='text-xs-center' onClick={this._toggleNestedCheckbox}>
<input
checked={state.all || state.selectedItemsIds.has(id)}
name={i} // position in visible items
onChange={this._onSelectItemCheckbox}
type='checkbox'
/>
</td>
)
const actionsColumn = hasIndividualActions && (
<td>
<div className='pull-right'>
<ButtonGroup>
{map(individualActions, (props, key) => (
<IndividualAction
{...props}
item={item}
key={key}
userData={userData}
/>
))}
</ButtonGroup>
</div>
</td>
)
return rowLink != null ? (
<BlockLink
className={state.highlighted === i ? styles.highlight : undefined}
key={id}
tagName='tr'
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
>
{selectionColumn}
{columns}
{actionsColumn}
</BlockLink>
) : (
<tr
className={classNames(
rowAction && styles.clickableRow,
state.highlighted === i && styles.highlight
)}
key={id}
onClick={rowAction && (() => rowAction(item, userData))}
>
{selectionColumn}
{columns}
{actionsColumn}
</tr>
)
}
render () {
const { props, state } = this
const {
paginationContainer,
filterContainer,
filters,
rowAction,
rowLink,
userData
groupedActions,
itemsPerPage,
paginationContainer,
shortcutsTarget,
userData,
} = props
const { all } = state
const nFilteredItems = this._getAllItems().length
const nAllItems = this._getTotalNumberOfItems()
const nItems = this._getItems().length
const nSelectedItems = state.selectedItemsIds.size
const nVisibleItems = this._getVisibleItems().length
const paginationInstance = (
const hasGroupedActions = this._hasGroupedActions()
const hasIndividualActions = !isEmpty(props.individualActions)
const nColumns = props.columns.length + (hasIndividualActions ? 2 : 1)
const displayPagination =
paginationContainer === undefined && itemsPerPage < nAllItems
const displayFilter = filterContainer === undefined && nAllItems !== 0
const paginationInstance = displayPagination && (
<Pagination
first
last
prev
next
ellipsis
boundaryLinks
maxButtons={10}
items={ceil(nFilteredItems / state.itemsPerPage)}
activePage={this.state.activePage}
maxButtons={7}
items={ceil(nItems / itemsPerPage)}
activePage={state.page}
onSelect={this._onPageSelection}
/>
)
const filterInstance = (
const filterInstance = displayFilter && (
<TableFilter
filters={filters}
nFilteredItems={nFilteredItems}
nItems={this._getTotalNumberOfItems()}
onChange={this._onFilterChange}
filters={props.filters}
onChange={this._setFilter}
ref='filterInput'
value={state.filter}
/>
)
return (
<div>
{shortcutsTarget !== undefined && (
<Shortcuts
handler={this._getShortcutsHandler()}
name='SortedTable'
stopPropagation
targetNodeSelector={shortcutsTarget}
/>
)}
<table className='table'>
<thead className='thead-default'>
<tr>
<th colSpan={nColumns}>
{nItems === nAllItems
? _('sortedTableNumberOfItems', { nTotal: nItems })
: _('sortedTableNumberOfFilteredItems', {
nFiltered: nItems,
nTotal: nAllItems,
})}
{all ? (
<span>
{' '}
-{' '}
<span className='text-danger'>
{_('sortedTableAllItemsSelected')}
</span>
</span>
) : (
nSelectedItems !== 0 && (
<span>
{' '}
-{' '}
{_('sortedTableNumberOfSelectedItems', {
nSelected: nSelectedItems,
})}
{nSelectedItems === nVisibleItems &&
nSelectedItems < nItems && (
<Button
btnStyle='info'
className='ml-1'
onClick={this._selectAll}
size='small'
>
{_('sortedTableSelectAllItems')}
</Button>
)}
</span>
)
)}
{nSelectedItems !== 0 && (
<div className='pull-right'>
<ButtonGroup>
{map(groupedActions, (props, key) => (
<GroupedAction
{...props}
key={key}
selectedItems={this._getSelectedItems()}
userData={userData}
/>
))}
</ButtonGroup>
</div>
)}
</th>
</tr>
<tr>
{hasGroupedActions && (
<th
className='text-xs-center'
onClick={this._toggleNestedCheckbox}
>
<Checkbox
onChange={this._selectAllVisibleItems}
checked={all || nSelectedItems !== 0}
indeterminate={
!all &&
nSelectedItems !== 0 &&
nSelectedItems !== nVisibleItems
}
/>
</th>
)}
{map(props.columns, (column, key) => (
<ColumnHead
textAlign={column.textAlign}
columnId={key}
key={key}
name={column.name}
sort={column.sortCriteria && this._sort}
sortIcon={state.selectedColumn === key ? state.sortOrder : 'sort'}
/>
sortIcon={
state.selectedColumn === key ? state.sortOrder : 'sort'
}
/>
))}
{hasIndividualActions && <th />}
</tr>
</thead>
<tbody>
{map(this._getVisibleItems(), (item, i) => {
const columns = map(props.columns, (column, key) => (
<td key={key}>
{column.itemRenderer(item, userData)}
{nVisibleItems !== 0 ? (
map(this._getVisibleItems(), this._renderItem)
) : (
<tr>
<td className='text-info text-xs-center' colSpan={nColumns}>
{_('sortedTableNoItems')}
</td>
))
const { id = i } = item
return rowLink
? <BlockLink
key={id}
tagName='tr'
to={isFunction(rowLink) ? rowLink(item, userData) : rowLink}
>{columns}</BlockLink>
: <tr
className={rowAction && styles.clickableRow}
key={id}
onClick={rowAction && (() => rowAction(item, userData))}
>
{columns}
</tr>
})}
</tr>
)}
</tbody>
</table>
{(!paginationContainer || !filterContainer) && (
{(displayFilter || displayPagination) && (
<Container>
<SingleLineRow>
<Col mediumSize={8}>
{paginationContainer
? (
{displayPagination &&
(paginationContainer !== undefined ? (
// Rebuild container function to refresh Portal component.
<Portal container={() => paginationContainer()}>
{paginationInstance}
</Portal>
) : paginationInstance
}
) : (
paginationInstance
))}
</Col>
<Col mediumSize={4}>
{filterContainer
? (
{displayFilter &&
(filterContainer ? (
<Portal container={() => filterContainer()}>
{filterInstance}
</Portal>
) : filterInstance
}
) : (
filterInstance
))}
</Col>
</SingleLineRow>
</Container>

View File

@@ -0,0 +1,46 @@
import React from 'react'
import styled from 'styled-components'
import { omit } from 'lodash'
import ActionButton from './action-button'
import propTypes from './prop-types-decorator'
// do not forward `state` to ActionButton
const Button = styled(p => <ActionButton {...omit(p, 'state')} />)`
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,
disabledHandlerParam,
disabledLabel,
disabledTooltip,
enabledLabel,
enabledTooltip,
enabledHandler,
enabledHandlerParam,
state,
...props
}) => (
<Button
handler={state ? enabledHandler : disabledHandler}
handlerParam={state ? enabledHandlerParam : disabledHandlerParam}
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,24 @@
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)),
return action
})({ type }),
'toString',
{ value: () => type }
)
}
})()
// ===================================================================
@@ -40,7 +31,10 @@ export const connected = createAction('CONNECTED')
export const disconnected = createAction('DISCONNECTED')
export const updateObjects = createAction('UPDATE_OBJECTS', updates => updates)
export const updatePermissions = createAction('UPDATE_PERMISSIONS', permissions => permissions)
export const updatePermissions = createAction(
'UPDATE_PERMISSIONS',
permissions => permissions
)
export const signedIn = createAction('SIGNED_IN', user => user)
export const signedOut = createAction('SIGNED_OUT')
@@ -48,5 +42,11 @@ export const signedOut = createAction('SIGNED_OUT')
export const xoaUpdaterState = createAction('XOA_UPDATER_STATE', state => state)
export const xoaTrialState = createAction('XOA_TRIAL_STATE', state => state)
export const xoaUpdaterLog = createAction('XOA_UPDATER_LOG', log => log)
export const xoaRegisterState = createAction('XOA_REGISTER_STATE', registration => registration)
export const xoaConfiguration = createAction('XOA_CONFIGURATION', configuration => configuration)
export const xoaRegisterState = createAction(
'XOA_REGISTER_STATE',
registration => registration
)
export const xoaConfiguration = createAction(
'XOA_CONFIGURATION',
configuration => configuration
)

View File

@@ -4,10 +4,7 @@ import React from 'react'
import { createDevTools } from 'redux-devtools'
export default createDevTools(
<DockMonitor
changePositionKey='ctrl-q'
toggleVisibilityKey='ctrl-h'
>
<DockMonitor changePositionKey='ctrl-q' toggleVisibilityKey='ctrl-h'>
<LogMonitor />
</DockMonitor>
)

View File

@@ -1,10 +1,5 @@
import reduxThunk from 'redux-thunk'
import {
applyMiddleware,
combineReducers,
compose,
createStore
} from 'redux'
import { applyMiddleware, combineReducers, compose, createStore } from 'redux'
import { connectStore as connectXo } from '../xo'
@@ -13,9 +8,7 @@ import reducer from './reducer'
// ===================================================================
const enhancers = [
applyMiddleware(reduxThunk)
]
const enhancers = [applyMiddleware(reduxThunk)]
DevTools && enhancers.push(DevTools.instrument())
const store = createStore(

View File

@@ -54,19 +54,16 @@ const combineActionHandlers = invoke(
const actionType = firstProp(handlers)
const handler = handlers[actionType]
return (state = initialState, action) => (
return (state = initialState, action) =>
action.type === actionType
? handler(state, action.payload, action)
: state
)
}
return (state = initialState, action) => {
const handler = handlers[action.type]
return handler
? handler(state, action.payload, action)
: state
return handler ? handler(state, action.payload, action) : state
}
}
)
@@ -79,74 +76,91 @@ export default {
cookies.set('lang', lang)
return lang
},
}),
permissions: combineActionHandlers(
{},
{
[actions.updatePermissions]: (_, permissions) => permissions,
}
}),
),
permissions: combineActionHandlers({}, {
[actions.updatePermissions]: (_, permissions) => permissions
}),
objects: combineActionHandlers(
{
all: {}, // Mutable for performance!
byType: {},
},
{
[actions.updateObjects]: ({ all, byType: prevByType }, updates) => {
const byType = { ...prevByType }
const get = type => {
const curr = byType[type]
const prev = prevByType[type]
return curr === prev ? (byType[type] = { ...prev }) : curr
}
objects: combineActionHandlers({
all: {}, // Mutable for performance!
byType: {}
}, {
[actions.updateObjects]: ({ all, byType: prevByType }, updates) => {
const byType = { ...prevByType }
const get = type => {
const curr = byType[type]
const prev = prevByType[type]
return curr === prev
? (byType[type] = { ...prev })
: curr
}
for (const id in updates) {
const object = updates[id]
if (object) {
all[id] = object
get(object.type)[id] = object
} else {
for (const id in updates) {
const object = updates[id]
const previous = all[id]
if (previous) {
if (object) {
const { type } = object
all[id] = object
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]
}
}
}
return { all, byType, fetched: true }
return { all, byType, fetched: true }
},
}
}),
),
user: combineActionHandlers(null, {
[actions.signedIn]: {
next: (_, user) => user
}
next: (_, user) => user,
},
}),
status: combineActionHandlers('disconnected', {
[actions.connected]: () => 'connected',
[actions.disconnected]: () => 'disconnected'
[actions.disconnected]: () => 'disconnected',
}),
xoaUpdaterState: combineActionHandlers('disconnected', {
[actions.xoaUpdaterState]: (_, state) => state
[actions.xoaUpdaterState]: (_, state) => state,
}),
xoaTrialState: combineActionHandlers({}, {
[actions.xoaTrialState]: (_, state) => state
}),
xoaUpdaterLog: combineActionHandlers([], {
[actions.xoaUpdaterLog]: (_, log) => log
}),
xoaRegisterState: combineActionHandlers({state: '?'}, {
[actions.xoaRegisterState]: (_, registration) => registration
}),
xoaConfiguration: combineActionHandlers({proxyHost: '', proxyPort: '', proxyUser: ''}, { // defined values for controlled inputs
[actions.xoaConfiguration]: (_, configuration) => {
delete configuration.password
return configuration
xoaTrialState: combineActionHandlers(
{},
{
[actions.xoaTrialState]: (_, state) => state,
}
})
),
xoaUpdaterLog: combineActionHandlers([], {
[actions.xoaUpdaterLog]: (_, log) => log,
}),
xoaRegisterState: combineActionHandlers(
{ state: '?' },
{
[actions.xoaRegisterState]: (_, registration) => registration,
}
),
xoaConfiguration: combineActionHandlers(
{ proxyHost: '', proxyPort: '', proxyUser: '' },
{
// defined values for controlled inputs
[actions.xoaConfiguration]: (_, configuration) => {
delete configuration.password
return configuration
},
}
),
}

View File

@@ -7,36 +7,24 @@ import Link from './link'
const STYLE = {
marginBottom: '1em',
marginLeft: '1em'
marginLeft: '1em',
}
const TabButton = ({
labelId,
...props
}) => (
<ActionButton
{...props}
size='large'
style={STYLE}
><span className='hidden-md-down'>{_(labelId)}</span></ActionButton>
const TabButton = ({ labelId, ...props }) => (
<ActionButton {...props} size='large' style={STYLE}>
{labelId !== undefined && (
<span className='hidden-md-down'>{_(labelId)}</span>
)}
</ActionButton>
)
export { TabButton as default }
export const TabButtonLink = ({
labelId,
icon,
...props
}) => (
<Link
{...props}
className='btn btn-lg btn-primary'
style={STYLE}
>
export const TabButtonLink = ({ labelId, icon, ...props }) => (
<Link {...props} className='btn btn-lg btn-primary' style={STYLE}>
<span className='hidden-md-down'>
{icon && (
<span>
<Icon icon={icon} />
{' '}
<Icon icon={icon} />{' '}
</span>
)}
{_(labelId)}

View File

@@ -5,11 +5,11 @@ import React from 'react'
import Component from './base-component'
import Icon from './icon'
import propTypes from './prop-types'
import propTypes from './prop-types-decorator'
const INPUT_STYLE = {
margin: '2px',
maxWidth: '4em'
maxWidth: '4em',
}
const TAG_STYLE = {
backgroundColor: '#2598d9',
@@ -19,26 +19,30 @@ const TAG_STYLE = {
margin: '0.2em',
marginTop: '-0.1em',
padding: '0.3em',
verticalAlign: 'middle'
verticalAlign: 'middle',
}
const LINK_STYLE = {
cursor: 'pointer',
}
const ADD_TAG_STYLE = {
cursor: 'pointer',
fontSize: '0.8em',
marginLeft: '0.2em'
marginLeft: '0.2em',
}
const REMOVE_TAG_STYLE = {
cursor: 'pointer'
cursor: 'pointer',
}
@propTypes({
labels: propTypes.arrayOf(React.PropTypes.string).isRequired,
onAdd: propTypes.func,
onChange: propTypes.func,
onClick: propTypes.func,
onDelete: propTypes.func,
onAdd: propTypes.func
})
export default class Tags extends Component {
componentWillMount () {
this.setState({editing: false})
this.setState({ editing: false })
}
_startEdit = () => {
@@ -53,7 +57,7 @@ export default class Tags extends Component {
if (!includes(labels, newTag)) {
onAdd && onAdd(newTag)
onChange && onChange([ ...labels, newTag ])
onChange && onChange([...labels, newTag])
}
}
_deleteTag = tag => {
@@ -81,29 +85,29 @@ export default class Tags extends Component {
}
render () {
const {
labels,
onAdd,
onChange,
onDelete
} = this.props
const { labels, onAdd, onChange, onClick, onDelete } = this.props
const deleteTag = (onDelete || onChange) && this._deleteTag
return (
<span className='form-group' style={{ color: '#999' }}>
<Icon icon='tags' />
{' '}
<Icon icon='tags' />{' '}
<span>
{map(labels.sort(), (label, index) =>
<Tag label={label} onDelete={deleteTag} key={index} />
)}
{map(labels.sort(), (label, index) => (
<Tag
label={label}
onDelete={deleteTag}
key={index}
onClick={onClick}
/>
))}
</span>
{(onAdd || onChange) && !this.state.editing
? <span onClick={this._startEdit} style={ADD_TAG_STYLE}>
{(onAdd || onChange) && !this.state.editing ? (
<span onClick={this._startEdit} style={ADD_TAG_STYLE}>
<Icon icon='add-tag' />
</span>
: <span>
) : (
<span>
<input
type='text'
autoFocus
@@ -112,23 +116,32 @@ export default class Tags extends Component {
onBlur={this._stopEdit}
/>
</span>
}
)}
</span>
)
}
}
export const Tag = ({ label, onDelete }) => (
export const Tag = ({ type, label, onDelete, onClick }) => (
<span style={TAG_STYLE}>
{label}{' '}
{onDelete
? <span onClick={onDelete && (() => onDelete(label))} style={REMOVE_TAG_STYLE}>
<span
onClick={onClick && (() => onClick(label))}
style={onClick && LINK_STYLE}
>
{label}
</span>{' '}
{onDelete ? (
<span
onClick={onDelete && (() => onDelete(label))}
style={REMOVE_TAG_STYLE}
>
<Icon icon='remove-tag' />
</span>
: []
}
) : (
[]
)}
</span>
)
Tag.propTypes = {
label: React.PropTypes.string.isRequired
label: React.PropTypes.string.isRequired,
}

View File

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