Compare commits

...

787 Commits

Author SHA1 Message Date
Julien Fontanet
595c4bd5a8 feat(computed): decorator for computed props 2018-01-24 14:35:00 +01:00
badrAZ
1a2f553094 fix(vm): hide cores per socket selector when no container (#2221) 2018-01-18 15:35:03 +01:00
badrAZ
4d69866532 feat(migrate-vm-modal): controlled form (#2259) 2018-01-18 10:40:07 +01:00
Pierre Donias
495c97b44b fix(xo): links to /xoa/update & www-xo forum (#2571) 2018-01-16 16:53:59 +01:00
Pierre Donias
e817b3254e fix(vm/network): duplicate key error (#2570)
Fixes #2553
2018-01-16 11:41:27 +01:00
Julien Fontanet
dd6987efe9 fix(SortedTable): infinite loop when displaying last page (#2568)
Fixes #2569
2018-01-15 16:56:00 +01:00
Julien Fontanet
d7f8d12d88 chore(prop-types-decorator): deprecate 2018-01-15 12:48:22 +01:00
Julien Fontanet
504895a730 chore(package): update dependencies 2018-01-15 12:37:00 +01:00
Rajaa.BARHTAOUI
cde92836f3 feat(user/ssh): use SortedTable (#2514)
See #2416
2018-01-12 14:56:47 +01:00
Rajaa.BARHTAOUI
c787988b06 feat(pool,vm/logs): use SortedTable (#2513) 2018-01-11 14:51:59 +01:00
Julien Fontanet
898434b267 Revert "chore(package): use React 16 (#2552)"
This reverts commit c8669dc88f.

Let's go back to React 15 until we move away from react-bootstrap.
2018-01-09 14:21:03 +01:00
Julien Fontanet
6e44c65a07 chore: fixes various warnings (#2554) 2018-01-08 15:18:15 +01:00
Julien Fontanet
03028bca50 chore(Pagination): own implementation instead of react-bootstrap (#2549) 2018-01-08 14:43:50 +01:00
Julien Fontanet
c8669dc88f chore(package): use React 16 (#2552) 2018-01-08 09:46:08 +01:00
Julien Fontanet
82240979c2 chore(react-select): autofocus → autoFocus (#2550) 2018-01-08 09:40:35 +01:00
Julien Fontanet
db5d495105 fix(addSubscription): export as default 2018-01-05 15:52:35 +01:00
Julien Fontanet
6e8dfe8833 chore(utils): remove unused checkPropsState 2018-01-05 15:48:21 +01:00
Julien Fontanet
242d9e20c4 chore(addSubscriptions): move into own module 2018-01-05 15:48:21 +01:00
Julien Fontanet
e446eb0cd0 chore(build): fix PostCSS warning 2018-01-03 18:26:02 +01:00
Julien Fontanet
b63efe579a fix(build): temporary use stage-0
The compiled code has an issue with stage-3 and plugins
2018-01-03 15:50:08 +01:00
Julien Fontanet
f3410f1491 chore(package): remove unused redux devtool 2018-01-03 15:03:09 +01:00
Julien Fontanet
b27ac11d56 chore(jsx): remove unnecessary braces 2018-01-03 10:00:32 +01:00
Julien Fontanet
a55d73614e chore(package): missing peer dependency 2018-01-02 22:29:02 +01:00
Julien Fontanet
25cd1957c7 chore(package): update Babel conf
- preset-es2015 → preset-env
- preset-stage-0 → preset-stage-3 + handpicked plugins
2018-01-02 22:20:55 +01:00
Pierre Donias
abd97abc24 fix(xosan): beta-is-over message (#2546) 2018-01-02 21:50:40 +01:00
Julien Fontanet
6ddfd909f0 chore(package): add clean script 2018-01-02 19:01:33 +01:00
Julien Fontanet
e054eec555 chore(gulpfile): fix sourcemaps 2018-01-02 19:01:33 +01:00
Olivier Lambert
e253657770 feat(home): bulk suspend VMs (#2551)
Fixes #2547
2018-01-02 17:16:11 +01:00
Julien Fontanet
102e629e16 chore(package): update dependencies 2018-01-02 10:04:27 +01:00
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
Julien Fontanet
39342cd662 5.3.1 2016-10-27 18:24:31 +02:00
Olivier Lambert
051a3ac122 feat(changelog): modify changelog for 5.3.1 2016-10-27 18:21:25 +02:00
Julien Fontanet
f842a321ba fix(xo): properly sign out on auth failure (#1712)
Fixes #1711
2016-10-27 18:17:20 +02:00
Julien Fontanet
3cd2dd65d3 chore(xo/importConfig): no need to promisify 2016-10-27 17:52:13 +02:00
Pierre Donias
5ce7e0b108 feat(settings/config): import/export XO configuration (#1703)
See #786
2016-10-27 15:08:45 +02:00
Julien Fontanet
71c2058cc8 fix(package): do not test on Node 7 (#1705)
* fix(package): do not test on Node 7
* fix(package): directly depend on chartist 0.9
* fix(package): directly depend on react-overlays 0.6
2016-10-27 11:57:06 +02:00
Greenkeeper
f200d39d23 chore(package): update modular-css to version 0.28.0 (#1707)
https://greenkeeper.io/
2016-10-26 23:34:32 +02:00
Pierre Donias
7932845ac5 fix(xo/exportVm): window.location instead of window.open() (#1704) 2016-10-26 16:52:26 +02:00
Julien Fontanet
94bda6ac9e chore(intl): update all locales (#1698) 2016-10-25 14:48:53 +02:00
fufroma
7a65f80406 feat(intl/messages): more translations (#1684) 2016-10-25 14:12:11 +02:00
Pierre Donias
36ab58dad9 feat(backup/restore): VM-centered UI (#1697)
Fixes #1609
2016-10-25 12:59:01 +02:00
Julien Fontanet
e9be9e3761 chore(package): update react-router to version 3.0.0 2016-10-25 11:21:46 +02:00
Julien Fontanet
b54645c86c chore(package): update d3 to version 4.2.8 2016-10-25 11:21:11 +02:00
Greenkeeper
ab77d8430c chore(package): update promise-toolbox to version 0.7.0 (#1690)
https://greenkeeper.io/
2016-10-24 15:02:02 +02:00
Pierre Donias
c6f683b532 feat(host,pool): edit PIFs VLAN (#1685)
See #1092
2016-10-24 09:52:47 +02:00
Pierre Donias
a2604f5156 fix(xo-app): ensure there is always a page title (#1687) 2016-10-21 18:10:53 +02:00
Pierre Donias
5ae7f683d6 fix(new-vm): fix getting disks in self service (#1688) 2016-10-21 18:00:38 +02:00
fufroma
f953c89979 fix(new/sr): fix username handling (#1683)
Regression was introduced in e79096626a
2016-10-21 11:56:36 +02:00
Julien Fontanet
bb8aab02ea fix(form/Password): generator in controlled mode
Fixes #1678
2016-10-20 17:58:49 +02:00
Julien Fontanet
af0c03ff6a 5.3.0 2016-10-20 16:09:25 +02:00
Olivier Lambert
8859900537 feat(changelog): update for release 2016-10-20 16:07:25 +02:00
Julien Fontanet
130852ab85 fix(xo): pass the refresh function to tap() 2016-10-20 15:13:55 +02:00
fufroma
65fa8f96b4 feat(intl): minor improvements (#1668) 2016-10-20 14:54:06 +02:00
Pierre Donias
0a84e9e363 feat(host/network): configure IP mode (#1671)
Fixes #1651
2016-10-20 13:32:43 +02:00
Pierre Donias
163c69454b fix(modal): disable shortcuts when a modal is open (#1673)
Fixes #1589
2016-10-20 13:30:29 +02:00
Julien Fontanet
49d3fde0f3 fix(JsonSchemaInput/Array): fix items handling
#1663
2016-10-20 13:22:11 +02:00
Pierre Donias
bb67e2254e fix(package): use new syntax of Boostrap classes (#1667) 2016-10-19 16:05:59 +02:00
Pierre Donias
6d2abc4e74 feat(IP pools): can be used in resource sets (#1662)
Fixes #1565
2016-10-19 11:17:44 +02:00
Julien Fontanet
4875450053 fix(home): correctly set default filter when user is loaded
Fixes #1665
2016-10-18 11:57:55 +02:00
Julien Fontanet
19184ca8a0 chore(base-component): disable verbose logs 2016-10-18 11:18:00 +02:00
Julien Fontanet
654c3d324b chore(package): jade → pug 2016-10-18 11:17:35 +02:00
fufroma
c5b4811f16 feat(intl): various new translations (#1659)
Also: favicon.
2016-10-17 14:43:50 +02:00
Julien Fontanet
7a9dc4fd59 fix(package): minor style issues 2016-10-14 14:13:20 +02:00
fufroma
e79096626a feat(intl): more translatable messages (#1641) 2016-10-14 10:46:46 +02:00
fufroma
332d074d32 fix(intl/locales/fr): various fixes (#1638) 2016-10-13 14:14:42 +02:00
Pierre Donias
e511ecd76e fix(vm/copy): inital value of compress should be false (#1652)
Fixes #1645
2016-10-12 09:06:28 +02:00
Greenkeeper
bcfbd5eba9 chore(package): update promise-toolbox to version 0.6.0 (#1650)
https://greenkeeper.io/
2016-10-11 17:32:23 +02:00
Pierre Donias
9fa3db395b fix(self): SizeInput's empty value should be null (#1649) 2016-10-11 15:01:43 +02:00
Julien Fontanet
52a41ceb04 chore(package): update standard to version 8.4.0 2016-10-11 11:39:27 +02:00
Julien Fontanet
e65d67266d style: indent fixes 2016-10-11 10:21:57 +02:00
Julien Fontanet
0d1045821c chore(intl/locales/zh): update skeleton 2016-10-10 16:02:44 +02:00
Pierre Donias
45d526dda2 fix(select-objects): dynamic option height (#1644)
Fixes #1411
2016-10-10 15:47:46 +02:00
Julien Fontanet
e52f998e78 fix(tools/update-locales): remove incorrect import 2016-10-10 10:15:58 +02:00
Julien Fontanet
42ed3b9355 feat(tools/update-locales): create or update locales (#1632)
Replace the existing `tools/create-locale`.
2016-10-10 09:57:08 +02:00
Julien Fontanet
563b4cb1ec 5.2.5 2016-10-07 15:45:28 +02:00
Olivier Lambert
45bad231cf feat(changelog): add 5.2.4 and 5.2.5 release 2016-10-07 15:44:47 +02:00
Pierre Donias
d76bd2484b fix(console): disable shortcuts when console is focused (#1637)
Fixes #1614
2016-10-07 15:26:20 +02:00
Pierre Donias
445b60bb63 fix(vm/console): initial scale value should be 1 (#1639) 2016-10-07 14:06:16 +02:00
Julien Fontanet
3214e0e41e fix: style & minor issues 2016-10-06 18:28:23 +02:00
Julien Fontanet
c61230e145 fix(intl/locales/fr): remove incorrect entry 2016-10-06 16:13:54 +02:00
fufroma
fac6a29226 feat(intl): new translatable messages (#1627) 2016-10-06 16:05:47 +02:00
Olivier Lambert
7a8f414748 feat(home/host): sparklines in expanded zone (#1619)
Fixes #1634
2016-10-06 15:14:35 +02:00
Julien Fontanet
9f450d282e chore(package): use index-modules 2016-10-06 14:41:46 +02:00
Pierre Donias
31787067e3 feat(new-vm): set dynamic and static memory bounds (#1618)
Fixes #1603
2016-10-05 17:27:59 +02:00
fufroma
1a769b23e2 feat(i18n): update French translation (#1600) 2016-10-05 10:38:12 +02:00
Olivier Lambert
ae002abafc feat(home/pool): bar for pool RAM usage (#1626)
Fixes #1625
2016-10-05 10:26:47 +02:00
Julien Fontanet
31a25d9c16 5.2.4 2016-10-04 15:35:11 +02:00
Julien Fontanet
356295c361 fix(package): add missing make-error 2016-10-04 15:33:24 +02:00
Julien Fontanet
d10681b6d1 fix(package): add missing even-to-promise 2016-10-04 15:33:24 +02:00
Julien Fontanet
0602410aa8 fix(package): update xo-acl-resolver to 0.2.2
Fixes vatesfr/xo-web#1621
2016-10-04 15:33:23 +02:00
Olivier Lambert
1112768adc feat(home/host): add memory bar (#1617)
Fixes #1616
2016-10-03 18:06:36 +02:00
Julien Fontanet
86b599df89 5.2.3 2016-10-03 09:39:59 +02:00
Olivier Lambert
88f7661172 feat(changelog): add info for 5.2.3 release 2016-10-03 09:21:45 +02:00
Julien Fontanet
29c96c0119 chore(gitignore): pnpm compat 2016-10-03 09:15:18 +02:00
Julien Fontanet
d8c6e54c68 fix(user): add VM template label 2016-10-03 09:13:42 +02:00
Julien Fontanet
df053eb016 fix(user): do not crash on missing type label 2016-10-03 09:13:26 +02:00
Julien Fontanet
d1715f7711 chore(intl): homeTypeTemplate → homeTypeVmTemplate 2016-10-03 09:09:40 +02:00
Julien Fontanet
240282c72d chore(intl): remove unused message 2016-10-03 09:09:03 +02:00
Olivier Lambert
9e8dd6ea21 fix(README): broken link to doc 2016-10-02 23:23:19 +02:00
Julien Fontanet
32806a20c9 fix(sr): goes to homepage if object disappear
Fixes #1611
2016-10-02 23:02:14 +02:00
Pierre Donias
34dcfbbf49 fix(home/item): prevent item from being displayed on 2 rows (#1608)
Fixes #1580
2016-09-30 13:33:05 +02:00
Pierre Donias
91fec43866 feat(pool/network): create a bonded network (#1605)
See #876
2016-09-30 11:23:44 +02:00
Greenkeeper
aa2d196a79 chore(package): update vinyl to version 2.0.0 (#1607)
https://greenkeeper.io/
2016-09-29 23:06:53 +02:00
Pierre Donias
180ca458ad feat(vm/network): allow VIF edition (#1596)
See #1446
2016-09-28 14:29:15 +02:00
Greenkeeper
aa881c60e7 chore(package): update babel-eslint to version 7.0.0 (#1597)
https://greenkeeper.io/
2016-09-27 23:39:03 +02:00
Greenkeeper
5b6966042d standard@8.2.0 breaks build 🚨 (#1595)
https://greenkeeper.io/
2016-09-27 01:11:46 +02:00
Julien Fontanet
dc859da0cd chore(build): remove embedded dev server 2016-09-26 16:45:02 +02:00
Pierre Donias
151eb6cbd6 feat(updates,users,servers): disable credentials autocomplete (#1592)
Fixes #1304
2016-09-26 16:00:04 +02:00
Olivier Lambert
16db591bbf feat(vm): add red icon if VM doesn't have tools. Fixes #1575 2016-09-26 12:24:58 +02:00
Pierre Donias
05a55e5eb2 fix(xoa-upgrade): more suitable message for non-admin users (#1591)
Fixes #1564
2016-09-26 10:31:23 +02:00
Pierre Donias
dcd84b2b8f feat(shortcuts): help modal and new home shortcuts (#1588)
Fixes #1578
2016-09-23 18:43:10 +02:00
Greenkeeper
4a89119f0a Update react-virtualized to version 8.0.8 🚀 (#1587)
https://greenkeeper.io/
2016-09-23 17:11:55 +02:00
Julien Fontanet
bc1c30a7bf chore(package): add __self prop, better React warnings 2016-09-23 16:02:19 +02:00
Julien Fontanet
33cffbf28b fix(Copiable): do not pass tagName prop to wrapper 2016-09-23 15:50:40 +02:00
Julien Fontanet
a18b68116c chore(package): improve React stack traces in dev build 2016-09-23 15:45:03 +02:00
Pierre Donias
d5acf15bca feat(vm/network): indicate when an IP is already used (#1584)
Fixes #1566
2016-09-23 12:13:48 +02:00
Pierre Donias
84f970af68 fix(shortcuts): prevent Shortcuts from stopping events propagation (#1583)
`Editable`s were broken (could not use *enter* to save).
2016-09-23 11:35:56 +02:00
Julien Fontanet
969f636bb7 fix(host/storage): name sorting 2016-09-23 10:54:36 +02:00
Pierre Donias
6939aee20a feat(home): keyboard shortcuts (#1400)
Fixes #1279
2016-09-22 19:01:05 +02:00
Pierre Donias
ab2a02a555 fix(vm/tab-network): lock icon conditions (#1576)
Fixes #1573
2016-09-22 16:52:37 +02:00
Pierre Donias
70038e0764 fix(new-vm): lodash/sum instead of lodash/sumBy (#1577) 2016-09-22 16:19:34 +02:00
Olivier Lambert
e730ef5e11 feat(host/sr): Sr link and better storage tab. (#1572)
Fixes #1567
2016-09-22 16:04:45 +02:00
Olivier Lambert
835ad5aaf1 feat(vm/host/pools/sr): add tooltips. Fixes #1568 2016-09-22 11:02:05 +02:00
Pierre Donias
ac645c8617 fix(home): types dropdown button title (#1570) 2016-09-22 10:47:48 +02:00
Julien Fontanet
b801fdbab2 5.2.2 2016-09-21 18:01:03 +02:00
Pierre Donias
bf495953e2 feat(new-vm): show resource set limits (#1563)
Fixes #1541
2016-09-21 11:58:07 -04:00
Pierre Donias
45b165deec fix(home): VM bulk restart (#1562)
Fixes #1561
2016-09-21 14:48:16 +02:00
Olivier Lambert
09169578e8 feat(changelog): add issue #1562 2016-09-21 14:47:52 +02:00
Olivier Lambert
43b2366927 feat(changelog): update changelog for 5.2.2 2016-09-21 12:22:37 +02:00
Julien Fontanet
f015a69eec feat(host/patches): display if needs to be restarted (#1559)
Fixes #1352
2016-09-21 10:10:44 +02:00
Olivier Lambert
99568508dd fix(charts): change color order to avoid confusions. Fixes #1265 2016-09-20 18:41:44 +02:00
Olivier Lambert
e8515344dd feat(home): display a message if a filter is empty. (#1560)
feat(home): display a message if a filter is empty. Fixes #1517
2016-09-20 12:34:07 -04:00
Julien Fontanet
edc873a570 fix(pool/storage): fix sort by usage 2016-09-20 15:53:45 +02:00
Julien Fontanet
1a03e96ab2 fix(pool/storage): fix passing pool to SortedTable 2016-09-20 15:53:25 +02:00
Olivier Lambert
89e0bb4f0a feat(home/templates): template management (#1533)
Fixes #1091
2016-09-20 13:46:58 +02:00
Olivier Lambert
7d0fd60908 feat(pool/storage): display default SR and add button to set it (#1557)
Fixes #1554
2016-09-20 13:19:47 +02:00
Pierre Donias
6b20523df4 fix(vm/tab-network): check for undefined network (#1556)
Fixes #1518
2016-09-20 11:52:17 +02:00
Julien Fontanet
e9a612647e fix(home): do not overwrite current filter (#1555)
Fixes #1513
2016-09-20 05:38:33 -04:00
Julien Fontanet
28404ef149 feat(SortedTable): default column can be set by a simple prop 2016-09-20 09:43:57 +02:00
Olivier Lambert
a5f8230def feat(self): hide some buttons and tabs for self service VMs (#1550) 2016-09-19 15:17:45 +02:00
Pierre Donias
39171de5de fix(job/new): forbid "_" character in job names (#1548)
Fixes #1414
2016-09-19 11:58:46 +02:00
Pierre Donias
5aa5a0acbc fix(home): set items per page in createPager (#1549) 2016-09-19 11:39:56 +02:00
Olivier Lambert
a4518e630a fix(log): sort logs by end date 2016-09-19 11:04:15 +02:00
Julien Fontanet
94975f5ea6 feat(settings/logs): group action buttons
Fixes #1547
2016-09-19 10:59:19 +02:00
Julien Fontanet
7e98838d96 feat(ActionButton): can have a tooltip 2016-09-19 10:59:19 +02:00
Pierre Donias
e8c9c196ff feat(vm/snapshot): "Snapshot before" checkbox in revert modal (#1543)
Fixes #1445
2016-09-17 00:11:51 +02:00
Julien Fontanet
db314a238f fix(xo/subcriptions): mark as non-running on error 2016-09-16 17:13:56 +02:00
Julien Fontanet
2c85a6d4ab fix(self/admin): do not fail on empty limits
Fixes #1537
2016-09-16 17:13:56 +02:00
Pierre Donias
b683e14e80 feat(self): merge dashboard and administration (#1542)
Fixes #1429
2016-09-16 16:47:20 +02:00
Olivier Lambert
ba45095fa8 fix(editable): limit input size for long text to 50ex. Fixes #1528 2016-09-15 16:45:19 +02:00
Olivier Lambert
b8e5ffa9f7 feat(backup): improved view. Fixes #1534 2016-09-15 16:26:42 +02:00
Pierre Donias
b4bff9e032 feat(vm/disks): "Long click to migrate" tooltip on SR (#1529)
* feat(vm/disks): "Long click to migrate" tooltip on SR

Fixes #1512

* feat(vm/disks): migrate VDI button, force cursor on editable

* fix bulk VDI migration

* Return Promise.all

* Ternary operator
2016-09-15 16:08:50 +02:00
Olivier Lambert
0c461bc4e2 feat(backuplogs): shorten the start/end date format. Fixes #1535 2016-09-15 15:55:01 +02:00
Olivier Lambert
a33b2a5294 fix(home/menu): adapt message depending of user perm (#1532)
Fixes #1519
2016-09-15 13:56:33 +02:00
Olivier Lambert
298e1c4471 fix(host console): removing useless ISO selector for host. Fixes #1527 2016-09-15 13:07:33 +02:00
Julien Fontanet
1c70cdc10b fix(render-xo-item): remove unused import 2016-09-15 13:07:04 +02:00
Julien Fontanet
160e4bb530 fix(SelectVmTemplates): use correct placeholder
Fixes #1530
2016-09-15 12:00:32 +02:00
fufroma
e69ba8dd96 Fix typo on Changelog (#1526) 2016-09-15 04:44:42 -04:00
Julien Fontanet
e55f4c3eb2 feat(settings/logs): show complete error object 2016-09-14 17:42:32 +02:00
Pierre Donias
1a3272b980 feat(new-vm): auto poweron setting (#1514)
Fixes #1444
2016-09-14 15:25:54 +02:00
Julien Fontanet
7bed5e025a fix(home): fix empty default filter (#1504)
Fixes #1354
2016-09-14 14:38:24 +02:00
Pierre Donias
29d22c0598 feat(backup/new): confirm modal when local remote is selected (#1507)
Fixes #1441
2016-09-14 13:20:51 +02:00
Greenkeeper
a38c7c34ac chore(package): update react-key-handler to version 0.3.0 (#1505)
https://greenkeeper.io/
2016-09-14 01:42:16 +02:00
Julien Fontanet
8d690ce4ff 5.2.1 2016-09-13 16:54:42 +02:00
Olivier Lambert
2569568a03 feat(changelog): patch version 5.2.1 changelog 2016-09-13 16:52:47 +02:00
Olivier Lambert
2c6ff6b5b8 feat(stopHost modal): explain consequences if halting a pool master. Fixes #1458 2016-09-13 15:28:30 +02:00
Pierre Donias
1257f01027 feat(new-vm): create tags during VM creation (#1501)
See #1431
2016-09-13 14:17:04 +02:00
Pierre Donias
fad6830863 fix(settings/remotes): SMB domain is required (#1502)
Fixes #1499
2016-09-13 11:28:07 +02:00
Olivier Lambert
66262bb20b fix(vm console): re-fix issue #1485 2016-09-13 11:01:18 +02:00
Julien Fontanet
4abb0754c7 fix(remotes): edition of URL parts (#1500)
Fixes #1498
2016-09-13 10:20:48 +02:00
Pierre Donias
78c53bf3ad fix(xo): better message for snapshot deletion (#1497)
Fixes #1433
2016-09-13 09:42:18 +02:00
Pierre Donias
810d666d84 feat(home): tooltips on bulk action buttons (#1496)
Fixes #1456
2016-09-12 17:04:02 +02:00
Pierre Donias
67699f0bb6 feat(pool/network): SortedTable and collapsable PIFs table (#1494)
Fixes #1461
2016-09-12 16:44:43 +02:00
Pierre Donias
46274948c0 fix(settings/logs): properly handle unknown user (#1495) 2016-09-12 16:42:52 +02:00
Olivier Lambert
28e3a842ef feat(vm): new containers tab (#1492)
Fixes #1442
2016-09-12 13:50:14 +02:00
Pierre Donias
6d90f1d45d fix(user): prevent SSH key overflow (#1491)
Fixes #1475
2016-09-12 12:57:11 +02:00
Pierre Donias
09642c347d feat(new-vm): Advanced section (#1486)
Fixes #1444
2016-09-12 12:27:11 +02:00
Pierre Donias
2d0e06f785 fix(backup/new): better month and week days layout (#1490)
Fixes #1488
2016-09-12 11:58:24 +02:00
Pierre Donias
a5bc8497cf fix(vm/tab-network): set default network and MTU for VIF creation (#1487)
Fixes #1472
2016-09-12 11:26:32 +02:00
Julien Fontanet
4bcb65c518 5.2.0 2016-09-09 16:31:24 +02:00
Olivier Lambert
25361fa7eb feat(changelog): add IP management feature in changelog 2016-09-09 16:30:13 +02:00
Pierre Donias
889a265000 feat(settings/ips): new page to manage IP pools (#1418)
Fixes #1350, fixes #988 and fixes #240.
2016-09-09 16:19:26 +02:00
Olivier Lambert
3122f6dcd5 feat(changelog): update changelog for 5.2.0 2016-09-09 14:21:54 +02:00
Olivier Lambert
16aa2e8085 bug(vmConsole): re-display header if VM goes from running to halted state. Fixes #1485 2016-09-08 16:29:56 +02:00
Julien Fontanet
074d51a670 fix(xo/deleteVms): delete disks as well
Fixes #1484
2016-09-08 15:06:42 +02:00
Greenkeeper
2122a79132 chore(package): update chartist-plugin-legend to version 0.5.0 (#1482)
https://greenkeeper.io/
2016-09-08 11:01:50 +02:00
Ronan Abhamon
26dbc585ba feat(vm-import): supports ova import (#709) (#1361)
Fixes #709
2016-09-08 10:15:44 +02:00
Greenkeeper
4b3cfbd424 chore(package): update modular-css to version 0.27.1 (#1478)
https://greenkeeper.io/
2016-09-08 09:55:38 +02:00
Julien Fontanet
035191a2cc feat(xo/installAllPatchesOnPool): better pool-wide patching (#1476)
Fixes #1392
2016-09-07 17:55:02 +02:00
Olivier Lambert
06a40180a1 fix(vm): do not display VDI or VIF disconnect but when VM is not running. Fixes #1470 and fixes #1468 2016-09-05 16:59:21 +02:00
Julien Fontanet
aaf4c5dff7 chore(Vm): move isRunning into utils/isVmRunning 2016-09-05 16:40:13 +02:00
Olivier Lambert
0c83bc2b0e feat(logview): standardize and improve the settings/log view. Fixes #1467 2016-09-05 16:10:12 +02:00
Julien Fontanet
2d412fd8db fix(scheduling): month and day display
There were issues with some timezones.

Fixes #1404 & fixes #1438.
2016-09-01 11:47:50 -03:00
Julien Fontanet
443e2bec25 chore(NewVm#_getIsoPredicate): memoise selector 2016-09-01 10:57:13 -03:00
Olivier Lambert
d5e1323d82 feat(newVif): select management network by default when adding a vif. Fixes #1425 2016-09-01 15:34:49 +02:00
Julien Fontanet
7f0b77cc89 chore(package): update chartist-plugin-legend to version 0.4.0 (#1450)
https://greenkeeper.io/
2016-08-31 10:32:50 +02:00
greenkeeperio-bot
0169cff66c chore(package): update chartist-plugin-legend to version 0.4.0
https://greenkeeper.io/
2016-08-31 10:17:48 +02:00
Olivier Lambert
0fd1424a41 fix(newVm): check pool object for ISO selector when creating a VM from selfservice. Fixes #1448 2016-08-30 21:26:59 +02:00
Julien Fontanet
6280d56f32 chore(xo): use resolveId() (only) where it makes sense 2016-08-26 16:18:03 -04:00
Julien Fontanet
9f2a77872f fix(xo/deleteUser): dont attempt to display error when cancelled
Fixes vatesfr/xo-web#1439
2016-08-26 14:38:02 -04:00
Pierre Donias
b571c18e9a feat(host): indicate pool master in multiple places (#1423)
Fixes #1407
2016-08-25 12:33:55 -04:00
Greenkeeper
49863d6e4d Update standard to version 8.0.0 🚀 (#1435)
https://greenkeeper.io/
2016-08-24 13:00:00 -04:00
Julien Fontanet
48cc7bb647 5.1.9 2016-08-22 14:02:40 -04:00
Pierre Donias
442d42d8dc fix(settings/logs): show params in a modal (#1424) 2016-08-19 16:09:20 +02:00
Olivier Lambert
9501ebacfc feat(menu): add warning icon when disconnected 2016-08-19 13:52:31 +02:00
Pierre Donias
23f9fa46f8 fix(home/host-item): do not show pool name if not enough permissions (#1421) 2016-08-19 13:40:55 +02:00
Julien Fontanet
1bd0f37fd4 feat(Menu): display when disconnected
Fixes vatesfr/xo-web#1417
2016-08-19 12:27:48 +02:00
Pierre Donias
ed74ded923 feat(settings/logs): display parameters (#1420) 2016-08-19 10:06:45 +02:00
Olivier Lambert
b732410b74 feat(vm and home): add tooltip to OS icon. Fixes #1416 2016-08-18 14:12:46 +02:00
Olivier Lambert
a51f2b7fcf fix(newvm): check if ssh keys object exists 2016-08-18 13:59:08 +02:00
Olivier Lambert
fe12bbb60d fix(sr): container var check if not defined 2016-08-18 13:51:37 +02:00
Olivier Lambert
8882df7939 5.1.8 2016-08-17 11:07:30 +02:00
Olivier Lambert
185a554cd9 fix(newVm): fix wrong ISO SR predicate. Fixes #1415 2016-08-17 11:06:35 +02:00
Olivier Lambert
230e0dc2a5 5.1.7 2016-08-16 15:38:59 +02:00
Pierre Donias
f5b69fdfdc feat(vm/console): hide header and resize console (#1410)
Fix #1268
2016-08-16 14:49:44 +02:00
Greenkeeper
01dc0d8f1e chore(package): update modular-css to version 0.26.0 (#1385)
https://greenkeeper.io/
2016-08-16 12:45:29 +02:00
Greenkeeper
8035886a3c chore(package): update promise-toolbox to version 0.5.0 (#1409)
https://greenkeeper.io/
2016-08-16 12:22:45 +02:00
Olivier Lambert
0ab5f4b13f fix(host): wrong storage link. Fixes #1408 2016-08-16 11:16:56 +02:00
Pierre Donias
a1bc98def8 feat(host): redirect to home when host disappears (#1406) 2016-08-16 09:50:29 +02:00
Olivier Lambert
868cf6140b feat(settings): more tooltips for server connect/disconnect 2016-08-15 18:04:01 +02:00
Olivier Lambert
4b3473f480 feat(logstackmodal): use pre tag for stack trace 2016-08-15 17:52:04 +02:00
Olivier Lambert
7bc782cc62 feat(copiable): add tooltip on copiable component 2016-08-15 17:33:59 +02:00
Olivier Lambert
e625a53e4a fix(vm migration): allow target network without IPs. Fixes #1403 2016-08-15 15:20:59 +02:00
Olivier Lambert
b31185d96d fix(newVm): typo spotted by @Danp2 2016-08-15 14:07:12 +02:00
Olivier Lambert
09d75e972f feat(newVm): add missing tooltips. Fixes #1402 2016-08-15 11:44:36 +02:00
Olivier Lambert
f33568951b 5.1.6 2016-08-12 17:28:49 +02:00
Pierre Donias
8d8c442be5 feat(settings/logs): new view to display API logs (#1401)
Fix #1344
2016-08-12 17:27:50 +02:00
Olivier Lambert
f890b8ea7a feat(modal text): warns users about consequences of host eject 2016-08-11 21:13:32 +02:00
Pierre Donias
1b80b3929c feat(host): detach host from its pool (#1399)
Fixes #1395
2016-08-11 17:49:25 +02:00
Pierre Donias
4f946293f6 feat(pool): add host (#1398)
Fixes #1374
2016-08-11 17:05:41 +02:00
Olivier Lambert
36788cde2b feat(vm disk): add VBD connect for a running VM. Fixes #1397 2016-08-11 16:52:52 +02:00
Pierre Donias
1547c99e5a feat(new-vm): use saved SSH key in cloud config(#1394)
* feat(new-vm): use saved SSH key in cloud config. Fixes #1319
2016-08-11 13:32:54 +02:00
Olivier Lambert
5c9606dad8 feat(pool): improve pool view. Fixes #1393 2016-08-11 10:34:03 +02:00
Olivier Lambert
fdcb1dccf5 feat(pool): start to work on adding an existing host to a pool 2016-08-11 09:47:52 +02:00
Olivier Lambert
12812b8c23 5.1.5 2016-08-10 18:06:19 +02:00
Olivier Lambert
0098497255 fix(select): select color modified due to an update. Fixes #1391 2016-08-10 16:02:48 +02:00
Olivier Lambert
6562d2de7f feat(sr select): display space left on SR. Fixes #1358 2016-08-10 15:58:36 +02:00
Olivier Lambert
1f0e88cdb0 feat(backup): better tooltips. Fixes #1363 2016-08-10 14:17:27 +02:00
Olivier Lambert
197da91ef3 feat(vdi remove): add modal when removing a VDI. Fixes #1388 2016-08-10 13:39:13 +02:00
Olivier Lambert
cbd59789e2 fix(vm disks): _isFreeForWriting missing case. Fixes #1386 2016-08-10 13:13:17 +02:00
Olivier Lambert
190ecf3d74 fix(pool): pool name and description edition. Fixes #1390 2016-08-10 12:42:46 +02:00
Olivier Lambert
15b8f6bca2 feat(meter tooltips): add tooltips for meter object. Fixes #1387 2016-08-10 12:31:28 +02:00
Pierre Donias
5b406d731b fix(vm): select destination SR when at least one VDI is local (#1382)
* Fixes #1357 
* fix(vm): select destination SR when at least one VDI is local
* fix(vm): do not send map when not necessary
2016-08-09 17:03:08 +02:00
Olivier Lambert
4be9e67ac4 fix(metercss): remove useless and conflicting CSS styles 2016-08-09 10:31:03 +02:00
Olivier Lambert
d047421685 feat(updates): enhance update view. Also fixes #1341 2016-08-08 16:46:47 +02:00
Olivier Lambert
f6f415a421 fix(network): name instead of description 2016-08-08 14:55:15 +02:00
Pierre Donias
edfaaebac0 feat(dashboard/health): Storage table: BlockLink (SR) and Link (SR's pool)
Fixes #1381
2016-08-08 14:15:49 +02:00
Olivier Lambert
67df22a1bf feat(vmsnapshot): add snapshot export and copy. Fixes #1353 and #1336 2016-08-08 14:05:27 +02:00
Pierre Donias
7dc59a00f6 feat(pool): action button to create an SR (#1380)
Fixes #1372
2016-08-08 12:45:12 +02:00
Pierre Donias
6214fe4c2e feat(pool): action button to create a VM (#1379)
Fix #1373
2016-08-08 11:35:24 +02:00
Greenkeeper
21610c3e0a chore(package): update ava to version 0.16.0 (#1377)
https://greenkeeper.io/
2016-08-08 09:57:36 +02:00
Olivier Lambert
87550b0189 5.1.4 2016-08-07 19:35:37 +02:00
Ronan Abhamon
b7c42d0a08 fix(scheduling): range not working
Fixes #1376
2016-08-07 19:35:05 +02:00
Olivier Lambert
c15ad299ac fix(sparklines): smaller sparklines and removing useless dots 2016-08-05 14:39:07 +02:00
274 changed files with 56926 additions and 17059 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',
},
}

10
.gitignore vendored
View File

@@ -1,9 +1,9 @@
/.nyc_output/
/bower_components/
/dist/
/node_modules/
/src/common/intl/locales/index.js
/src/common/themes/index.js
npm-debug.log
npm-debug.log.*
!node_modules/*
node_modules/*/
pnpm-debug.log
pnpm-debug.log.*

4
.prettierrc.js Normal file
View File

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

View File

@@ -1,11 +1,10 @@
language: node_js
node_js:
- 'stable'
#- '4' # Disabled for now because npm 2 cannot properly handled broken peer dependencies.
- '6'
#- '4' # npm 3's flat tree is needed because some packages do not
# declare their deps correctly (e.g. chartist-plugin-tooltip)
cache:
directories:
- node_modules
cache: yarn
# Use containers.
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/

View File

@@ -1,5 +1,538 @@
# ChangeLog
## **5.15.0** (2017-12-29)
### Enhancements
* 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)
### Enhancements
- Missing favicon [\#1660](https://github.com/vatesfr/xo-web/issues/1660)
- ipPools quota [\#1565](https://github.com/vatesfr/xo-web/issues/1565)
- Dashboard - orphaned VDI [\#1654](https://github.com/vatesfr/xo-web/issues/1654)
- Stats in home/host view when expanded [\#1634](https://github.com/vatesfr/xo-web/issues/1634)
- Bar for used and total RAM on home pool view [\#1625](https://github.com/vatesfr/xo-web/issues/1625)
- Can't translate some text [\#1624](https://github.com/vatesfr/xo-web/issues/1624)
- Dynamic RAM allocation at creation time [\#1603](https://github.com/vatesfr/xo-web/issues/1603)
- Display memory bar in home/host view [\#1616](https://github.com/vatesfr/xo-web/issues/1616)
- Improve keyboard navigation [\#1578](https://github.com/vatesfr/xo-web/issues/1578)
- Strongly suggest to install the guest tools [\#1575](https://github.com/vatesfr/xo-web/issues/1575)
- Missing tooltip [\#1568](https://github.com/vatesfr/xo-web/issues/1568)
- Emphasize already used ips in ipPools [\#1566](https://github.com/vatesfr/xo-web/issues/1566)
- Change "missing feature message" for non-admins [\#1564](https://github.com/vatesfr/xo-web/issues/1564)
- Allow VIF edition [\#1446](https://github.com/vatesfr/xo-web/issues/1446)
- Disable browser autocomplete on credentials on the Update page [\#1304](https://github.com/vatesfr/xo-web/issues/1304)
- keyboard shortcuts [\#1279](https://github.com/vatesfr/xo-web/issues/1279)
- Add network bond creation [\#876](https://github.com/vatesfr/xo-web/issues/876)
- `pool.setDefaultSr\(\)` should not require `pool` param [\#1558](https://github.com/vatesfr/xo-web/issues/1558)
- Select default SR [\#1554](https://github.com/vatesfr/xo-web/issues/1554)
- No error message when I exceed my resource set quota [\#1541](https://github.com/vatesfr/xo-web/issues/1541)
- Hide some buttons for self service VMs [\#1539](https://github.com/vatesfr/xo-web/issues/1539)
- Add Job ID to backup schedules [\#1534](https://github.com/vatesfr/xo-web/issues/1534)
- Correct name for VM selector with templates [\#1530](https://github.com/vatesfr/xo-web/issues/1530)
- Help text when no matches for a filter [\#1517](https://github.com/vatesfr/xo-web/issues/1517)
- Icon or tooltip to allow VDI migration in VM disk view [\#1512](https://github.com/vatesfr/xo-web/issues/1512)
- Create a snapshot before restoring one [\#1445](https://github.com/vatesfr/xo-web/issues/1445)
- Auto power on setting at creation time [\#1444](https://github.com/vatesfr/xo-web/issues/1444)
- local remotes should be avoided if possible [\#1441](https://github.com/vatesfr/xo-web/issues/1441)
- Self service edition unclear [\#1429](https://github.com/vatesfr/xo-web/issues/1429)
- Avoid "\_" char in job tag name [\#1414](https://github.com/vatesfr/xo-web/issues/1414)
- Display message if host reboot needed to apply patches [\#1352](https://github.com/vatesfr/xo-web/issues/1352)
- Color code on host PIF stats can be misleading [\#1265](https://github.com/vatesfr/xo-web/issues/1265)
- Sign in page is not rendered correctly [\#1161](https://github.com/vatesfr/xo-web/issues/1161)
- Template management [\#1091](https://github.com/vatesfr/xo-web/issues/1091)
- On pool view: collapse network list [\#1461](https://github.com/vatesfr/xo-web/issues/1461)
- Alert when trying to reboot/halt the pool master XS [\#1458](https://github.com/vatesfr/xo-web/issues/1458)
- Adding tooltip on Home page [\#1456](https://github.com/vatesfr/xo-web/issues/1456)
- Docker container management functionality missing from v5 [\#1442](https://github.com/vatesfr/xo-web/issues/1442)
- bad error message - delete snapshot [\#1433](https://github.com/vatesfr/xo-web/issues/1433)
- Create tag during VM creation [\#1431](https://github.com/vatesfr/xo-web/issues/1431)
### Bug fixes
- Display issues on plugin array edition [\#1663](https://github.com/vatesfr/xo-web/issues/1663)
- Import of delta backups fails [\#1656](https://github.com/vatesfr/xo-web/issues/1656)
- Host - Missing IP config for PIF [\#1651](https://github.com/vatesfr/xo-web/issues/1651)
- Remote copy is always activating compression [\#1645](https://github.com/vatesfr/xo-web/issues/1645)
- LB plugin UI problems [\#1630](https://github.com/vatesfr/xo-web/issues/1630)
- Keyboard shortcuts should not work when a modal is open [\#1589](https://github.com/vatesfr/xo-web/issues/1589)
- UI small bug in drop-down lists [\#1411](https://github.com/vatesfr/xo-web/issues/1411)
- md5 delta backup error [\#1672](https://github.com/vatesfr/xo-web/issues/1672)
- Can't edit VIF network [\#1640](https://github.com/vatesfr/xo-web/issues/1640)
- Do not expose shortcuts while console is focused [\#1614](https://github.com/vatesfr/xo-web/issues/1614)
- All users can see VM templates [\#1621](https://github.com/vatesfr/xo-web/issues/1621)
- Profile page is broken [\#1612](https://github.com/vatesfr/xo-web/issues/1612)
- SR delete should redirect to home [\#1611](https://github.com/vatesfr/xo-web/issues/1611)
- Delta VHD backup checksum is invalidated by chaining [\#1606](https://github.com/vatesfr/xo-web/issues/1606)
- VM with long description break on 2 lines [\#1580](https://github.com/vatesfr/xo-web/issues/1580)
- Network status on VM edition [\#1573](https://github.com/vatesfr/xo-web/issues/1573)
- VM template deletion fails [\#1571](https://github.com/vatesfr/xo-web/issues/1571)
- Template edition - "no such object" [\#1569](https://github.com/vatesfr/xo-web/issues/1569)
- missing links / element not displayed as links [\#1567](https://github.com/vatesfr/xo-web/issues/1567)
- Backup restore stalled on some SMB shares [\#1412](https://github.com/vatesfr/xo-web/issues/1412)
- Wrong bond display [\#1156](https://github.com/vatesfr/xo-web/issues/1156)
- Multiple reboot selection doesn't work [\#1562](https://github.com/vatesfr/xo-web/issues/1562)
- Server logs should be displayed in reverse chonological order [\#1547](https://github.com/vatesfr/xo-web/issues/1547)
- Cannot create resource sets without limits [\#1537](https://github.com/vatesfr/xo-web/issues/1537)
- UI - Weird display when editing long VM desc [\#1528](https://github.com/vatesfr/xo-web/issues/1528)
- Useless iso selector in host console [\#1527](https://github.com/vatesfr/xo-web/issues/1527)
- Pool and Host dummy welcome message [\#1519](https://github.com/vatesfr/xo-web/issues/1519)
- Bug on Network VM tab [\#1518](https://github.com/vatesfr/xo-web/issues/1518)
- Link to home with filter in query does not work [\#1513](https://github.com/vatesfr/xo-web/issues/1513)
- VHD merge fails with "RangeError: index out of range" on SMB remote [\#1511](https://github.com/vatesfr/xo-web/issues/1511)
- DR: previous VDIs are not removed [\#1510](https://github.com/vatesfr/xo-web/issues/1510)
- DR: previous copies not removed when same number as depth [\#1509](https://github.com/vatesfr/xo-web/issues/1509)
- Empty Saved Search doesn't load when set to default filter [\#1354](https://github.com/vatesfr/xo-web/issues/1354)
- Removing a user/group should delete its ACLs [\#899](https://github.com/vatesfr/xo-web/issues/899)
- OVA Import - XO stuck during import [\#1551](https://github.com/vatesfr/xo-web/issues/1551)
- SMB remote empty domain fails [\#1499](https://github.com/vatesfr/xo-web/issues/1499)
- Can't edit a remote password [\#1498](https://github.com/vatesfr/xo-web/issues/1498)
- Issue in VM create with CoreOS [\#1493](https://github.com/vatesfr/xo-web/issues/1493)
- Overlapping months in backup view [\#1488](https://github.com/vatesfr/xo-web/issues/1488)
- No line break for SSH key in user view [\#1475](https://github.com/vatesfr/xo-web/issues/1475)
- Create VIF UI issues [\#1472](https://github.com/vatesfr/xo-web/issues/1472)
## **5.2.0** (2016-09-09)
### Enhancements
- IP management [\#1350](https://github.com/vatesfr/xo-web/issues/1350), [\#988](https://github.com/vatesfr/xo-web/issues/988), [\#1427](https://github.com/vatesfr/xo-web/issues/1427) and [\#240](https://github.com/vatesfr/xo-web/issues/240)
- Update reverse proxy example [\#1474](https://github.com/vatesfr/xo-web/issues/1474)
- Improve log view [\#1467](https://github.com/vatesfr/xo-web/issues/1467)
- Backup Reports: e-mail subject [\#1463](https://github.com/vatesfr/xo-web/issues/1463)
- Backup Reports: report the error [\#1462](https://github.com/vatesfr/xo-web/issues/1462)
- Vif selector: select management network by default [\#1425](https://github.com/vatesfr/xo-web/issues/1425)
- Display when browser disconnected to server [\#1417](https://github.com/vatesfr/xo-web/issues/1417)
- Tooltip on OS icon in VM view [\#1416](https://github.com/vatesfr/xo-web/issues/1416)
- Display pool master [\#1407](https://github.com/vatesfr/xo-web/issues/1407)
- Missing tooltips in VM creation view [\#1402](https://github.com/vatesfr/xo-web/issues/1402)
- Handle 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)
- Pool name modification [\#1390](https://github.com/vatesfr/xo-web/issues/1390)
- Confirmation dialog before destroying VDIs [\#1388](https://github.com/vatesfr/xo-web/issues/1388)
- Tooltips for meter object [\#1387](https://github.com/vatesfr/xo-web/issues/1387)
- New Host assistant [\#1374](https://github.com/vatesfr/xo-web/issues/1374)
- New VM assistant [\#1373](https://github.com/vatesfr/xo-web/issues/1373)
- New SR assistant [\#1372](https://github.com/vatesfr/xo-web/issues/1372)
- Direct access to VDI listing from dashboard's SR usage breakdown [\#1371](https://github.com/vatesfr/xo-web/issues/1371)
- Can't set a network name at pool level [\#1368](https://github.com/vatesfr/xo-web/issues/1368)
- Change a few mouse over descriptions [\#1363](https://github.com/vatesfr/xo-web/issues/1363)
- Hide network install in VM create if template is HVM [\#1362](https://github.com/vatesfr/xo-web/issues/1362)
- SR space left during VM creation [\#1358](https://github.com/vatesfr/xo-web/issues/1358)
- Add destination SR on migration modal in VM view [\#1357](https://github.com/vatesfr/xo-web/issues/1357)
- Ability to create a new VM from a snapshot [\#1353](https://github.com/vatesfr/xo-web/issues/1353)
- Missing explanation/confirmation on Snapshot Page [\#1349](https://github.com/vatesfr/xo-web/issues/1349)
- Log view: expose API errors in the web UI [\#1344](https://github.com/vatesfr/xo-web/issues/1344)
- Registration on update page [\#1341](https://github.com/vatesfr/xo-web/issues/1341)
- Add export snapshot button [\#1336](https://github.com/vatesfr/xo-web/issues/1336)
- Use saved SSH keys in VM create CloudConfig [\#1319](https://github.com/vatesfr/xo-web/issues/1319)
- Collapse header in console view [\#1268](https://github.com/vatesfr/xo-web/issues/1268)
- Two max concurrent jobs in parallel [\#915](https://github.com/vatesfr/xo-web/issues/915)
- Handle OVA import via the web UI [\#709](https://github.com/vatesfr/xo-web/issues/709)
### Bug fixes
- Bug on VM console when header is hidden [\#1485](https://github.com/vatesfr/xo-web/issues/1485)
- Disks not removed when deleting multiple VMs [\#1484](https://github.com/vatesfr/xo-web/issues/1484)
- Do not display VDI disconnect button when a VM is not running [\#1470](https://github.com/vatesfr/xo-web/issues/1470)
- Do not display VIF disconnect button when a VM is not running [\#1468](https://github.com/vatesfr/xo-web/issues/1468)
- Error on migration if no default SR \(even when not used\) [\#1466](https://github.com/vatesfr/xo-web/issues/1466)
- DR issue while rotating old backup [\#1464](https://github.com/vatesfr/xo-web/issues/1464)
- Giving resource set to end-user ends with error [\#1448](https://github.com/vatesfr/xo-web/issues/1448)
- Error thrown when cancelling out of Delete User confirmation dialog [\#1439](https://github.com/vatesfr/xo-web/issues/1439)
- Wrong month label shown in Backup and Job scheduler [\#1438](https://github.com/vatesfr/xo-web/issues/1438)
- Bug on Self service creation/edition [\#1428](https://github.com/vatesfr/xo-web/issues/1428)
- ISO selection during VM create is not mounted after [\#1415](https://github.com/vatesfr/xo-web/issues/1415)
- Hosts general view: bad link for storage [\#1408](https://github.com/vatesfr/xo-web/issues/1408)
- Backup Schedule - "Month" and "Day of Week" display error [\#1404](https://github.com/vatesfr/xo-web/issues/1404)
- Migrate dialog doesn't present all available VIF's in new UI interface [\#1403](https://github.com/vatesfr/xo-web/issues/1403)
- NFS mount issues [\#1396](https://github.com/vatesfr/xo-web/issues/1396)
- Select component color [\#1391](https://github.com/vatesfr/xo-web/issues/1391)
- SR created with local path shouldn't be shared [\#1389](https://github.com/vatesfr/xo-web/issues/1389)
- Disks (VBD) are attached to VM in RO mode instead of RW even if RO is unchecked [\#1386](https://github.com/vatesfr/xo-web/issues/1386)
- Re-connection issues between server and XS hosts [\#1384](https://github.com/vatesfr/xo-web/issues/1384)
- Meter object style with Chrome 52 [\#1383](https://github.com/vatesfr/xo-web/issues/1383)
- Editing a rolling snapshot job seems to fail [\#1376](https://github.com/vatesfr/xo-web/issues/1376)
- Dashboard SR usage and total inverted [\#1370](https://github.com/vatesfr/xo-web/issues/1370)
- XenServer connection issue with host while using VGPUs [\#1369](https://github.com/vatesfr/xo-web/issues/1369)
- Job created with v4 are not correctly displayed in v5 [\#1366](https://github.com/vatesfr/xo-web/issues/1366)
- CPU accounting in resource set [\#1365](https://github.com/vatesfr/xo-web/issues/1365)
- Tooltip stay displayed when a button change state [\#1360](https://github.com/vatesfr/xo-web/issues/1360)
- Failure on host reboot [\#1351](https://github.com/vatesfr/xo-web/issues/1351)
- Editing Backup Jobs Without Compression, Slider Always Set To On [\#1339](https://github.com/vatesfr/xo-web/issues/1339)
- Month Selection on Backup Screen Wrong [\#1338](https://github.com/vatesfr/xo-web/issues/1338)
- Delta backup fail when removed VDIs [\#1333](https://github.com/vatesfr/xo-web/issues/1333)
## **5.1.0** (2016-07-26)
### Enhancements

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)
@@ -10,7 +10,7 @@ ___
## Installation
XOA or manual install procedure is [available here](https://github.com/vatesfr/xo/blob/master/doc/installation/README.md)
XOA or manual install procedure is [available here](https://xen-orchestra.com/docs/installation.html)
## Compilation

View File

@@ -2,28 +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
// Port to use for the embedded web server.
//
// Set to 0 to choose a random port at each run.
var SERVER_PORT = LIVERELOAD_PORT + 1
// Address the server should bind to.
//
// - `'localhost'` to make it accessible from this host only
// - `null` to make it accessible for the whole network
var SERVER_ADDR = 'localhost'
var PRODUCTION = process.env.NODE_ENV === 'production'
var DEVELOPMENT = !PRODUCTION
const PRODUCTION = process.env.NODE_ENV === 'production'
const DEVELOPMENT = !PRODUCTION
if (!process.env.XOA_PLAN) {
process.env.XOA_PLAN = '5' // Open Source
@@ -31,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)
}
@@ -48,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) {
@@ -68,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)) {
@@ -92,7 +79,7 @@ var pipe = lazyFn(function () {
}
})
var resolvePath = lazyFn(function () {
const resolvePath = lazyFn(function () {
return require('path').resolve
})
@@ -100,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')()
)
@@ -140,17 +125,13 @@ 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 = {
sourcemaps: {
path: '.'
}
const opts = {
sourcemaps: '.',
}
return PRODUCTION
@@ -158,7 +139,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
}
@@ -171,9 +152,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',
@@ -181,12 +162,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])
}
@@ -194,7 +175,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.
@@ -203,11 +188,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) {
@@ -215,11 +200,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,
})
)
})
}
@@ -249,11 +236,12 @@ function browserify (path, opts) {
gulp.task(function buildPages () {
return pipe(
src('index.jade', { sourcemaps: true }),
require('gulp-jade')(),
DEVELOPMENT && require('gulp-embedlr')({
port: LIVERELOAD_PORT
}),
src('index.pug'),
require('gulp-pug')(),
DEVELOPMENT &&
require('gulp-embedlr')({
port: LIVERELOAD_PORT,
}),
dest()
)
})
@@ -263,12 +251,17 @@ gulp.task(function buildScripts () {
browserify('./index.js', {
plugins: [
// ['css-modulesify', {
['modular-css/browserify', {
css: DIST_DIR + '/modules.css'
}]
]
[
'modular-cssify',
{
css: DIST_DIR + '/modules.css',
from: undefined,
},
],
],
}),
PRODUCTION && require('gulp-uglify')(),
require('gulp-sourcemaps').init({ loadMaps: true }),
PRODUCTION && require('gulp-uglify/composer')(require('uglify-es'))(),
dest()
)
})
@@ -277,10 +270,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()
)
@@ -291,50 +281,23 @@ 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')
)
// -------------------------------------------------------------------
gulp.task(function clean (done) {
require('rimraf')(DIST_DIR, done)
})
// -------------------------------------------------------------------
gulp.task(function server (done) {
require('connect')()
.use(require('serve-static')(DIST_DIR))
.listen(SERVER_PORT, SERVER_ADDR, function onListen () {
var address = this.address()
var port = address.port
address = address.address
// Correctly handle IPv6 addresses.
if (address.indexOf(':') !== -1) {
address = '[' + address + ']'
}
/* jshint devel: true*/
console.log('Listening on http://' + address + ':' + port)
})
.on('error', done)
.on('close', function onClose () {
done()
})
})

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-web",
"version": "5.1.3",
"version": "5.15.1",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -31,102 +31,142 @@
"npm": ">=3"
},
"devDependencies": {
"ansi_up": "^1.3.0",
"asap": "^2.0.4",
"ava": "^0.15.0",
"babel-eslint": "^6.0.0",
"@nraynaud/novnc": "0.6.1",
"ansi_up": "^2.0.2",
"asap": "^2.0.6",
"babel-core": "^6.26.0",
"babel-eslint": "^8.1.2",
"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",
"babel-plugin-transform-react-jsx-self": "^6.11.0",
"babel-plugin-transform-react-jsx-source": "^6.9.0",
"babel-plugin-transform-runtime": "^6.6.0",
"babel-preset-es2015": "^6.6.0",
"babel-preset-env": "^1.6.1",
"babel-preset-react": "^6.5.0",
"babel-preset-stage-0": "^6.5.0",
"babel-runtime": "^6.6.1",
"babelify": "^7.2.0",
"babel-preset-stage-0": "^6.24.1",
"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-plugin-legend": "^0.3.1",
"bootstrap": "4.0.0-alpha.5",
"browserify": "^15.1.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",
"connect": "^3.4.1",
"complex-matcher": "^0.1.1",
"cookies-js": "^1.2.2",
"d3": "^4.0.0-alpha.50",
"dependency-check": "^2.5.1",
"font-awesome": "^4.5.0",
"font-mfizz": "github:fizzed/font-mfizz",
"ghooks": "^1.1.1",
"globby": "^6.0.0",
"gulp": "github:gulpjs/gulp#4.0",
"gulp-autoprefixer": "^3.1.0",
"gulp-csso": "^2.0.0",
"d3": "^4.12.2",
"dependency-check": "^2.9.2",
"enzyme": "^3.3.0",
"enzyme-adapter-react-15": "^1.0.5",
"enzyme-to-json": "^3.3.0",
"eslint": "^4.14.0",
"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": "^4.0.0",
"gulp-autoprefixer": "^4.1.0",
"gulp-csso": "^3.0.0",
"gulp-embedlr": "^0.5.2",
"gulp-jade": "^1.1.0",
"gulp-plumber": "^1.1.0",
"gulp-pug": "^3.1.0",
"gulp-refresh": "^1.1.0",
"gulp-sass": "^2.2.0",
"gulp-uglify": "^2.0.0",
"gulp-watch": "^4.3.5",
"human-format": "^0.6.0",
"jsonrpc-websocket-client": "0.0.1-5",
"gulp-sass": "^3.0.0",
"gulp-sourcemaps": "^2.6.2",
"gulp-uglify": "^3.0.0",
"gulp-watch": "^5.0.0",
"human-format": "^0.10.0",
"husky": "^0.14.3",
"immutable": "^3.8.2",
"index-modules": "^0.3.0",
"is-ip": "^2.0.0",
"jest": "^22.0.4",
"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",
"marked": "^0.3.5",
"modular-css": "^0.25.0",
"moment": "^2.13.0",
"moment-timezone": "^0.5.4",
"notifyjs": "^2.0.1",
"novnc-node": "^0.5.3",
"promise-toolbox": "^0.4.0",
"make-error": "^1.3.2",
"marked": "^0.3.9",
"modular-cssify": "^7.2.0",
"moment": "^2.20.1",
"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.2.0",
"react-notify": "^2.0.1",
"react-redux": "^4.4.0",
"react-router": "^3.0.0-alpha.1",
"react-select": "^1.0.0-beta13",
"react-sparklines": "^1.5.0",
"react-virtualized": "^7.4.0",
"readable-stream": "^2.0.6",
"redux": "^3.3.1",
"redux-devtools": "^3.1.1",
"redux-devtools-dock-monitor": "^1.1.0",
"redux-devtools-log-monitor": "^1.0.5",
"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.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.3.3",
"redux": "^3.7.2",
"redux-thunk": "^2.0.1",
"reselect": "^2.2.1",
"serve-static": "^1.10.2",
"standard": "^7.0.0",
"superagent": "^2.0.0",
"vinyl": "^1.1.1",
"reselect": "^2.5.4",
"semver": "^5.4.1",
"styled-components": "^2.4.0",
"tar-stream": "^1.5.5",
"uglify-es": "^3.3.4",
"uncontrollable-input": "^0.1.1",
"url-parse": "^1.2.0",
"vinyl": "^2.1.0",
"watchify": "^3.7.0",
"xo-acl-resolver": "^0.2.1",
"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"
},
"scripts": {
"benchmarks": "./tools/run-benchmarks.js 'src/**/*.bench.js'",
"build": "npm run build-indexes && NODE_ENV=production gulp build",
"build-indexes": "./tools/generate-index src/common/intl/locales",
"dev": "npm run build-indexes && gulp build server",
"dev-test": "ava --watch",
"lint": "standard",
"posttest": "npm run lint",
"prepublish": "npm run build",
"test": "ava"
"build-indexes": "index-modules --auto src",
"clean": "gulp clean",
"dev": "npm run build-indexes && NODE_ENV=development gulp build",
"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/",
"prebuild": "npm run clean",
"precommit": "lint-staged",
"predev": "npm run clean",
"prepublishOnly": "npm run build",
"test": "jest"
},
"browserify": {
"transform": [
@@ -134,17 +174,14 @@
"loose-envify"
]
},
"ava": {
"babel": "inherit",
"files": [
"src/**/*.spec.js"
],
"require": [
"babel-register"
]
},
"babel": {
"env": {
"development": {
"plugins": [
"transform-react-jsx-self",
"transform-react-jsx-source"
]
},
"production": {
"plugins": [
"transform-react-constant-elements",
@@ -153,24 +190,38 @@
}
},
"plugins": [
"dev",
"lodash",
"transform-decorators-legacy",
"transform-runtime"
],
"presets": [
"es2015",
[
"env",
{
"targets": {
"browsers": ">2%"
}
}
],
"react",
"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

@@ -4,16 +4,16 @@
$ct-series-colors: (
$brand-success,
$brand-primary,
#60bd68,
#f17cb0,
#b2912f,
#b276b2,
#decf3f,
#f15854,
#4d4d4d,
#dda458,
#eacf7d,
#86797d,
#b276b2,
#f15854,
#b2912f,
#decf3f,
#dda458,
#60bd68,
#4d4d4d,
#eacf7d,
#b2c326,
#6188e2,
#a748ca
@@ -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,36 +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 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, ({ handler, handlerParam = param, label, icon }, index) => (
<Tooltip key={index} content={_(label)}>
<ActionButton
key={index}
btnStyle='secondary'
handler={handler}
handlerParam={handlerParam}
icon={icon}
size='large'
/>
</Tooltip>
))}
{React.Children.map(children, (child, key) => {
if (!child) {
return
}
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
})
).isRequired,
display: React.PropTypes.oneOf(['icon', 'text', 'both'])
display: propTypes.oneOf(['icon', 'both']),
handlerParam: propTypes.any,
}
export { ActionBar as default }

View File

@@ -1,68 +1,95 @@
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'
])
// 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,
})
logError(error)
// 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)
)
}
}
}
_execute = ::this._execute
@@ -76,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)
}
}
@@ -84,36 +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
},
state: { error, working }
props: { children, icon, pending, tooltip, ...props },
state: { error, working },
} = this
return <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
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

@@ -0,0 +1,29 @@
import map from 'lodash/map'
import React from 'react'
const call = fn => fn()
// `subscriptions` can be a function if we want to ensure that the subscription
// callbacks have been correctly initialized when there are circular dependencies
const addSubscriptions = subscriptions => Component =>
class SubscriptionWrapper extends React.PureComponent {
_unsubscribes = null
componentWillMount () {
this._unsubscribes = map(
typeof subscriptions === 'function' ? subscriptions(this.props) : subscriptions,
(subscribe, prop) =>
subscribe(value => this.setState({ [prop]: value }))
)
}
componentWillUnmount () {
this._unsubscribes.forEach(call)
this._unsubscribes = null
}
render () {
return <Component {...this.props} {...this.state} />
}
}
export { addSubscriptions as default }

View File

@@ -1,11 +1,26 @@
import forEach from 'lodash/forEach'
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'
export default class BaseComponent extends Component {
// Should components logs every renders?
//
// Usually set to process.env.NODE_ENV !== 'production'.
const VERBOSE = false
const get = (object, path, depth) => {
if (depth >= path.length) {
return object
}
const prop = path[depth++]
return isArray(object) && prop === '*'
? map(object, value => get(value, path, depth))
: get(object[prop], path, depth)
}
export default class BaseComponent extends PureComponent {
constructor (props, context) {
super(props, context)
@@ -14,41 +29,74 @@ export default class BaseComponent extends Component {
this._linkedState = null
if (process.env.NODE_ENV !== 'production') {
this.render = invoke(this.render, render => () => {
if (VERBOSE) {
this.render = (render => () => {
console.log('render', this.constructor.name)
return render.call(this)
})
})(this.render)
}
}
// See https://preactjs.com/guide/linked-state
linkState (name) {
linkState (name, targetPath) {
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[name])) {
} else if ((cb = linkedState[key]) !== undefined) {
return cb
}
return (linkedState[name] = event => {
let getValue
if (targetPath !== undefined) {
const path = targetPath.split('.')
getValue = event => get(getEventValue(event), path, 0)
} else {
getValue = getEventValue
}
if (includes(name, '.')) {
const path = name.split('.')
return (linkedState[key] = event => {
this.setState(cowSet(this.state, path, getValue(event), 0))
})
}
return (linkedState[key] = event => {
this.setState({
[name]: getEventValue(event)
[name]: getValue(event),
})
})
}
shouldComponentUpdate (newProps, newState) {
return !(
shallowEqual(this.props, newProps) &&
shallowEqual(this.state, newState)
)
toggleState (name) {
let linkedState = this._linkedState
let cb
if (linkedState === null) {
linkedState = this._linkedState = {}
} else if ((cb = linkedState[name]) !== undefined) {
return cb
}
if (includes(name, '.')) {
const path = name.split('.')
return (linkedState[path] = event => {
this.setState(cowSet(this.state, path, !get(this.state, path, 0), 0))
})
}
return (linkedState[name] = () => {
this.setState({
[name]: !this.state[name],
})
})
}
}
if (process.env.NODE_ENV !== 'production') {
if (VERBOSE) {
const diff = (name, old, cur) => {
const keys = []

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

126
src/common/computed.js Normal file
View File

@@ -0,0 +1,126 @@
import React, { PureComponent } from 'react'
const {
create,
defineProperty,
defineProperties,
getOwnPropertyDescriptors = obj => {
const descriptors = {}
const { getOwnPropertyDescriptor } = Object
for (const prop in obj) {
const descriptor = getOwnPropertyDescriptor(obj, prop)
if (descriptor !== undefined) {
descriptors[prop] = descriptor
}
}
return descriptors
},
prototype: { hasOwnProperty },
} = Object
const makePropsSpy =
typeof Proxy !== 'undefined'
? (obj, spy) =>
new Proxy(obj, {
get: (target, property) => (spy[property] = target[property]),
})
: (obj, spy) => {
const descriptors = {}
const props = getOwnPropertyDescriptors(obj)
for (const prop in props) {
const { configurable, enumerable, get, value } = props[prop]
descriptors[prop] = {
configurable,
enumerable,
get:
get !== undefined
? () => (spy[prop] = get.call(obj))
: () => (spy[prop] = value),
}
}
return create(null, descriptors)
}
// Decorator which provides computed properties for React components.
//
// ```js
// const MyComponent = computed({
// fullName: ({ firstName, lastName }) => `${lastName}, ${firstName}`
// })(({ fullName }) =>
// <p>{fullName}</p>
// )
// ```
const computed = computed => Component =>
class extends PureComponent {
constructor () {
super()
this._computedCache = create(null)
this._computedDeps = create(null)
const descriptors = (this._descriptors = {})
for (const name in computed) {
if (!hasOwnProperty.call(computed, name)) {
continue
}
const transform = computed[name]
let running = false
descriptors[name] = {
configurable: true,
enumerable: true,
get: () => {
// this is necessary to allow a computed value to depend on
// itself
if (running) {
console.log(name, 'running')
return this.props[name]
}
const cache = this._computedCache
const deps = this._computedDeps
const dependencies = deps[name]
let needsRecompute = dependencies === undefined
if (!needsRecompute) {
const { props } = this
for (const depName in dependencies) {
const value =
depName === name || !(depName in cache)
? props[depName]
: cache[depName]
needsRecompute = value !== dependencies[depName]
if (needsRecompute) {
break
}
}
}
console.log(name, needsRecompute)
if (needsRecompute) {
running = true
cache[name] = transform(
makePropsSpy(this._props, (deps[name] = create(null)))
)
running = false
}
const value = cache[name]
defineProperty(this._props, name, {
enumerable: true,
value,
})
return value
},
}
}
}
render () {
this._props = defineProperties({ ...this.props }, this._descriptors)
return <Component {...this._props} />
}
}
export { computed as default }

View File

@@ -2,26 +2,33 @@ import CopyToClipboard from 'react-copy-to-clipboard'
import classNames from 'classnames'
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
})(props => createElement(
props.tagName || 'span',
{
...props,
className: classNames(styles.container, props.className)
},
props.children,
' ',
<CopyToClipboard text={props.data || props.children}>
<button className={classNames('btn btn-sm btn-secondary', styles.button)}>
<Icon icon='clipboard' />
</button>
</CopyToClipboard>
))
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 { '}
return (
<pre>
{'Promise { '}
{status === 'rejected' && '<rejected> '}
{toString(value)}
{' }'}
</pre>
{' }'}
</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

@@ -0,0 +1,22 @@
@value dropzoneColor: #8f8686;
.dropzone {
border-radius: 4px;
border: 2px dashed dropzoneColor;
cursor: pointer;
display: flex;
height: 12em;
margin-bottom: 20px;
width: 100%;
}
.activeDropzone {
background: #f0f0f0;
border-style: solid;
}
.dropzoneText {
color: dropzoneColor;
font-size: 1.2em;
margin: auto;
}

View File

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

View File

@@ -0,0 +1,13 @@
.clickToEdit * {
cursor: context-menu !important;
}
.shortClick {
border-bottom: 1px dashed #ccc;
}
.select {
padding: 0px;
}
.size {
width: 10rem;
}

View File

@@ -1,3 +1,4 @@
import classNames from 'classnames'
import findKey from 'lodash/findKey'
import isFunction from 'lodash/isFunction'
import isString from 'lodash/isString'
@@ -5,46 +6,43 @@ import map from 'lodash/map'
import pick from 'lodash/pick'
import React from 'react'
import _ from './intl'
import Component from './base-component'
import Icon from './icon'
import logError from './log-error'
import propTypes from './prop-types'
import Tooltip from './tooltip'
import { formatSize } from './utils'
import { SizeInput } from './form'
import _ from '../intl'
import Component from '../base-component'
import getEventValue from '../get-event-value'
import Icon from '../icon'
import logError from '../log-error'
import propTypes from '../prop-types-decorator'
import Tooltip from '../tooltip'
import { formatSize } from '../utils'
import { SizeInput } from '../form'
import {
SelectHost,
SelectIp,
SelectNetwork,
SelectPool,
SelectRemote,
SelectResourceSetIp,
SelectSr,
SelectSubject,
SelectTag,
SelectVgpuType,
SelectVm,
SelectVmTemplate
} from './select-objects'
SelectVmTemplate,
} from '../select-objects'
import styles from './index.css'
const LONG_CLICK = 400
const SELECT_STYLE = { padding: '0px' }
const SIZE_STYLE = { width: '10rem' }
const EDITABLE_STYLE = {
borderBottom: '1px dashed #ccc',
cursor: 'context-menu'
}
const LONG_EDITABLE_STYLE = {
cursor: 'context-menu'
}
@propTypes({
alt: propTypes.node.isRequired
alt: propTypes.node.isRequired,
})
class Hover extends Component {
constructor () {
super()
this.state = {
hover: false
hover: false,
}
this._onMouseEnter = () => this.setState({ hover: true })
@@ -53,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 () {
@@ -97,7 +88,7 @@ class Editable extends Component {
this.setState({
editing: true,
error: null,
saving: false
saving: false,
})
}
@@ -115,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) {
@@ -139,8 +127,9 @@ class Editable extends Component {
this._closeEdition()
} catch (error) {
this.setState({
error: isString(error) ? error : error.message,
saving: false
// `error` may be undefined if the action has been cancelled
error: error !== undefined && (isString(error) ? error : error.message),
saving: false,
})
logError(error)
}
@@ -163,34 +152,59 @@ class Editable extends Component {
const { useLongClick } = props
const success = <Icon icon='success' />
return <span style={useLongClick ? LONG_EDITABLE_STYLE : EDITABLE_STYLE}>
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 ? undefined : this._openEdition}
onMouseDown={useLongClick ? this.__startTimer : undefined}
onMouseUp={useLongClick ? this.__stopTimer : undefined}
>
{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>
)
}
}
@@ -199,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 () {
@@ -219,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 () {
@@ -249,24 +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`
}}
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'}
/>
)
}
}
@@ -278,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 () {
@@ -301,117 +314,117 @@ 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({
labelProp: propTypes.string.isRequired,
options: propTypes.oneOfType([
propTypes.array,
propTypes.object
]).isRequired
options: propTypes.oneOfType([propTypes.array, propTypes.object]).isRequired,
renderer: propTypes.func,
})
export class Select extends Editable {
constructor (props) {
super()
this._defaultValue = findKey(props.options, option => option === props.value)
componentWillReceiveProps (props) {
if (
props.value !== this.props.value ||
props.options !== this.props.options
) {
this.setState({
valueKey: findKey(props.options, option => option === props.value),
})
}
}
get value () {
return this.props.options[this._select.value]
return this.props.options[this.state.valueKey]
}
_onChange = event => {
this._save()
this.setState({ valueKey: getEventValue(event) }, this._save)
}
_optionToJsx = (option, index) => {
const { labelProp } = this.props
return <option
key={index}
value={index}
>
{labelProp ? option[labelProp] : option}
</option>
_optionToJsx = (option, key) => {
const { renderer } = this.props
return (
<option key={key} value={key}>
{renderer ? renderer(option) : option}
</option>
)
}
_onEditionMount = ref => {
this._select = ref
// Seems to work in Google Chrome (not in Firefox)
ref && ref.dispatchEvent(new window.MouseEvent('mousedown'))
}
_renderDisplay () {
return this.props.children ||
<span>{this.props.value[this.props.labelProp]}</span>
const { children, renderer, value } = this.props
return children || <span>{renderer ? renderer(value) : value}</span>
}
_renderEdition () {
const { saving } = this.state
const { saving, valueKey } = this.state
const { options } = this.props
return <select
autoFocus
className='form-control'
defaultValue={this._defaultValue}
onBlur={this._closeEdition}
onChange={this._onChange}
onKeyDown={this._onKeyDown}
readOnly={saving}
ref={this._onEditionMount}
style={SELECT_STYLE}
>
{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>
)
}
}
const MAP_TYPE_SELECT = {
host: SelectHost,
ip: SelectIp,
network: SelectNetwork,
pool: SelectPool,
remote: SelectRemote,
resourceSetIp: SelectResourceSetIp,
SR: SelectSr,
subject: SelectSubject,
tag: SelectTag,
vgpuType: SelectVgpuType,
VM: SelectVm,
'VM-template': SelectVmTemplate
'VM-template': SelectVmTemplate,
}
@propTypes({
labelProp: propTypes.string.isRequired,
predicate: propTypes.func,
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 {
placeholder,
predicate,
saving,
xoType
} = this.props
const { saving, xoType, ...props } = this.props
const Select = MAP_TYPE_SELECT[xoType]
if (process.env.NODE_ENV !== 'production') {
@@ -422,21 +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
autoFocus
disabled={saving}
onChange={this._onChange}
placeholder={placeholder}
predicate={predicate}
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 () {
@@ -454,24 +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
onBlur={this._closeEditionIfUnfocused}
onFocus={this._focus}
onKeyDown={this._onKeyDown}
>
<SizeInput
autoFocus
ref='input'
readOnly={saving}
style={SIZE_STYLE}
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 () {
@@ -36,114 +34,98 @@ export class Password extends Component {
}
_generate = () => {
this.refs.field.value = randomPassword(8)
const value = randomPassword(8)
const isControlled = this.props.value !== undefined
if (isControlled) {
this.props.onChange(value)
} else {
this.refs.field.value = value
}
// FIXME: in controlled mode, visibility should only be updated
// when the value prop is changed according to the emitted value.
this.setState({
visible: true
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) {
const { onChange } = this.props
this.state.value = +value
if (onChange) {
onChange(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>
)
}
}
@@ -162,86 +144,121 @@ const DEFAULT_UNIT = 'GiB'
readOnly: propTypes.bool,
required: propTypes.bool,
style: propTypes.object,
value: propTypes.number
value: propTypes.oneOfType([propTypes.number, propTypes.oneOf([null])]),
})
export class SizeInput extends BaseComponent {
constructor (props) {
super(props)
this.state = this._createStateFromBytes(firstDefined(props.value, props.defaultValue, 0))
this.state = this._createStateFromBytes(
defined(props.value, props.defaultValue, null)
)
}
componentWillReceiveProps (newProps) {
const { value } = newProps
if (value == null && value === this.props.value) {
return
}
const { _bytes, _unit, _value } = this
this._bytes = this._unit = this._value = null
if (value === _bytes) {
// Update input value
this.setState({
unit: _unit,
value: _value
})
} else {
componentWillReceiveProps (props) {
const { value } = props
if (value !== undefined && value !== this.props.value) {
this.setState(this._createStateFromBytes(value))
}
}
_createStateFromBytes = bytes => {
const humanSize = bytes && formatSizeRaw(bytes)
_createStateFromBytes (bytes) {
if (bytes === this._bytes) {
return {
input: this._input,
unit: this._unit,
}
}
if (bytes === null) {
return {
input: '',
unit: this.props.defaultUnit || DEFAULT_UNIT,
}
}
const { prefix, value } = formatSizeRaw(bytes)
return {
unit: humanSize && humanSize.value ? humanSize.prefix + 'B' : this.props.defaultUnit || DEFAULT_UNIT,
value: humanSize ? round(humanSize.value, 3) : ''
input: String(round(value, 2)),
unit: `${prefix}B`,
}
}
get value () {
const { unit, value } = this.state
return parseSize(value + ' ' + unit)
const { input, unit } = this.state
if (!input) {
return null
}
return parseSize(`${+input} ${unit}`)
}
set value (newValue) {
set value (value) {
if (
process.env.NODE_ENV !== 'production' &&
this.props.value != null
this.props.value !== undefined
) {
throw new Error('cannot set value of controlled SizeInput')
}
this.setState(this._createStateFromBytes(newValue))
this.setState(this._createStateFromBytes(value))
}
_onChange = value =>
this.props.onChange && this.props.onChange(value)
_onChange (input, unit) {
const { onChange } = this.props
_updateValue = event => {
const { value } = event.target
if (this.props.value != null) {
this._value = value
this._unit = this.state.unit
this._bytes = parseSize((value || 0) + ' ' + this.state.unit)
// Empty input equals null.
const bytes = input ? parseSize(`${+input} ${unit}`) : null
this._onChange(this._bytes)
} else {
this.setState({ value }, () => {
this._onChange(this.value)
})
}
}
_updateUnit = unit => {
if (this.props.value != null) {
this._value = this.state.value
const isControlled = this.props.value !== undefined
if (isControlled) {
// Store input and unit for this change to update correctly on new
// props.
this._bytes = bytes
this._input = input
this._unit = unit
this._bytes = parseSize((this.state.value || 0) + ' ' + unit)
this._onChange(this._bytes)
} else {
this.setState({ unit }, () => {
this._onChange(this.value)
})
this.setState({ input, unit })
// onChange is optional in uncontrolled mode.
if (!onChange) {
return
}
}
onChange(bytes)
}
_updateNumber = event => {
const input = event.target.value
if (!input) {
return this._onChange(input, this.state.unit)
}
const number = +input
if (Number.isNaN(number)) {
return
}
// Same numeric value: simply update the input.
const prevInput = this.state.input
if (prevInput && +prevInput === number) {
return this.setState({ input })
}
this._onChange(input, this.state.unit)
}
_updateUnit = unit => {
const { input } = this.state
// 0 is always 0, no matter the unit.
if (+input) {
this._onChange(input, unit)
} else {
this.setState({ unit })
}
}
@@ -249,50 +266,40 @@ export class SizeInput extends BaseComponent {
const {
autoFocus,
className,
placeholder,
readOnly,
placeholder,
required,
style
style,
} = this.props
const {
value,
unit
} = this.state
return <span
className={classNames(className, 'input-group')}
style={style}
>
<input
autoFocus={autoFocus}
className='form-control'
min={0}
onChange={this._updateValue}
placeholder={placeholder}
readOnly={readOnly}
required={required}
type='number'
value={value}
/>
<span className='input-group-btn'>
<DropdownButton
bsStyle='secondary'
return (
<span className={classNames('input-group', className)} style={style}>
<input
autoFocus={autoFocus}
className='form-control'
disabled={readOnly}
id='size'
pullRight
title={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
@@ -101,7 +101,7 @@ export default class SelectPlainObject extends Component {
return (
<Select
autofocus={props.autoFocus}
autoFocus={props.autoFocus}
disabled={props.disabled}
multi={props.multi}
onChange={this._handleChange}
@@ -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

@@ -1,66 +1,89 @@
import map from 'lodash/map'
import React, { Component } from 'react'
import ReactSelect from 'react-select'
import {
AutoSizer,
VirtualScroll
} from 'react-virtualized'
import sum from 'lodash/sum'
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,
optionHeight: propTypes.number
})
export default class Select extends Component {
static defaultProps = {
maxHeight: 200,
optionHeight: 40,
optionRenderer: (option, labelKey) => option[labelKey]
optionRenderer: (option, labelKey) => option[labelKey],
}
_renderMenu = ({
focusedOption,
options,
...otherOptions
}) => {
const {
maxHeight,
optionHeight
} = this.props
_renderMenu = ({ focusedOption, options, ...otherOptions }) => {
const { maxHeight } = this.props
const focusedOptionIndex = options.indexOf(focusedOption)
const height = Math.min(maxHeight, options.length * optionHeight)
let height = options.length > MAX_OPTIONS && maxHeight
const wrappedRowRenderer = ({ index }) =>
const wrappedRowRenderer = ({ index, key, style }) =>
this._optionRenderer({
...otherOptions,
focusedOption,
focusedOptionIndex,
key,
option: options[index],
options
options,
style,
})
return (
<AutoSizer disableHeight>
{({ width }) => (
<VirtualScroll
height={height}
rowCount={options.length}
rowHeight={optionHeight}
rowRenderer={wrappedRowRenderer}
scrollToIndex={focusedOptionIndex}
width={width}
/>
)}
{({ width }) =>
width ? (
<CellMeasurer
cellRenderer={({ rowIndex }) =>
wrappedRowRenderer({ index: rowIndex })
}
columnCount={1}
rowCount={options.length}
// FIXME: 16 px: ugly workaround to take into account the scrollbar
// during the offscreen render to measure the row height
// See https://github.com/bvaughn/react-virtualized/issues/401
width={width - 16}
>
{({ getRowHeight }) => {
if (options.length <= MAX_OPTIONS) {
height = sum(
map(options, (_, index) => getRowHeight({ index }))
)
}
return (
<List
height={height}
rowCount={options.length}
rowHeight={getRowHeight}
rowRenderer={wrappedRowRenderer}
scrollToIndex={focusedOptionIndex}
style={LIST_STYLE}
width={width}
/>
)
}}
</CellMeasurer>
) : null
}
</AutoSizer>
)
}
@@ -68,9 +91,11 @@ export default class Select extends Component {
_optionRenderer = ({
focusedOption,
focusOption,
key,
labelKey,
option,
selectValue
style,
selectValue,
}) => {
let className = 'Select-option'
@@ -89,9 +114,10 @@ export default class Select extends Component {
return (
<div
className={className}
onClick={!disabled && (() => selectValue(option))}
onMouseOver={!disabled && (() => focusOption(option))}
style={{ height: props.optionHeight }}
onClick={disabled ? undefined : () => selectValue(option)}
onMouseOver={disabled ? undefined : () => focusOption(option)}
style={style}
key={key}
>
{props.optionRenderer(option, labelKey)}
</div>
@@ -101,7 +127,9 @@ export default class Select extends Component {
render () {
return (
<ReactSelect
closeOnSelect={!this.props.multi}
{...this.props}
backspaceToRemoveMessage=''
menuRenderer={this._renderMenu}
menuStyle={SELECT_MENU_STYLE}
style={SELECT_STYLE}

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.isRequired,
icon: propTypes.string,
iconOn: propTypes.string,
iconOff: propTypes.string,
iconSize: propTypes.number,
value: propTypes.bool.isRequired,
})
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,16 +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 = {
...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,25 +1,24 @@
import isEmpty from 'lodash/isEmpty'
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
installAllHostPatches,
installAllPatchesOnPool,
subscribeHostMissingPatches,
} from './xo'
// ===================================================================
@@ -27,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'),
@@ -49,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)
@@ -78,105 +93,102 @@ 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
)
)
_installAllMissingPatches = () => (
Promise.all(map(this._getHosts(), this._installAllHostPatches))
)
if (this.unsubscribeMissingPatches !== undefined) {
this.unsubscribeMissingPatches()
}
_refreshHostMissingPatches = host => (
getHostMissingPatches(host).then(patches => {
this.setState({
missingPatches: {
...this.state.missingPatches,
[host.id]: patches.length
}
})
this.unsubscribeMissingPatches = () => forEach(unsubs, unsub => unsub())
}
_installAllMissingPatches = () => {
const pools = {}
forEach(this._getHosts(), host => {
pools[host.$pool] = true
})
)
_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
? (
{!noPatches ? (
<SortedTable
collection={hosts}
columns={props.displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
columns={
displayPools
? POOLS_MISSING_PATCHES_COLUMNS
: MISSING_PATCHES_COLUMNS
}
userData={{
installAllHostPatches: this._installAllHostPatches,
installAllHostPatches,
missingPatches: this.state.missingPatches,
pools: props.pools
pools,
}}
/>
) : <p>{_('patchNothing')}</p>
}
<Portal container={() => props.buttonsGroupContainer()}>
{Buttons}
) : (
<p>{_('patchNothing')}</p>
)}
<Portal container={() => buttonsGroupContainer()}>
<Container>
<Button
btnStyle='primary'
disabled={noPatches}
handler={this._installAllMissingPatches}
icon='host-patch-update'
labelId='installPoolPatches'
/>
</Container>
</Portal>
</div>
)
@@ -189,7 +201,7 @@ class HostsPatchesTable extends Component {
const getPools = createGetObjectsOfType('pool')
return {
pools: getPools
pools: getPools,
}
})
class HostsPatchesTableByPool extends Component {
@@ -207,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

View File

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

136
src/common/ip.js Normal file
View File

@@ -0,0 +1,136 @@
import forEachRight from 'lodash/forEachRight'
import forEach from 'lodash/forEach'
import isArray from 'lodash/isArray'
import isIp from 'is-ip'
import some from 'lodash/some'
export { isIp }
export const isIpV4 = isIp.v4
export const isIpV6 = isIp.v6
// Source: https://github.com/ezpaarse-project/ip-range-generator/blob/master/index.js
const ipv4 = /^(?:(?:[0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(?:\.(?!$)|$)){4}$/
function ip2hex (ip) {
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[0] * 16777216 // 2^24
return n
}
function assertIpv4 (str, msg) {
if (!ipv4.test(str)) {
throw new Error(msg)
}
}
function * range (ip1, ip2) {
assertIpv4(ip1, 'argument "ip1" must be a valid IPv4 address')
assertIpv4(ip2, 'argument "ip2" must be a valid IPv4 address')
let hex = ip2hex(ip1)
let hex2 = ip2hex(ip2)
if (hex > hex2) {
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}`
}
}
// -----------------------------------------------------------------------------
export const getNextIpV4 = ip => {
const splitIp = ip.split('.')
if (
splitIp.length !== 4 ||
some(splitIp, value => value < 0 || value > 255)
) {
return
}
let index
forEachRight(splitIp, (value, i) => {
if (value < 255) {
index = i
return false
}
splitIp[i] = 1
})
if (index === 0 && +splitIp[0] === 255) {
return 0
}
splitIp[index]++
return splitIp.join('.')
}
export const formatIps = ips => {
if (!isArray(ips)) {
throw new Error('ips must be an array')
}
if (ips.length === 0) {
return []
}
const sortedIps = ips.sort((ip1, ip2) => {
const splitIp1 = ip1.split('.')
const splitIp2 = ip2.split('.')
if (splitIp1.length !== 4) {
return 1
}
if (splitIp2.length !== 4) {
return -1
}
return (
splitIp1[3] -
splitIp2[3] +
(splitIp1[2] - splitIp2[2]) * 256 +
(splitIp1[1] - splitIp2[1]) * 256 * 256 +
(splitIp1[0] - splitIp2[0]) * 256 * 256 * 256
)
})
const range = { first: '', last: '' }
const formattedIps = []
let index = 0
forEach(sortedIps, ip => {
if (ip !== getNextIpV4(range.last)) {
if (range.first) {
formattedIps[index] =
range.first === range.last ? range.first : { ...range }
index++
}
range.first = range.last = ip
} else {
range.last = ip
}
})
formattedIps[index] = range.first === range.last ? range.first : range
return formattedIps
}
export const parseIpPattern = pattern => {
const ips = []
forEach(pattern.split(';'), rawIpRange => {
const ipRange = rawIpRange.split('-')
if (ipRange.length < 2) {
ips.push(ipRange[0])
} else if (!isIpV4(ipRange[0]) || !isIpV4(ipRange[1])) {
ips.push(rawIpRange)
} else {
ips.push(...range(ipRange[0], ipRange[1]))
}
})
return ips
}

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-xs-right' type='button' onClick={this.props.onDelete}>
{_('remove')}
</button>
</li>
)
}
}
// ===================================================================
import { descriptionRender, forceDisplayOptionalAttr } from './helpers'
@propTypes({
depth: propTypes.number,
@@ -48,138 +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.state = {
use: props.required || forceDisplayOptionalAttr(props),
children: this._makeChildren(props)
}
this._nextChildKey = 0
@uncontrollableInput()
export default class ObjectInput extends Component {
state = {
use: this.props.required || forceDisplayOptionalAttr(this.props),
}
get value () {
if (this.state.use) {
return map(this.refs, 'value')
}
_onAddItem = () => {
const { props } = this
props.onChange((props.value || EMPTY_ARRAY).concat(undefined))
}
set value (value = []) {
this.setState({
children: this._makeChildren({ ...this.props, value })
})
_onChangeItem = (value, name) => {
const key = Number(name)
const { props } = this
const newValue = (props.value || EMPTY_ARRAY).slice()
newValue[key] = value
props.onChange(newValue)
}
_handleOptionalChange = event => {
this.setState({
use: event.target.checked
})
}
_handleAdd = () => {
const { children } = this.state
this.setState({
children: children.concat(this._makeChild(this.props))
})
}
_remove (key) {
this.setState({
children: filter(this.state.children, child => child.key !== key)
})
}
_makeChild (props) {
const key = String(this._nextChildKey++)
const {
schema: {
items
}
} = props
return (
<ArrayItem key={key} onDelete={() => { this._remove(key) }}>
<GenericInput
depth={props.depth}
disabled={props.disabled}
label={items.title || _('item')}
required
schema={items}
uiSchema={props.uiSchema.items}
defaultValue={props.defaultValue}
/>
</ArrayItem>
)
}
_makeChildren ({ defaultValue, ...props }) {
return map(defaultValue, defaultValue => {
return (
this._makeChild({
...props,
defaultValue
})
)
})
}
componentWillReceiveProps (props) {
if (
!propsEqual(
this.props,
props,
[ 'depth', 'disabled', 'label', 'required', 'schema', 'uiSchema' ]
)
) {
this.setState({
children: this._makeChildren(props)
})
}
_onRemoveItem = key => {
const { props } = this
props.onChange(filter(props.value, (_, i) => i !== key))
}
render () {
const {
props,
state
props: {
depth = 0,
disabled,
label,
required,
schema,
uiSchema,
value = EMPTY_ARRAY,
},
state: { use },
} = this
const {
disabled,
schema
} = props
const { use } = state
const depth = props.depth || 0
const childDepth = depth + 2
const itemSchema = schema.items
const itemUiSchema = uiSchema && uiSchema.items
const itemLabel = itemSchema.title || _('item')
return (
<div style={{'paddingLeft': `${depth}em`}}>
<legend>{props.label}</legend>
<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-xs-right m-t-1 m-r-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='p-b-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,11 +1,18 @@
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,
} from './shortcuts'
let instance
@@ -15,31 +22,100 @@ const modal = (content, onClose) => {
} else if (instance.state.showModal) {
throw new Error('Other modal still open.')
}
instance.setState({ content, onClose, showModal: true })
instance.setState({ content, onClose, showModal: true }, disableShortcuts)
}
export const alert = (title, body) => {
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)) {
@@ -52,75 +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.value || body.getWrappedInstance && body.getWrappedInstance().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
)
})
@@ -130,18 +159,22 @@ export default class Modal extends Component {
constructor () {
super()
this.state = { showModal: false }
}
componentDidMount () {
if (instance) {
throw new Error('Modal is a singleton!')
}
instance = this
}
componentWillMount () {
this.setState({ showModal: false })
componentWillUnmount () {
instance = undefined
}
close () {
this.setState({ showModal: false })
this.setState({ showModal: false }, enableShortcuts)
}
_onHide = () => {
@@ -152,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>
)

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

@@ -0,0 +1,41 @@
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 = props => {
const { collection } = props
if (collection == null) {
return <img src='assets/loading.svg' alt='loading' />
}
if (isEmpty(collection)) {
return <p>{props.emptyMessage}</p>
}
const { children, component: Component, ...otherProps } = props
return children !== undefined ? (
children(otherProps)
) : (
<Component {...otherProps} />
)
}
propTypes(NoObjects)({
children: propTypes.func,
collection: propTypes.oneOfType([propTypes.array, propTypes.object]),
component: propTypes.func,
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 () {

125
src/common/pagination.js Normal file
View File

@@ -0,0 +1,125 @@
import React from 'react'
import PropTypes from 'prop-types'
const PageItem = ({ active, children, disabled, onClick, value }) =>
active ? (
<li className='active page-item'>
<span className='page-link'>{children}</span>
</li>
) : disabled ? (
<li className='disabled page-item'>
<span className='page-link'>{children}</span>
</li>
) : (
<li className='page-item'>
<a className='page-link' href='#' onClick={onClick} data-value={value}>
{children}
</a>
</li>
)
export default class Pagination extends React.PureComponent {
static defaultProps = {
ellipsis: true,
maxButtons: 7,
next: true,
prev: true,
}
static propTypes = {
ariaLabel: PropTypes.string,
ellipsis: PropTypes.bool,
maxButtons: PropTypes.number,
next: PropTypes.bool,
onChange: PropTypes.func.isRequired,
pages: PropTypes.number.isRequired,
prev: PropTypes.bool,
value: PropTypes.number.isRequired,
}
_onClick (event) {
event.preventDefault()
this.props.onChange(+event.currentTarget.dataset.value)
}
_onClick = this._onClick.bind(this)
render () {
const {
ariaLabel,
ellipsis,
maxButtons,
next,
pages,
prev,
value,
} = this.props
const onClick = this._onClick
let min, max
if (pages <= maxButtons) {
min = 1
max = pages
} else {
min = Math.max(
1,
Math.min(value - Math.floor(maxButtons / 2), pages - maxButtons + 1)
)
max = min + maxButtons - 1
}
const pageButtons = []
if (ellipsis && min !== 1) {
pageButtons.push(
<PageItem disabled key='firstEllipsis'>
</PageItem>
)
}
for (let page = min; page <= max; ++page) {
pageButtons.push(
<PageItem
active={page === value}
key={page}
onClick={onClick}
value={page}
>
{page}
</PageItem>
)
}
if (ellipsis && max !== pages) {
pageButtons.push(
<PageItem disabled key='lastEllipsis'>
</PageItem>
)
}
return (
<nav aria-label={ariaLabel}>
<ul className='pagination'>
{prev && (
<PageItem
aria-label='Previous'
disabled={value === 1}
onClick={onClick}
value={value - 1}
>
</PageItem>
)}
{pageButtons}
{next && (
<PageItem
aria-label='Next'
disabled={value === pages}
onClick={onClick}
value={value + 1}
>
</PageItem>
)}
</ul>
</nav>
)
}
}

View File

@@ -0,0 +1,45 @@
import assign from 'lodash/assign'
import PropTypes from 'prop-types'
// Deprecated because :
// - unnecessary
// - not standard in the React ecosystem
if (__DEV__) {
console.warn(`DEPRECATED: use prop-types directly:
class MyComponent extends React.Component {
static propTypes = {
foo: PropTypes.string.isRequired
}
}`)
}
// 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,19 +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'
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]
@@ -24,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) {
@@ -41,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
)
}
}
@@ -70,28 +71,46 @@ export default class NoVnc extends Component {
this._rfb = null
rfb.disconnect()
}
enableShortcuts()
}
_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()
}
componentDidMount () {
@@ -102,6 +121,14 @@ export default class NoVnc extends Component {
this._clean()
}
componentWillReceiveProps (props) {
const rfb = this._rfb
if (rfb && this.props.scale !== props.scale) {
rfb.get_display().set_scale(props.scale || 1)
rfb.get_mouse().set_scale(props.scale || 1)
}
}
_focus = () => {
const rfb = this._rfb
if (rfb) {
@@ -112,6 +139,8 @@ export default class NoVnc extends Component {
rfb.get_keyboard().grab()
rfb.get_mouse().grab()
disableShortcuts()
}
}
@@ -120,17 +149,21 @@ export default class NoVnc extends Component {
if (rfb) {
rfb.get_keyboard().ungrab()
rfb.get_mouse().ungrab()
enableShortcuts()
}
}
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 React, { Component } from 'react'
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)})`
}
return (
<span>
<Icon icon='sr' /> {label}
{container && ` (${container.name_label || container.id})`}
</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}
@@ -113,6 +116,22 @@ const xoItemToRender = {
<Icon icon='resource-set' /> {resourceSet.name}
</span>
),
sshKey: key => (
<span>
<Icon icon='ssh-key' /> {key.label}
</span>
),
ipPool: ipPool => (
<span>
<Icon icon='ip' /> {ipPool.name}
</span>
),
ipAddress: ({ label, used }) => {
if (used) {
return <strong className='text-warning'>{label}</strong>
}
return <span>{label}</span>
},
// XO objects.
pool: pool => (
@@ -123,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>
),
@@ -140,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>
),
@@ -158,15 +180,44 @@ 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 (!type && label) {
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`)
}
return (
<span key={id} className={className}>
{label}
@@ -195,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'>no such item</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,40 +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 { FormattedTime } from 'react-intl'
import {
Tab,
Tabs
} from 'react-bootstrap-4/lib'
import { FormattedDate, FormattedTime } from 'react-intl'
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 = []
@@ -52,16 +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)
}
}
@@ -87,7 +90,7 @@ const PICKTIME_TO_ID = {
hour: 1,
monthDay: 2,
month: 3,
weekDay: 4
weekDay: 4,
}
const TIME_FORMAT = {
@@ -104,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) =>
<FormattedTime value={new Date(1970, monthNum)} month='long' />
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
<FormattedTime value={new Date(1970, 0, 4 + dayNum)} weekday='long' />
<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 p-b-1'>
<Range min={MIN_PREVIEWS} max={MAX_PREVIEWS} onChange={this._handleChange} />
<div className='mb-1' style={PREVIEW_SLIDER_STYLE}>
<Range
min={MIN_PREVIEWS}
max={MAX_PREVIEWS}
onChange={this.linkState('value')}
value={+value}
/>
</div>
<ul className='list-group'>
{map(dates, (date, id) => (
@@ -162,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 = () => {
@@ -173,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>
)
@@ -183,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 = () => {
@@ -219,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>
@@ -232,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-xs-right' onClick={this._reset}>
{_('selectTableReset')}
</button>
<Button className='pull-right' onClick={this._reset}>
{_(`selectTableAll${labelId}`)}{' '}
{value && !value.length && <Icon icon='success' />}
</Button>
</div>
)
}
@@ -258,219 +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 { refs } = this
const { value, valueRenderer } = props
if (value.indexOf('/') === 1) {
this.setState({
activeKey: NAV_EVERY_N
})
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>
@@ -478,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,17 +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 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'
@@ -22,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'
// -------------------------------------------------------------------
@@ -36,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
}
@@ -85,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)
@@ -98,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
})
)
// ===================================================================
@@ -206,21 +198,53 @@ 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
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')
}
@@ -240,37 +264,46 @@ const _getPermissionsPredicate = invoke(() => {
}
})
export const isAdmin = (...args) => {
const user = getUser(...args)
return user && user.permission === 'admin'
}
// ===================================================================
// Common selector creators.
// Creates an object selector from an id selector.
export const createGetObject = (idSelector = _getId) =>
(state, props, 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 = {
@@ -281,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.
@@ -320,7 +350,11 @@ const _extendCollectionSelector = (selector, objectsType) => {
return selector
}
_addGroupBy(selector)
selector.find = predicate => createFinder(selector, predicate)
const _addFind = selector => {
selector.find = predicate => createFinder(selector, predicate)
return selector
}
_addFind(selector)
// groupBy can be chained.
const _addSort = selector => {
@@ -332,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 => _addFilter(_addGroupBy(_addSort(
createPicker(selector, idsSelector)
)))
selector.pick = idsSelector =>
_addFind(
_addFilter(_addGroupBy(_addSort(createPicker(selector, idsSelector))))
)
return selector
}
@@ -360,7 +394,7 @@ const _extendCollectionSelector = (selector, objectsType) => {
// - groupBy: returns a selector which returns the objects grouped by
// a value determined by a getter selector
// - pick: returns a selector which returns only the objects with given
// ids (filter, groupBy and sort can be chained)
// ids (filter, find, groupBy and sort can be chained)
// - sort: returns a selector which returns the objects appropriately
// sorted (groupBy can be chained)
export const createGetObjectsOfType = type => {
@@ -368,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 => {
@@ -379,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'
@@ -414,15 +470,54 @@ export const createGetObjectMessages = objectSelector =>
// ...
export const getObject = createGetObject((_, id) => id)
export const createGetHostMetrics = hostSelector => _createCollectionWrapper(
export const createDoesHostNeedRestart = hostSelector => {
// 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'
),
])
return create(
hostSelector,
(...args) => args,
(host, args) => host.rebootRequired || !!patchRequiresReboot(...args)
)
}
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++
@@ -431,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
}

35
src/common/shortcuts.js Normal file
View File

@@ -0,0 +1,35 @@
import Component from 'base-component'
import forEach from 'lodash/forEach'
import React from 'react'
import remove from 'lodash/remove'
import { Shortcuts as ReactShortcuts } from 'react-shortcuts'
let enabled = true
const instances = []
const updateInstances = () => {
forEach(instances, instance => instance.forceUpdate())
}
export const enable = () => {
enabled = true
updateInstances()
}
export const disable = () => {
enabled = false
updateInstances()
}
export default class Shortcuts extends Component {
componentDidMount () {
instances.push(this)
}
componentWillUnmount () {
remove(instances, this)
}
render () {
return enabled ? <ReactShortcuts {...this.props} /> : null
}
}

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

@@ -1,8 +1,17 @@
.clickableColumn {
cursor: pointer;
cursor: pointer;
}
.clickableColumn:hover {
color: #fff;
background-color: #96b8d1;
color: #fff;
background-color: #96b8d1;
}
.clickableRow {
cursor: pointer;
}
.highlight {
outline: 2px solid #366e98;
outline-offset: -2px;
}

File diff suppressed because it is too large Load Diff

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)

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