Compare commits

..

125 Commits

Author SHA1 Message Date
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
211 changed files with 25452 additions and 16652 deletions

15
.eslintrc.js Normal file
View File

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

4
.prettierrc.js Normal file
View File

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

View File

@@ -1,6 +1,109 @@
# ChangeLog
## **5.10.0** (2017-05-31)
## **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)
### 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

View File

@@ -183,7 +183,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.

View File

@@ -1,7 +1,7 @@
{
"private": false,
"name": "xo-web",
"version": "5.10.1",
"version": "5.14.2",
"license": "AGPL-3.0",
"description": "Web interface client for Xen-Orchestra",
"keywords": [
@@ -31,10 +31,10 @@
"npm": ">=3"
},
"devDependencies": {
"@nraynaud/novnc": "^0.6.1-1",
"@nraynaud/novnc": "0.6.1",
"ansi_up": "^1.3.0",
"asap": "^2.0.4",
"babel-eslint": "^7.0.0",
"asap": "^2.0.6",
"babel-eslint": "^8.0.2",
"babel-plugin-dev": "^1.0.0",
"babel-plugin-lodash": "^3.2.11",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
@@ -46,25 +46,34 @@
"babel-preset-es2015": "^6.6.0",
"babel-preset-react": "^6.5.0",
"babel-preset-stage-0": "^6.5.0",
"babel-register": "^6.16.3",
"babel-runtime": "^6.6.1",
"babelify": "^7.2.0",
"babel-register": "^6.26.0",
"babel-runtime": "^6.26.0",
"babelify": "^8.0.0",
"benchmark": "^2.1.0",
"bootstrap": "4.0.0-alpha.5",
"browserify": "^14.1.0",
"bundle-collapser": "^1.2.1",
"browserify": "^14.5.0",
"bundle-collapser": "^1.3.0",
"chartist": "^0.10.1",
"chartist-plugin-legend": "^0.6.1",
"chartist-plugin-tooltip": "0.0.11",
"classnames": "^2.2.3",
"cookies-js": "^1.2.2",
"d3": "^4.2.8",
"d3": "^4.11.0",
"dependency-check": "^2.5.1",
"enzyme": "^2.6.0",
"enzyme-to-json": "^1.4.4",
"enzyme": "^3.1.1",
"enzyme-adapter-react-15": "^1.0.5",
"enzyme-to-json": "^3.2.2",
"eslint": "^4.11.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": "github:fizzed/font-mfizz",
"font-mfizz": "^2.4.1",
"get-stream": "^2.3.0",
"globby": "^6.0.0",
"gulp": "github:gulpjs/gulp#4.0",
@@ -78,63 +87,65 @@
"gulp-sourcemaps": "^2.2.3",
"gulp-uglify": "^3.0.0",
"gulp-watch": "^4.3.5",
"human-format": "^0.8.0",
"husky": "^0.13.1",
"human-format": "^0.9.0",
"husky": "^0.14.3",
"immutable": "^3.8.2",
"index-modules": "^0.3.0",
"is-ip": "^1.0.0",
"jest": "^20.0.4",
"jsonrpc-websocket-client": "^0.1.1",
"jest": "^21.2.1",
"jsonrpc-websocket-client": "^0.2.0",
"kindof": "^2.0.0",
"later": "^1.2.0",
"lint-staged": "^5.0.0",
"lodash": "^4.6.1",
"loose-envify": "^1.1.0",
"make-error": "^1.2.1",
"marked": "^0.3.5",
"modular-css": "^5.1.6",
"moment": "^2.13.0",
"moment-timezone": "^0.5.4",
"modular-css": "^6.1.0",
"moment": "^2.19.1",
"moment-timezone": "^0.5.14",
"notifyjs": "^3.0.0",
"promise-toolbox": "^0.9.4",
"prettier": "^1.8.2",
"promise-toolbox": "^0.9.5",
"random-password": "^0.1.2",
"react": "^15.4.1",
"react-addons-shallow-compare": "^15.1.0",
"react-addons-test-utils": "^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.12.0",
"react-copy-to-clipboard": "^5.0.0",
"react-debounce-input": "^3.0.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.4.1",
"react-dropzone": "^3.5.0",
"react-intl": "^2.0.1",
"react-key-handler": "^0.3.0",
"react-notify": "^2.0.1",
"react-overlays": "^0.6.0",
"react-redux": "^5.0.0",
"react-dropzone": "^4.2.1",
"react-intl": "^2.4.0",
"react-key-handler": "^1.0.1",
"react-notify": "^3.0.0",
"react-overlays": "^0.8.3",
"react-redux": "^5.0.6",
"react-router": "^3.0.0",
"react-select": "^1.0.0-rc.4",
"react-shortcuts": "^1.3.1",
"react-sparklines": "^1.5.0",
"react-select": "^1.0.0-rc.10",
"react-shortcuts": "^1.6.1",
"react-sparklines": "1.6.0",
"react-test-renderer": "^15.6.2",
"react-virtualized": "^8.0.8",
"readable-stream": "^2.0.6",
"redux": "^3.3.1",
"readable-stream": "^2.3.3",
"redux": "^3.7.2",
"redux-devtools": "^3.1.1",
"redux-devtools-dock-monitor": "^1.1.0",
"redux-devtools-log-monitor": "^1.0.5",
"redux-thunk": "^2.0.1",
"reselect": "^2.5.4",
"semver": "^5.3.0",
"standard": "^10.0.0",
"styled-components": "^2.1.0",
"superagent": "^3.5.0",
"semver": "^5.4.1",
"styled-components": "^2.2.3",
"tar-stream": "^1.5.2",
"uglify-es": "^3.0.18",
"uglify-es": "^3.1.8",
"uncontrollable-input": "^0.0.1",
"vinyl": "^2.0.0",
"vinyl": "^2.1.0",
"watchify": "^3.7.0",
"xml2js": "^0.4.17",
"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",
@@ -144,11 +155,12 @@
"benchmarks": "./tools/run-benchmarks.js 'src/**/*.bench.js'",
"build": "npm run build-indexes && NODE_ENV=production gulp build",
"build-indexes": "index-modules --auto src",
"commitmsg": "npm test",
"dev": "npm run build-indexes && NODE_ENV=development gulp build",
"dev-test": "jest --watch",
"lint": "standard",
"posttest": "npm run lint",
"lint-staged-stash": "touch .lint-staged && git stash save --include-untracked --keep-index && true",
"lint-staged-unstash": "git stash pop && rm -f .lint-staged && true",
"posttest": "eslint --ignore-path .gitignore src/",
"precommit": "lint-staged",
"prepublish": "npm run build",
"test": "jest"
},
@@ -186,17 +198,19 @@
]
},
"jest": {
"setupTestFrameworkScriptFile": "./setup-tests.js",
"snapshotSerializers": [
"enzyme-to-json/serializer"
]
},
"standard": {
"globals": [
"__DEV__"
],
"ignore": [
"dist"
],
"parser": "babel-eslint"
"lint-staged": {
"*.js": [
"lint-staged-stash",
"prettier --write",
"eslint --fix",
"jest --findRelatedTests",
"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

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

View File

@@ -39,17 +39,14 @@ import { error as _error } from './notification'
//
// if a function, it will be called with the result of the action to
// compute the path
redirectOnSuccess: propTypes.oneOfType([
propTypes.func,
propTypes.string
]),
redirectOnSuccess: propTypes.oneOfType([propTypes.func, propTypes.string]),
// React element to use tooltip for the component
tooltip: propTypes.node
tooltip: propTypes.node,
})
export default class ActionButton extends Component {
static contextTypes = {
router: propTypes.object
router: propTypes.object,
}
async _execute () {
@@ -57,17 +54,12 @@ export default class ActionButton extends Component {
return
}
const {
children,
handler,
handlerParam,
tooltip
} = this.props
const { children, handler, handlerParam, tooltip } = this.props
try {
this.setState({
error: undefined,
working: true
working: true,
})
const result = await handler(handlerParam)
@@ -75,23 +67,28 @@ export default class ActionButton extends Component {
const { redirectOnSuccess } = this.props
if (redirectOnSuccess) {
return this.context.router.push(
isFunction(redirectOnSuccess) ? redirectOnSuccess(result) : redirectOnSuccess
isFunction(redirectOnSuccess)
? redirectOnSuccess(result)
: redirectOnSuccess
)
}
this.setState({
working: false
working: false,
})
} catch (error) {
this.setState({
error,
working: false
working: false,
})
// ignore when undefined because it usually means that the action has been canceled
if (error !== undefined) {
logError(error)
_error(children || tooltip || error.name, error.message || String(error))
_error(
children || tooltip || error.name,
error.message || String(error)
)
}
}
}
@@ -106,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)
}
}
@@ -114,20 +113,16 @@ 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: {
children,
icon,
pending,
tooltip,
...props
},
state: { error, working }
props: { children, icon, pending, tooltip, ...props },
state: { error, working },
} = this
if (error !== undefined) {
@@ -143,14 +138,14 @@ export default class ActionButton extends Component {
}
delete props.redirectOnSuccess
const button = <Button {...props}>
<Icon icon={pending || working ? 'loading' : icon} fixedWidth />
{children && ' '}
{children}
</Button>
const button = (
<Button {...props}>
<Icon icon={pending || working ? 'loading' : icon} fixedWidth />
{children && ' '}
{children}
</Button>
)
return tooltip
? <Tooltip content={tooltip}>{button}</Tooltip>
: 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

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

View File

@@ -1,9 +1,6 @@
import clone from 'lodash/clone'
import includes from 'lodash/includes'
import isArray from 'lodash/isArray'
import forEach from 'lodash/forEach'
import map from 'lodash/map'
import { PureComponent } from 'react'
import { cowSet } from 'utils'
import { includes, isArray, forEach, map } from 'lodash'
import getEventValue from './get-event-value'
@@ -12,17 +9,6 @@ import getEventValue from './get-event-value'
// Usually set to process.env.NODE_ENV !== 'production'.
const VERBOSE = false
const cowSet = (object, path, value, depth) => {
if (depth >= path.length) {
return value
}
object = object != null ? clone(object) : {}
const prop = path[depth]
object[prop] = cowSet(object[prop], path, value, depth + 1)
return object
}
const get = (object, path, depth) => {
if (depth >= path.length) {
return object
@@ -54,9 +40,7 @@ export default class BaseComponent extends PureComponent {
// See https://preactjs.com/guide/linked-state
linkState (name, targetPath) {
const key = targetPath !== undefined
? `${name}##${targetPath}`
: name
const key = targetPath !== undefined ? `${name}##${targetPath}` : name
let linkedState = this._linkedState
let cb
@@ -83,7 +67,7 @@ export default class BaseComponent extends PureComponent {
return (linkedState[key] = event => {
this.setState({
[name]: getValue(event)
[name]: getValue(event),
})
})
}
@@ -106,7 +90,7 @@ export default class BaseComponent extends PureComponent {
return (linkedState[name] = () => {
this.setState({
[name]: !this.state[name]
[name]: !this.state[name],
})
})
}

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

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

View File

@@ -43,14 +43,11 @@ propTypes({
'link',
'primary',
'success',
'warning'
'warning',
]),
outline: propTypes.bool,
size: propTypes.oneOf([
'large',
'small'
])
size: propTypes.oneOf(['large', 'small']),
})(Button)
export { Button as default }

View File

@@ -3,49 +3,38 @@ import React from 'react'
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

@@ -9,16 +9,16 @@ import propTypes from './prop-types-decorator'
children: propTypes.any.isRequired,
className: propTypes.string,
buttonText: propTypes.any.isRequired,
defaultOpen: propTypes.bool
defaultOpen: propTypes.bool,
})
export default class Collapse extends Component {
state = {
isOpened: this.props.defaultOpen
isOpened: this.props.defaultOpen,
}
_onClick = () => {
this.setState({
isOpened: !this.state.isOpened
isOpened: !this.state.isOpened,
})
}
@@ -29,7 +29,8 @@ export default class Collapse extends Component {
return (
<div className={props.className}>
<Button block btnStyle='primary' size='large' onClick={this._onClick}>
{props.buttonText} <Icon icon={`chevron-${isOpened ? 'up' : 'down'}`} />
{props.buttonText}{' '}
<Icon icon={`chevron-${isOpened ? 'up' : 'down'}`} />
</Button>
{isOpened && props.children}
</div>

View File

@@ -1,25 +1,22 @@
import React from 'react'
import uncontrollableInput from 'uncontrollable-input'
import { isEmpty, map } from 'lodash'
import {
DropdownButton,
MenuItem
} from 'react-bootstrap-4/lib'
import { DropdownButton, MenuItem } from 'react-bootstrap-4/lib'
import Component from './base-component'
import propTypes from './prop-types-decorator'
@uncontrollableInput({
defaultValue: ''
defaultValue: '',
})
@propTypes({
disabled: propTypes.bool,
options: propTypes.oneOfType([
propTypes.arrayOf(propTypes.string),
propTypes.objectOf(propTypes.string)
propTypes.objectOf(propTypes.string),
]),
onChange: propTypes.func.isRequired,
value: propTypes.string.isRequired
value: propTypes.string.isRequired,
})
export default class Combobox extends Component {
_handleChange = event => {
@@ -50,11 +47,11 @@ export default class Combobox extends Component {
id='selectInput'
title=''
>
{map(options, option =>
{map(options, option => (
<MenuItem key={option} onClick={() => this._setText(option)}>
{option}
</MenuItem>
)}
))}
</DropdownButton>
</div>
{Input}

View File

@@ -1,11 +1,5 @@
import {
parse,
toString
} from './'
import {
ast,
pattern
} from './index.fixtures'
import { parse, toString } from './'
import { ast, pattern } from './index.fixtures'
export default ({ benchmark }) => {
benchmark('parse', () => {
@@ -13,6 +7,6 @@ export default ({ benchmark }) => {
})
benchmark('toString', () => {
ast::toString()
;ast::toString()
})
}

View File

@@ -4,7 +4,7 @@ import {
createNot,
createProperty,
createString,
createTruthyProperty
createTruthyProperty,
} from './'
export const pattern = 'foo !"\\\\ \\"" name:|(wonderwoman batman) hasCape?'
@@ -12,9 +12,9 @@ export const pattern = 'foo !"\\\\ \\"" name:|(wonderwoman batman) hasCape?'
export const ast = createAnd([
createString('foo'),
createNot(createString('\\ "')),
createProperty('name', createOr([
createString('wonderwoman'),
createString('batman')
])),
createTruthyProperty('hasCape')
createProperty(
'name',
createOr([createString('wonderwoman'), createString('batman')])
),
createTruthyProperty('hasCape'),
])

View File

@@ -42,17 +42,19 @@ const isRawString = string => {
// -------------------------------------------------------------------
export const createAnd = children => children.length === 1
? children[0]
: { type: 'and', children }
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 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 createProperty = (name, child) => ({
type: 'property',
name,
child,
})
export const createString = value => ({ type: 'string', value })
@@ -97,7 +99,7 @@ export const parse = invoke(() => {
return
}
const terms = [ term ]
const terms = [term]
while ((term = parseTerm())) {
terms.push(term)
}
@@ -106,14 +108,13 @@ export const parse = invoke(() => {
const parseTerm = () => {
parseWs()
const child = (
const child =
parseGroupedAnd() ||
parseOr() ||
parseNot() ||
parseProperty() ||
parseTruthyProperty() ||
parseString()
)
if (child) {
parseWs()
return child
@@ -128,11 +129,7 @@ export const parse = invoke(() => {
}
const parseGroupedAnd = backtrace(() => {
let and
if (
input[i++] === '(' &&
(and = parseAnd()) &&
input[i++] === ')'
) {
if (input[i++] === '(' && (and = parseAnd()) && input[i++] === ')') {
return and
}
})
@@ -150,10 +147,7 @@ export const parse = invoke(() => {
})
const parseNot = backtrace(() => {
let child
if (
input[i++] === '!' &&
(child = parseTerm())
) {
if (input[i++] === '!' && (child = parseTerm())) {
return createNot(child)
}
})
@@ -162,7 +156,7 @@ export const parse = invoke(() => {
if (
(name = parseString()) &&
parseWs() &&
(input[i++] === ':') &&
input[i++] === ':' &&
(child = parseTerm())
) {
return createProperty(name.value, child)
@@ -196,10 +190,7 @@ export const parse = invoke(() => {
const parseRawString = () => {
let value = ''
let c
while (
(c = input[i]) &&
RAW_STRING_CHARS[c]
) {
while ((c = input[i]) && RAW_STRING_CHARS[c]) {
++i
value += c
}
@@ -209,11 +200,7 @@ export const parse = invoke(() => {
}
const parseTruthyProperty = backtrace(() => {
let name
if (
(name = parseString()) &&
parseWs() &&
input[i++] === '?'
) {
if ((name = parseString()) && parseWs() && input[i++] === '?') {
return createTruthyProperty(name.value)
}
})
@@ -251,7 +238,7 @@ const _getPropertyClauseStrings = ({ child }) => {
}
if (type === 'string') {
return [ child.value ]
return [child.value]
}
return []
@@ -267,7 +254,7 @@ export const getPropertyClausesStrings = function () {
if (type === 'property') {
return {
[this.name]: _getPropertyClauseStrings(this)
[this.name]: _getPropertyClauseStrings(this),
}
}
@@ -294,17 +281,17 @@ export const getPropertyClausesStrings = function () {
export const removePropertyClause = function (name) {
let type
if (!this || (
(type = this.type) === 'property' &&
this.name === name
)) {
if (!this || ((type = this.type) === 'property' && this.name === name)) {
return
}
if (type === 'and') {
return createAnd(filter(this.children, node =>
node.type !== 'property' || node.name !== name
))
return createAnd(
filter(
this.children,
node => node.type !== 'property' || node.name !== name
)
)
}
return this
@@ -313,14 +300,14 @@ export const removePropertyClause = function (name) {
// -------------------------------------------------------------------
const _addAndClause = (node, child, predicate, reducer) =>
createAnd(filterReduce(
node.type === 'and'
? node.children
: [ node ],
predicate,
reducer,
child
))
createAnd(
filterReduce(
node.type === 'and' ? node.children : [node],
predicate,
reducer,
child
)
)
export const setPropertyClause = function (name, child) {
const property = createProperty(
@@ -343,18 +330,12 @@ export const setPropertyClause = function (name, child) {
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])
),
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]),
truthyProperty: ({ name }, value) => !!value[name],
string: invoke(() => {
const match = (pattern, value) => {
@@ -369,10 +350,8 @@ export const execute = invoke(() => {
return false
}
return ({ value: pattern }, value) => (
match(pattern.toLowerCase(), value)
)
})
return ({ value: pattern }, value) => match(pattern.toLowerCase(), value)
}),
}
return function (value) {
@@ -390,11 +369,13 @@ export const toString = invoke(() => {
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}`)}"`,
truthyProperty: ({ name }) => `${toString(createString(name))}?`
property: ({ name, child }) =>
`${toString(createString(name))}:${toString(child)}`,
string: ({ value }) =>
isRawString(value)
? value
: `"${value.replace(/\\|"/g, match => `\\${match}`)}"`,
truthyProperty: ({ name }) => `${toString(createString(name))}?`,
}
const toString = node => visitors[node.type](node)
@@ -403,9 +384,7 @@ export const toString = invoke(() => {
return function () {
return !this
? ''
: this.type === 'and'
? toStringTerms(this.children)
: toString(this)
: this.type === 'and' ? toStringTerms(this.children) : toString(this)
}
})

View File

@@ -4,18 +4,15 @@ import {
getPropertyClausesStrings,
parse,
setPropertyClause,
toString
toString,
} from './'
import {
ast,
pattern
} from './index.fixtures'
import { ast, pattern } from './index.fixtures'
it('getPropertyClausesStrings', () => {
const tmp = parse('foo bar:baz baz:|(foo bar)')::getPropertyClausesStrings()
expect(tmp).toEqual({
bar: [ 'baz' ],
baz: [ 'foo', 'bar' ]
bar: ['baz'],
baz: ['foo', 'bar'],
})
})
@@ -24,20 +21,24 @@ it('parse', () => {
})
it('setPropertyClause', () => {
expect(
null::setPropertyClause('foo', 'bar')::toString()
).toBe('foo:bar')
expect(null::setPropertyClause('foo', 'bar')::toString()).toBe('foo:bar')
expect(
parse('baz')::setPropertyClause('foo', 'bar')::toString()
parse('baz')
::setPropertyClause('foo', 'bar')
::toString()
).toBe('baz foo:bar')
expect(
parse('plip foo:baz plop')::setPropertyClause('foo', 'bar')::toString()
parse('plip foo:baz plop')
::setPropertyClause('foo', 'bar')
::toString()
).toBe('plip plop foo:bar')
expect(
parse('foo:|(baz plop)')::setPropertyClause('foo', 'bar')::toString()
parse('foo:|(baz plop)')
::setPropertyClause('foo', 'bar')
::toString()
).toBe('foo:bar')
})

View File

@@ -12,21 +12,23 @@ import styles from './index.css'
const Copiable = propTypes({
data: propTypes.string,
tagName: propTypes.string
})(({ className, tagName = 'span', ...props }) => createElement(
tagName,
{
...props,
className: classNames(styles.container, className)
},
props.children,
' ',
<Tooltip content={_('copyToClipboard')}>
<CopyToClipboard text={props.data || props.children}>
<Button className={styles.button} size='small'>
<Icon icon='clipboard' />
</Button>
</CopyToClipboard>
</Tooltip>
))
tagName: propTypes.string,
})(({ className, tagName = 'span', ...props }) =>
createElement(
tagName,
{
...props,
className: classNames(styles.container, className),
},
props.children,
' ',
<Tooltip content={_('copyToClipboard')}>
<CopyToClipboard text={props.data || props.children}>
<Button className={styles.button} size='small'>
<Icon icon='clipboard' />
</Button>
</CopyToClipboard>
</Tooltip>
)
)
export { Copiable as default }

View File

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

View File

@@ -1,21 +1,20 @@
import React, { Component, PropTypes } from 'react'
import { isPromise } from 'promise-toolbox'
const toString = value => value === undefined
? 'undefined'
: 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(
@@ -35,21 +34,26 @@ class DebugAsync extends Component {
return <pre>{'Promise { <pending> }'}</pre>
}
return <pre>
{'Promise { '}
{status === 'rejected' && '<rejected> '}
{toString(value)}
{' }'}
</pre>
return (
<pre>
{'Promise { '}
{status === 'rejected' && '<rejected> '}
{toString(value)}
{' }'}
</pre>
)
}
}
const Debug = ({ value }) => isPromise(value)
? <DebugAsync promise={value} />
: <pre>{toString(value)}</pre>
const Debug = ({ value }) =>
isPromise(value) ? (
<DebugAsync promise={value} />
) : (
<pre>{toString(value)}</pre>
)
Debug.propTypes = {
value: PropTypes.any.isRequired
value: PropTypes.any.isRequired,
}
export { Debug as default }

View File

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

View File

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

View File

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

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

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

View File

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

View File

@@ -3,23 +3,17 @@
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)
it('filterReduce', () => {
// Returns all elements not matching the predicate and the result of
// a reduction over those who do.
expect(filterReduce(data, isEven, add)).toEqual(
[ 1, 3, 5, 7, 9, 20 ]
)
expect(filterReduce(data, isEven, add)).toEqual([1, 3, 5, 7, 9, 20])
// The default reducer is the identity.
expect(filterReduce(data, isEven)).toEqual(
[ 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.
expect(filterReduce(data, isEven, add, 22)).toEqual(
[ 1, 3, 5, 7, 9, 42 ]
)
expect(filterReduce(data, isEven, add, 22)).toEqual([1, 3, 5, 7, 9, 42])
})

View File

@@ -4,21 +4,15 @@ import * as Grid from './grid'
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

@@ -7,20 +7,14 @@ import React from 'react'
import round from 'lodash/round'
import SingleLineRow from 'single-line-row'
import { Container, Col } from 'grid'
import {
DropdownButton,
MenuItem
} from 'react-bootstrap-4/lib'
import { DropdownButton, MenuItem } from 'react-bootstrap-4/lib'
import Button from '../button'
import Component from '../base-component'
import defined from '../xo-defined'
import getEventValue from '../get-event-value'
import propTypes from '../prop-types-decorator'
import {
firstDefined,
formatSizeRaw,
parseSize
} from '../utils'
import { formatSizeRaw, parseSize } from '../utils'
export Select from './select'
export SelectPlainObject from './select-plain-object'
@@ -28,7 +22,7 @@ export SelectPlainObject from './select-plain-object'
// ===================================================================
@propTypes({
enableGenerator: propTypes.bool
enableGenerator: propTypes.bool,
})
export class Password extends Component {
get value () {
@@ -51,42 +45,42 @@ export class Password extends Component {
// FIXME: in controlled mode, visibility should only be updated
// when the value prop is changed according to the emitted value.
this.setState({
visible: true
visible: true,
})
}
_toggleVisibility = () => {
this.setState({
visible: !this.state.visible
visible: !this.state.visible,
})
}
render () {
const {
className,
enableGenerator = false,
...props
} = this.props
const { className, enableGenerator = false, ...props } = this.props
const { visible } = this.state
return <div className='input-group'>
{enableGenerator && <span className='input-group-btn'>
<Button 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>
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>
)
}
}
@@ -97,7 +91,7 @@ export class Password extends Component {
min: propTypes.number.isRequired,
onChange: propTypes.func,
step: propTypes.number,
value: propTypes.number
value: propTypes.number,
})
export class Range extends Component {
componentDidMount () {
@@ -108,30 +102,31 @@ export class Range extends Component {
}
}
_onChange = value =>
this.props.onChange(getEventValue(value))
_onChange = value => this.props.onChange(getEventValue(value))
render () {
const { max, min, step, value } = this.props
return <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>
return (
<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>
)
}
}
@@ -149,16 +144,15 @@ const DEFAULT_UNIT = 'GiB'
readOnly: propTypes.bool,
required: propTypes.bool,
style: propTypes.object,
value: propTypes.oneOfType([
propTypes.number,
propTypes.oneOf([ null ])
])
value: propTypes.oneOfType([propTypes.number, propTypes.oneOf([null])]),
})
export class SizeInput extends BaseComponent {
constructor (props) {
super(props)
this.state = this._createStateFromBytes(firstDefined(props.value, props.defaultValue, null))
this.state = this._createStateFromBytes(
defined(props.value, props.defaultValue, null)
)
}
componentWillReceiveProps (props) {
@@ -172,21 +166,21 @@ export class SizeInput extends BaseComponent {
if (bytes === this._bytes) {
return {
input: this._input,
unit: this._unit
unit: this._unit,
}
}
if (bytes === null) {
return {
input: '',
unit: this.props.defaultUnit || DEFAULT_UNIT
unit: this.props.defaultUnit || DEFAULT_UNIT,
}
}
const { prefix, value } = formatSizeRaw(bytes)
return {
input: String(round(value, 2)),
unit: `${prefix}B`
unit: `${prefix}B`,
}
}
@@ -214,9 +208,7 @@ export class SizeInput extends BaseComponent {
const { onChange } = this.props
// Empty input equals null.
const bytes = input
? parseSize(`${+input} ${unit}`)
: null
const bytes = input ? parseSize(`${+input} ${unit}`) : null
const isControlled = this.props.value !== undefined
if (isControlled) {
@@ -246,8 +238,7 @@ export class SizeInput extends BaseComponent {
const number = +input
// NaN: do not ack this change.
if (number !== number) { // eslint-disable-line no-self-compare
if (Number.isNaN(number)) {
return
}
@@ -278,38 +269,37 @@ export class SizeInput extends BaseComponent {
readOnly,
placeholder,
required,
style
style,
} = this.props
return <span className={classNames('input-group', className)} style={style}>
<input
autoFocus={autoFocus}
className='form-control'
disabled={readOnly}
onChange={this._updateNumber}
placeholder={placeholder}
required={required}
type='text'
value={this.state.input}
/>
<span className='input-group-btn'>
<DropdownButton
bsStyle='secondary'
id='size'
pullRight
return (
<span className={classNames('input-group', className)} style={style}>
<input
autoFocus={autoFocus}
className='form-control'
disabled={readOnly}
title={this.state.unit}
>
{map(UNITS, unit =>
<MenuItem
key={unit}
onClick={() => this._updateUnit(unit)}
>
{unit}
</MenuItem>
)}
</DropdownButton>
onChange={this._updateNumber}
placeholder={placeholder}
required={required}
type='text'
value={this.state.input}
/>
<span className='input-group-btn'>
<DropdownButton
bsStyle='secondary'
id='size'
pullRight
disabled={readOnly}
title={this.state.unit}
>
{map(UNITS, unit => (
<MenuItem key={unit} onClick={() => this._updateUnit(unit)}>
{unit}
</MenuItem>
))}
</DropdownButton>
</span>
</span>
</span>
)
}
}

View File

@@ -15,10 +15,10 @@ import Select from './select'
multi: propTypes.bool,
onChange: propTypes.func,
options: propTypes.array,
placeholder: propTypes.string,
placeholder: propTypes.node,
predicate: propTypes.func,
required: propTypes.bool,
value: propTypes.any
value: propTypes.any,
})
@uncontrollableInput()
export default class SelectPlainObject extends Component {
@@ -27,7 +27,7 @@ export default class SelectPlainObject extends Component {
this.setState({
options: this._computeOptions(options),
value: this._computeValue(value, this.props)
value: this._computeValue(value, this.props),
})
}
@@ -35,7 +35,7 @@ export default class SelectPlainObject extends Component {
if (newProps !== this.props) {
this.setState({
options: this._computeOptions(newProps.options),
value: this._computeValue(newProps.value, newProps)
value: this._computeValue(newProps.value, newProps),
})
}
}
@@ -43,7 +43,8 @@ export default class SelectPlainObject extends Component {
_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]
@@ -59,7 +60,7 @@ export default class SelectPlainObject extends Component {
const { optionRenderer = o => o.label || o[optionKey] || o } = this.props
return map(options, option => ({
value: option[optionKey] || option,
label: optionRenderer(option)
label: optionRenderer(option),
}))
}
@@ -72,7 +73,10 @@ export default class SelectPlainObject extends Component {
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) {

View File

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

View File

@@ -14,13 +14,13 @@ import propTypes from '../prop-types-decorator'
iconOn: propTypes.string,
iconOff: propTypes.string,
iconSize: propTypes.number,
value: propTypes.bool
value: propTypes.bool,
})
export default class Toggle extends Component {
static defaultProps = {
iconOn: 'toggle-on',
iconOff: 'toggle-off',
iconSize: 2
iconSize: 2,
}
_toggle = () => {

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

@@ -3,6 +3,7 @@ import React from 'react'
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>
))

View File

@@ -1,5 +1,5 @@
const common = {
homeFilterNone: ''
homeFilterNone: '',
}
export const VM = {
@@ -8,26 +8,26 @@ export const VM = {
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 = {
...common,
homeFilterTags: 'tags:'
homeFilterTags: 'tags:',
}
export const vmTemplate = {
...common,
homeFilterTags: 'tags:'
homeFilterTags: 'tags:',
}
export const SR = {
...common,
homeFilterTags: 'tags:'
homeFilterTags: 'tags:',
}

View File

@@ -10,27 +10,31 @@ import { createString, createProperty, toString } from './complex-matcher'
onAdd: propTypes.func,
onChange: propTypes.func,
onDelete: propTypes.func,
type: propTypes.string
type: propTypes.string,
})
export default class HomeTags extends Component {
static contextTypes = {
router: React.PropTypes.object
router: React.PropTypes.object,
}
_onClick = label => {
const s = encodeURIComponent(createProperty('tags', createString(label))::toString())
const s = encodeURIComponent(
createProperty('tags', createString(label))::toString()
)
const t = encodeURIComponent(this.props.type)
this.context.router.push(`/home?t=${t}&s=${s}`)
}
render () {
return <Tags
labels={this.props.labels}
onAdd={this.props.onAdd}
onChange={this.props.onChange}
onClick={this._onClick}
onDelete={this.props.onDelete}
/>
return (
<Tags
labels={this.props.labels}
onAdd={this.props.onAdd}
onChange={this.props.onChange}
onClick={this._onClick}
onDelete={this.props.onDelete}
/>
)
}
}

View File

@@ -1,13 +1,10 @@
import isEmpty from 'lodash/isEmpty'
import keys from 'lodash/keys'
import map from 'lodash/map'
import React from 'react'
import { Portal } from 'react-overlays'
import { forEach, isEmpty, keys, map, noop } from 'lodash'
import _ from './intl'
import ActionButton from './action-button'
import Component from './base-component'
import forEach from 'lodash/forEach'
import Link from './link'
import propTypes from './prop-types-decorator'
import SortedTable from './sorted-table'
@@ -16,12 +13,12 @@ import { connectStore } from './utils'
import {
createGetObjectsOfType,
createFilter,
createSelector
createSelector,
} from './selectors'
import {
installAllHostPatches,
installAllPatchesOnPool,
subscribeHostMissingPatches
subscribeHostMissingPatches,
} from './xo'
// ===================================================================
@@ -29,18 +26,22 @@ import {
const MISSING_PATCHES_COLUMNS = [
{
name: _('srHost'),
itemRenderer: host => <Link to={`/hosts/${host.id}`}>{host.name_label}</Link>,
sortCriteria: host => host.name_label
itemRenderer: host => (
<Link to={`/hosts/${host.id}`}>{host.name_label}</Link>
),
sortCriteria: host => host.name_label,
},
{
name: _('hostDescription'),
itemRenderer: host => host.name_description,
sortCriteria: host => host.name_description
sortCriteria: host => host.name_description,
},
{
name: _('hostMissingPatches'),
itemRenderer: (host, { missingPatches }) => <Link to={`/hosts/${host.id}/patches`}>{missingPatches[host.id]}</Link>,
sortCriteria: (host, { missingPatches }) => missingPatches[host.id]
itemRenderer: (host, { missingPatches }) => (
<Link to={`/hosts/${host.id}/patches`}>{missingPatches[host.id]}</Link>
),
sortCriteria: (host, { missingPatches }) => missingPatches[host.id],
},
{
name: _('patchUpdateButton'),
@@ -51,30 +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)}
>
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)
@@ -90,16 +94,21 @@ class HostsPatchesTable extends Component {
)
_subscribeMissingPatches = (hosts = this.props.hosts) => {
const unsubs = map(hosts, host =>
subscribeHostMissingPatches(
host,
patches => this.setState({
missingPatches: {
...this.state.missingPatches,
[host.id]: patches.length
}
})
)
const { hostsById } = this.props
const unsubs = map(
hosts,
host =>
hostsById
? subscribeHostMissingPatches(hostsById[host.id][0], patches =>
this.setState({
missingPatches: {
...this.state.missingPatches,
[host.id]: patches.length,
},
})
)
: noop
)
if (this.unsubscribeMissingPatches !== undefined) {
@@ -115,10 +124,7 @@ class HostsPatchesTable extends Component {
pools[host.$pool] = true
})
return Promise.all(map(
keys(pools),
installAllPatchesOnPool
))
return Promise.all(map(keys(pools), installAllPatchesOnPool))
}
componentDidMount () {
@@ -144,7 +150,7 @@ class HostsPatchesTable extends Component {
container,
displayPools,
pools,
useTabButton
useTabButton,
} = this.props
const hosts = this._getHosts()
@@ -152,25 +158,27 @@ class HostsPatchesTable extends Component {
const Container = container || 'div'
const Button = useTabButton
? TabButton
: ActionButton_
const Button = useTabButton ? TabButton : ActionButton_
return (
<div>
{!noPatches
? (
<SortedTable
collection={hosts}
columns={displayPools ? POOLS_MISSING_PATCHES_COLUMNS : MISSING_PATCHES_COLUMNS}
userData={{
installAllHostPatches,
missingPatches: this.state.missingPatches,
pools
}}
/>
) : <p>{_('patchNothing')}</p>
}
{!noPatches ? (
<SortedTable
collection={hosts}
columns={
displayPools
? POOLS_MISSING_PATCHES_COLUMNS
: MISSING_PATCHES_COLUMNS
}
userData={{
installAllHostPatches,
missingPatches: this.state.missingPatches,
pools,
}}
/>
) : (
<p>{_('patchNothing')}</p>
)}
<Portal container={() => buttonsGroupContainer()}>
<Container>
<Button
@@ -193,7 +201,7 @@ class HostsPatchesTable extends Component {
const getPools = createGetObjectsOfType('pool')
return {
pools: getPools
pools: getPools,
}
})
class HostsPatchesTableByPool extends Component {
@@ -211,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

@@ -4,22 +4,21 @@ import React from 'react'
import propTypes from './prop-types-decorator'
const Icon = ({ icon, size = 1, fixedWidth, ...props }) => {
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
])
size: propTypes.oneOfType([propTypes.string, propTypes.number]),
})
export default Icon

View File

@@ -1,18 +1,9 @@
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 React, { Component, PropTypes } 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 +35,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,8 +55,8 @@ 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
@@ -67,23 +65,25 @@ export class IntlProvider extends Component {
// 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_>
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>
)
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -204,7 +204,7 @@ export default {
editUserProfile: undefined,
// Original text: "Fetching data…"
homeFetchingData: 'מקבל נתונים, נא להמתין...',
homeFetchingData: 'מקבל נתונים, נא להמתין',
// Original text: "Welcome on Xen Orchestra!"
homeWelcome: 'ברוכים הבאים',
@@ -228,7 +228,7 @@ export default {
homeNoVms: 'אין מכונות',
// Original text: "Or…"
homeNoVmsOr: 'או...',
homeNoVmsOr: 'או',
// Original text: "Import VM"
homeImportVm: 'ההלעה של מכונה',
@@ -330,7 +330,7 @@ export default {
homeMore: 'עוד',
// Original text: "Migrate to…"
homeMigrateTo: 'העבר ל...',
homeMigrateTo: 'העבר ל',
// Original text: 'Missing patches'
homeMissingPaths: undefined,
@@ -3132,5 +3132,5 @@ export default {
settingsAclsButtonTooltipSR: undefined,
// Original text: 'Network'
settingsAclsButtonTooltipnetwork: undefined
settingsAclsButtonTooltipnetwork: undefined,
}

View File

@@ -231,7 +231,8 @@ export default {
homeWelcomeText: 'Hozzáadása your XenServer kiszolgálók or pools',
// Original text: "Some XenServers have been registered but are not connected"
homeConnectServerText: 'Some XenServers have been registered but are not Kapcsolódva',
homeConnectServerText:
'Some XenServers have been registered but are not Kapcsolódva',
// Original text: "Want some help?"
homeHelp: 'Segítségre van szüksége?',
@@ -363,7 +364,8 @@ export default {
homeDisplayedItems: '{displayed, number}x {icon} (on {total, number})',
// Original text: "{selected, number}x {icon} selected (on {total, number})"
homeSelectedItems: '{selected, number}x {icon} kiválasztott (on {total, number})',
homeSelectedItems:
'{selected, number}x {icon} kiválasztott (on {total, number})',
// Original text: "More"
homeMore: 'Több',
@@ -618,7 +620,8 @@ export default {
runJob: 'Feladat futtatása',
// Original text: "One shot running started. See overview for logs."
runJobVerbose: 'Sikeresen elindítva. A logokat kérjük mindneképp nézze meg az eredményekhez.',
runJobVerbose:
'Sikeresen elindítva. A logokat kérjük mindneképp nézze meg az eredményekhez.',
// Original text: "Started"
jobStarted: 'Elindítva',
@@ -633,16 +636,19 @@ export default {
deleteBackupSchedule: 'Mentési feladat eltávolítása',
// Original text: "Are you sure you want to delete this backup job?"
deleteBackupScheduleQuestion: 'Biztos benne, hogy törli ezt a mentési feladatot?',
deleteBackupScheduleQuestion:
'Biztos benne, hogy törli ezt a mentési feladatot?',
// Original text: "Enable immediately after creation"
scheduleEnableAfterCreation: 'Létrehozás utáni bekapcsolás engedélyezése',
// Original text: "You are editing Schedule {name} ({id}). Saving will override previous schedule state."
scheduleEditMessage: 'A következő Időzítést szerkeszti: {név} ({id}). A mentés felülírja az előző állapotot.',
scheduleEditMessage:
'A következő Időzítést szerkeszti: {név} ({id}). A mentés felülírja az előző állapotot.',
// Original text: "You are editing job {name} ({id}). Saving will override previous job state."
jobEditMessage: 'A következő Feladatot szerkeszti: {név} ({id}). A mentés felülírja az előző állapotot.',
jobEditMessage:
'A következő Feladatot szerkeszti: {név} ({id}). A mentés felülírja az előző állapotot.',
// Original text: "No scheduled jobs."
noScheduledJobs: 'Nincsenek időzített feladatok.',
@@ -672,7 +678,8 @@ export default {
jobUserNotFound: 'A feladat létrehozója már nem érhető el a rendszerben',
// Original text: "This backup's creator no longer exists"
backupUserNotFound: 'A mentési feladat létrehozója már nem érhető el a rendszerben',
backupUserNotFound:
'A mentési feladat létrehozója már nem érhető el a rendszerben',
// Original text: "Backup owner"
backupOwner: 'Mentés tulajdonosa',
@@ -693,10 +700,12 @@ export default {
localRemoteWarningTitle: 'Lokális távoli kiválasztva',
// Original text: "Warning: local remotes will use limited XOA disk space. Only for advanced users."
localRemoteWarningMessage: 'Figyelmeztetés: lokális távoli mentés korlátozott rendszer helyet használ. Kizárólag haladó felhasználóknak ajánlott, ha biztos benne, hogy ez a szervere elérhetőségét nem befolyásolja!.',
localRemoteWarningMessage:
'Figyelmeztetés: lokális távoli mentés korlátozott rendszer helyet használ. Kizárólag haladó felhasználóknak ajánlott, ha biztos benne, hogy ez a szervere elérhetőségét nem befolyásolja!.',
// Original text: "Warning: this feature works only with XenServer 6.5 or newer."
backupVersionWarning: 'Figyelmeztetés: 6.5 vagy újabb Xen támogatás szükséges!',
backupVersionWarning:
'Figyelmeztetés: 6.5 vagy újabb Xen támogatás szükséges!',
// Original text: "VMs"
editBackupVmsTitle: 'VPS-ek',
@@ -1329,16 +1338,19 @@ export default {
noHostsAvailableErrorTitle: 'Hiba a kiszolgáló újraindítása közben',
// Original text: "Some VMs cannot be migrated before restarting this host. Please try force reboot."
noHostsAvailableErrorMessage: 'Some VMs cannot be migrated before restarting this Host. Please try force Restart.',
noHostsAvailableErrorMessage:
'Some VMs cannot be migrated before restarting this Host. Please try force Restart.',
// Original text: "Error while restarting hosts"
failHostBulkRestartTitle: 'Hiba lépett fel a kiszolgálók újraindítása közben',
// Original text: "{failedHosts}/{totalHosts} host{failedHosts, plural, one {} other {s}} could not be restarted."
failHostBulkRestartMessage: '{failedhosts}/{totalHosts} Kiszolgáló újraindítása nem sikerült.',
failHostBulkRestartMessage:
'{failedhosts}/{totalHosts} Kiszolgáló újraindítása nem sikerült.',
// Original text: "Reboot to apply updates"
rebootUpdateHostLabel: 'A változtatások életbe lépéséhez újraindítás szükséges',
rebootUpdateHostLabel:
'A változtatások életbe lépéséhez újraindítás szükséges',
// Original text: "Emergency mode"
emergencyModeLabel: 'Vészhelyzet üzem',
@@ -1427,20 +1439,22 @@ export default {
// Original text: "Installation started"
supplementalPackInstallStartedTitle: 'Installation Started',
// Original text: "Installing new supplemental pack..."
supplementalPackInstallStartedMessage: 'Installing new supplemental pack...',
// Original text: "Installing new supplemental pack"
supplementalPackInstallStartedMessage: 'Installing new supplemental pack',
// Original text: "Installation error"
supplementalPackInstallErrorTitle: 'Installation error',
// Original text: "The installation of the supplemental pack failed."
supplementalPackInstallErrorMessage: 'The installation of the supplemental pack failed.',
supplementalPackInstallErrorMessage:
'The installation of the supplemental pack failed.',
// Original text: "Installation success"
supplementalPackInstallSuccessTitle: 'Installation success',
// Original text: "Supplemental pack successfully installed."
supplementalPackInstallSuccessMessage: 'Supplemental pack successfully installed.',
supplementalPackInstallSuccessMessage:
'Supplemental pack successfully installed.',
// Original text: "Add a network"
networkCreateButton: 'Add a Hálózat',
@@ -1713,7 +1727,8 @@ export default {
tipLabel: 'Tip:',
// Original text: "Due to a XenServer issue, non-US keyboard layouts aren't well supported. Switch your own layout to US to workaround it."
tipConsoleLabel: 'Rendszerkompatibilitás miatt egyedül amerikai (US) billentyűzetkiosztás működik a legstabilabban, ennek használata javasolt.',
tipConsoleLabel:
'Rendszerkompatibilitás miatt egyedül amerikai (US) billentyűzetkiosztás működik a legstabilabban, ennek használata javasolt.',
// Original text: "Hide infos"
hideHeaderTooltip: 'Információk elrejtése',
@@ -1902,7 +1917,8 @@ export default {
vifLockedNetwork: 'Hálózat zárolva',
// Original text: "Network locked and no IPs are allowed for this interface"
vifLockedNetworkNoIps: 'Hálózat zárolva és nincsenek engedélyezve IP címek ehhez az interfészhez',
vifLockedNetworkNoIps:
'Hálózat zárolva és nincsenek engedélyezve IP címek ehhez az interfészhez',
// Original text: "Network not locked"
vifUnLockedNetwork: 'Hálózat nincs zárolva',
@@ -1923,7 +1939,8 @@ export default {
snapshotCreateButton: 'Új Pillanatkép',
// Original text: "Just click on the snapshot button to create one!"
tipCreateSnapshotLabel: 'Csak kattintson a Pillanatkép gombra új pillanatkép készítéséhez!',
tipCreateSnapshotLabel:
'Csak kattintson a Pillanatkép gombra új pillanatkép készítéséhez!',
// Original text: "Revert VM to this snapshot"
revertSnapshot: 'VPS visszaállítása erre a pillanatképre',
@@ -2070,10 +2087,12 @@ export default {
templateDelete: 'Sablon törlése',
// Original text: "Delete VM template{templates, plural, one {} other {s}}"
templateDeleteModalTitle: 'VPS sablon{Templates, plural, one {} other {ok}} törlése',
templateDeleteModalTitle:
'VPS sablon{Templates, plural, one {} other {ok}} törlése',
// Original text: "Are you sure you want to delete {templates, plural, one {this} other {these}} template{templates, plural, one {} other {s}}?"
templateDeleteModalBody: 'Biztos benne, hogy törölni kívánja a kiválasztott {templates, plural, one {this} other {these}} sablon{Templates, plural, one {} other {oka}}t?',
templateDeleteModalBody:
'Biztos benne, hogy törölni kívánja a kiválasztott {templates, plural, one {this} other {these}} sablon{Templates, plural, one {} other {oka}}t?',
// Original text: "Pool{pools, plural, one {} other {s}}"
poolPanel: 'Pool{pools, plural, one {} other {ok}}',
@@ -2169,7 +2188,8 @@ export default {
statsDashboardGenericErrorTitle: 'Statisztikák hiba',
// Original text: "There is no stats available for:"
statsDashboardGenericErrorMessage: 'Jelenleg nincs elérhető statisztika a következőhöz:',
statsDashboardGenericErrorMessage:
'Jelenleg nincs elérhető statisztika a következőhöz:',
// Original text: "No selected metric"
noSelectedMetric: 'Nincs kiválasztott mérőszám',
@@ -2235,7 +2255,8 @@ export default {
newVmCreateNewVmOn2: 'VPS létrehozása a következőn: {select1} vagy {select2}',
// Original text: "You have no permission to create a VM"
newVmCreateNewVmNoPermission: 'Sajnáljuk, nincs jogosultsága új VPS készítéséhez',
newVmCreateNewVmNoPermission:
'Sajnáljuk, nincs jogosultsága új VPS készítéséhez',
// Original text: "Infos"
newVmInfoPanel: 'Információk',
@@ -2445,7 +2466,8 @@ export default {
noHostsAvailable: 'Nincs elérhető kiszolgáló.',
// Original text: "VMs created from this resource set shall run on the following hosts."
availableHostsDescription: 'Ezzel az erőforrás készlettel létrehozott VPS-ek a következő kiszolgálókon tudnak futni.',
availableHostsDescription:
'Ezzel az erőforrás készlettel létrehozott VPS-ek a következő kiszolgálókon tudnak futni.',
// Original text: "Maximum CPUs"
maxCpus: 'Maximum CPU',
@@ -2478,7 +2500,8 @@ export default {
resourceSetNew: 'Új',
// Original text: "Try dropping some VMs files here, or click to select VMs to upload. Accept only .xva/.ova files."
importVmsList: 'Húzza ide a VPS fájlokat, vagy kattintson a VPS választásra a feltöltésre. Csak .xva/.ova fájlok támogatottak.',
importVmsList:
'Húzza ide a VPS fájlokat, vagy kattintson a VPS választásra a feltöltésre. Csak .xva/.ova fájlok támogatottak.',
// Original text: "No selected VMs."
noSelectedVms: 'Nincs kiválasztott VPS.',
@@ -2565,7 +2588,8 @@ export default {
restoreBackups: 'Adatmentések Visszaállítása',
// Original text: "Click on a VM to display restore options"
restoreBackupsInfo: 'Kattintson egy VPS-re a visszaállítási lehetőségek megtekintéséhez',
restoreBackupsInfo:
'Kattintson egy VPS-re a visszaállítási lehetőségek megtekintéséhez',
// Original text: "Enabled"
remoteEnabled: 'Bekapcsolva',
@@ -2655,25 +2679,29 @@ export default {
emergencyShutdownHostsModalTitle: 'Vészhelyzet Kiszolgáló Lekapcsolás',
// Original text: "Are you sure you want to shutdown {nHosts} Host{nHosts, plural, one {} other {s}}?"
emergencyShutdownHostsModalMessage: 'Biztos benne, hogy lekapcsolja ezeket a kiszolgálókat?',
emergencyShutdownHostsModalMessage:
'Biztos benne, hogy lekapcsolja ezeket a kiszolgálókat?',
// Original text: "Shutdown host"
stopHostModalTitle: 'Kiszolgáló Leállítása',
// Original text: "This will shutdown your host. Do you want to continue? If it's the pool master, your connection to the pool will be lost"
stopHostModalMessage: 'Ezzel le fogja kapcsolni a Kiszolgálót. Biztos benne? Amennyiben ez a pool master a kapcsolatot el fogja veszíteni!',
stopHostModalMessage:
'Ezzel le fogja kapcsolni a Kiszolgálót. Biztos benne? Amennyiben ez a pool master a kapcsolatot el fogja veszíteni!',
// Original text: "Add host"
addHostModalTitle: 'Kiszolgáló Hozzáadása',
// Original text: "Are you sure you want to add {host} to {pool}?"
addHostModalMessage: 'Biztos benne, hogy hozzádja a(z) {Host} kiszolgálót a következő poolhoz: {pool}?',
addHostModalMessage:
'Biztos benne, hogy hozzádja a(z) {Host} kiszolgálót a következő poolhoz: {pool}?',
// Original text: "Restart host"
restartHostModalTitle: 'Kiszolgáló Újraindítása',
// Original text: "This will restart your host. Do you want to continue?"
restartHostModalMessage: 'Ez újra fogja indítani a Kiszolgálót. Biztosan folytatja?',
restartHostModalMessage:
'Ez újra fogja indítani a Kiszolgálót. Biztosan folytatja?',
// Original text: "Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}"
restartHostsAgentsModalTitle: 'Kiszolgáló(k) Újraindítása',
@@ -2697,7 +2725,8 @@ export default {
stopHostsModalTitle: 'Kiszolgáló Leállítása',
// Original text: "Are you sure you want to stop {nHosts} Host{nHosts, plural, one {} other {s}}?"
stopHostsModalMessage: 'Biztos benne, hogy leállítja? Ha ez a master, a kapcsolat elveszhet!',
stopHostsModalMessage:
'Biztos benne, hogy leállítja? Ha ez a master, a kapcsolat elveszhet!',
// Original text: "Stop VM{vms, plural, one {} other {s}}"
stopVmsModalTitle: 'VPS Leállítás',
@@ -2733,13 +2762,15 @@ export default {
deleteVmsModalTitle: 'VPS Törlés',
// Original text: "Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED"
deleteVmsModalMessage: 'Biztos benne, hogy törli a VPS-t? ÖSSZES VPS DISZK ELTÁVOLÍTÁSRA KERÜL!',
deleteVmsModalMessage:
'Biztos benne, hogy törli a VPS-t? ÖSSZES VPS DISZK ELTÁVOLÍTÁSRA KERÜL!',
// Original text: "Delete VM"
deleteVmModalTitle: 'VPS Törlés',
// Original text: "Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED"
deleteVmModalMessage: 'Biztos benne, hogy törli a VPS-t? ÖSSZES VPS DISZK ELTÁVOLÍTÁSRA KERÜL!',
deleteVmModalMessage:
'Biztos benne, hogy törli a VPS-t? ÖSSZES VPS DISZK ELTÁVOLÍTÁSRA KERÜL!',
// Original text: "Migrate VM"
migrateVmModalTitle: 'VPS Migrálása',
@@ -2760,10 +2791,12 @@ export default {
migrateVmsSelectSr: 'Válasszon cél Adattárolót:',
// Original text: "Select a destination SR for local disks:"
migrateVmsSelectSrIntraPool: 'Válasszon egy cél Adattárolót a helyi diszkek számára:',
migrateVmsSelectSrIntraPool:
'Válasszon egy cél Adattárolót a helyi diszkek számára:',
// Original text: "Select a network on which to connect each VIF:"
migrateVmsSelectNetwork: 'Válasszon egy Hálózatot amelyekhez csatlakoztasson minden VIF-et:',
migrateVmsSelectNetwork:
'Válasszon egy Hálózatot amelyekhez csatlakoztasson minden VIF-et:',
// Original text: "Smart mapping"
migrateVmsSmartMapping: 'Okos feltérképezés',
@@ -2784,13 +2817,15 @@ export default {
migrateVmNoTargetHost: 'Nincs cél Kiszolgáló',
// Original text: "A target host is required to migrate a VM"
migrateVmNoTargetHostMessage: 'Egy cél Kiszolgáló szükséges a VPS migráláshoz!',
migrateVmNoTargetHostMessage:
'Egy cél Kiszolgáló szükséges a VPS migráláshoz!',
// Original text: "Delete VDI"
deleteVdiModalTitle: 'VDI Törlése',
// Original text: "Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST"
deleteVdiModalMessage: 'Biztos benne, hogy törli a VPS diszkjét? ÖSSZES ADAT ELTÁVOLÍTÁSRA KERÜL!',
deleteVdiModalMessage:
'Biztos benne, hogy törli a VPS diszkjét? ÖSSZES ADAT ELTÁVOLÍTÁSRA KERÜL!',
// Original text: "Revert your VM"
revertVmModalTitle: 'VPS Visszaállítása',
@@ -2799,10 +2834,12 @@ export default {
deleteSnapshotModalTitle: 'Pillanatkép Törlése',
// Original text: "Are you sure you want to delete this snapshot?"
deleteSnapshotModalMessage: 'Biztos benne, hogy törli a kiválasztott Pillanatképet?',
deleteSnapshotModalMessage:
'Biztos benne, hogy törli a kiválasztott Pillanatképet?',
// Original text: "Are you sure you want to revert this VM to the snapshot state? This operation is irreversible."
revertVmModalMessage: 'Biztos benne, hogy visszaállítja a VPS-t a kiválasztott Pillanatkép állapotra? A folyamat visszafordíthatatlan és minden adat elveszik ami a Pillanatkép készítése óta keletkezett!',
revertVmModalMessage:
'Biztos benne, hogy visszaállítja a VPS-t a kiválasztott Pillanatkép állapotra? A folyamat visszafordíthatatlan és minden adat elveszik ami a Pillanatkép készítése óta keletkezett!',
// Original text: "Snapshot before"
revertVmModalSnapshotBefore: 'Pillanatkép ezelőtt',
@@ -2817,7 +2854,8 @@ export default {
importBackupModalSelectBackup: 'Válasszon mentést…',
// Original text: "Are you sure you want to remove all orphaned snapshot VDIs?"
removeAllOrphanedModalWarning: 'Biztos benne, hogy Eltávolítja az összes árvány hagyott Pillanatkép VDI-t?',
removeAllOrphanedModalWarning:
'Biztos benne, hogy Eltávolítja az összes árvány hagyott Pillanatkép VDI-t?',
// Original text: "Remove all logs"
removeAllLogsModalTitle: 'Összes Log Eltávolítása',
@@ -2832,25 +2870,29 @@ export default {
existingSrModalTitle: 'Előző Adattároló használata',
// Original text: "This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation."
existingSrModalText: 'This path has been previously used as a Storage by a XenServer Host. All data will be lost if you choose to continue the Storage Creation.',
existingSrModalText:
'This path has been previously used as a Storage by a XenServer Host. All data will be lost if you choose to continue the Storage Creation.',
// Original text: "Previous LUN Usage"
existingLunModalTitle: 'Előző LUN használat',
// Original text: "This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation."
existingLunModalText: 'This LUN has been previously used as a Storage by a XenServer Host. All data will be lost if you choose to continue the Storage Creation.',
existingLunModalText:
'This LUN has been previously used as a Storage by a XenServer Host. All data will be lost if you choose to continue the Storage Creation.',
// Original text: "Replace current registration?"
alreadyRegisteredModal: 'Replace current registration?',
// Original text: "Your XO appliance is already registered to {email}, do you want to forget and replace this registration ?"
alreadyRegisteredModalText: 'Your XO appliance is already registered to {email}, do you want to Elfelejt and replace this registration ?',
alreadyRegisteredModalText:
'Your XO appliance is already registered to {email}, do you want to Elfelejt and replace this registration ?',
// Original text: "Ready for trial?"
trialReadyModal: 'Ready for trial?',
// Original text: "During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!"
trialReadyModalText: 'During the trial period, XOA need to have a working internet Ceonnection This limitation does not apply for our paid plans!',
trialReadyModalText:
'During the trial period, XOA need to have a working internet Ceonnection This limitation does not apply for our paid plans!',
// Original text: "Host"
serverHost: 'Kiszolgáló',
@@ -2894,8 +2936,8 @@ export default {
// Original text: "Connection failed"
serverConnectionFailed: 'Csatlakozás Sikertelen',
// Original text: "Connecting..."
serverConnecting: 'Csatlakozás...',
// Original text: "Connecting"
serverConnecting: 'Csatlakozás',
// Original text: "Connected"
serverConnected: 'Kapcsolódva',
@@ -2913,7 +2955,8 @@ export default {
copyVm: 'VPS Másolás',
// Original text: "Are you sure you want to copy this VM to {SR}?"
copyVmConfirm: 'Biztos benne, hogy a VPS-t a következő Adattárolóra másolja? {Storage}?',
copyVmConfirm:
'Biztos benne, hogy a VPS-t a következő Adattárolóra másolja? {Storage}?',
// Original text: "Name"
copyVmName: 'Név',
@@ -2943,7 +2986,8 @@ export default {
detachHostModalTitle: 'Detach Host',
// Original text: "Are you sure you want to detach {host} from its pool? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND REBOOT THE HOST."
detachHostModalMessage: 'Biztos benne?? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND RESTART THE HOST.',
detachHostModalMessage:
'Biztos benne?? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND RESTART THE HOST.',
// Original text: "Detach"
detachHost: 'Detach',
@@ -3003,7 +3047,8 @@ export default {
addHostNoHost: 'Nincs Kiszolgáló',
// Original text: "No host selected to be added"
addHostNoHostMessage: 'Nincs Kiszolgáló kiválasztva amihez hozzá lehetne adni',
addHostNoHostMessage:
'Nincs Kiszolgáló kiválasztva amihez hozzá lehetne adni',
// Original text: "Xen Orchestra"
xenOrchestra: 'CLOUDXO',
@@ -3081,7 +3126,8 @@ export default {
availableIn: 'This feature is available Starting from {plan} Edition',
// Original text: "This feature is not available in your version, contact your administrator to know more."
notAvailable: 'This feature is not available in your Version, contact your administrator to know more.',
notAvailable:
'This feature is not available in your Version, contact your administrator to know more.',
// Original text: "Updates"
updateTitle: 'UpDates',
@@ -3129,10 +3175,12 @@ export default {
noUpdaterCommunity: 'No upDate available for Community Edition',
// Original text: "Please consider subscribe and try it with all features for free during 15 days on {link}."
considerSubscribe: 'Please consider subscribe and try it with all features for free during 15 days on {link}.',
considerSubscribe:
'Please consider subscribe and try it with all features for free during 15 days on {link}.',
// Original text: "Manual update could break your current installation due to dependencies issues, do it with caution"
noUpdaterWarning: 'Manual upDate could break your current installation due to dependencies issues, do it with caution',
noUpdaterWarning:
'Manual upDate could break your current installation due to dependencies issues, do it with caution',
// Original text: "Current version:"
currentVersion: 'Jelenlegi Verzió:',
@@ -3144,19 +3192,23 @@ export default {
editRegistration: 'Szerkesztés registration',
// Original text: "Please, take time to register in order to enjoy your trial."
trialRegistration: 'Please, take time to register in order to enjoy your trial.',
trialRegistration:
'Please, take time to register in order to enjoy your trial.',
// Original text: "Start trial"
trialStartButton: 'Elindít trial',
// Original text: "You can use a trial version until {date, date, medium}. Upgrade your appliance to get it."
trialAvailableUntil: 'You can use a trial Verzió until {date, Dátum, medium}. Upgrade your appliance to get it.',
trialAvailableUntil:
'You can use a trial Verzió until {date, Dátum, medium}. Upgrade your appliance to get it.',
// Original text: "Your trial has been ended. Contact us or downgrade to Free version"
trialConsumed: 'Your trial has been ended. Contact us or downgrade to Free Verzió',
trialConsumed:
'Your trial has been ended. Contact us or downgrade to Free Verzió',
// Original text: "Your xoa-updater service appears to be down. Your XOA cannot run fully without reaching this service."
trialLocked: 'Your xoa-upDátumr service appears to be down. Your XOA cannot run fully without reaching this service.',
trialLocked:
'Your xoa-upDátumr service appears to be down. Your XOA cannot run fully without reaching this service.',
// Original text: "No update information available"
noUpdateInfo: 'No upDátum information available',
@@ -3180,19 +3232,23 @@ export default {
promptUpgradeReloadTitle: 'Upgrade successful',
// Original text: "Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?"
promptUpgradeReloadMessage: 'Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?',
promptUpgradeReloadMessage:
'Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?',
// Original text: "Xen Orchestra from the sources"
disclaimerTitle: 'Xen Orchestra from the sources',
// Original text: "You are using XO from the sources! That's great for a personal/non-profit usage."
disclaimerText1: "You are using XO from the sources! That's great for a personal/non-profit használat.",
disclaimerText1:
"You are using XO from the sources! That's great for a personal/non-profit használat.",
// Original text: "If you are a company, it's better to use it with our appliance + pro support included:"
disclaimerText2: "If you are a company, it's better to use it with our appliance + pro support included:",
disclaimerText2:
"If you are a company, it's better to use it with our appliance + pro support included:",
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3: 'This Verzió is not bundled with any support nor upDates. Use it with caution for critical tasks.',
disclaimerText3:
'This Verzió is not bundled with any support nor upDates. Use it with caution for critical tasks.',
// Original text: "Connect PIF"
connectPif: 'Csatlakozás PIF',
@@ -3246,7 +3302,8 @@ export default {
pwdChangeError: 'Helytelen jelszó',
// Original text: "The old password provided is incorrect. Your password has not been changed."
pwdChangeErrorBody: 'A megadott régi jelszó helytelen, így a jelszó NEM lett megváltoztatva!',
pwdChangeErrorBody:
'A megadott régi jelszó helytelen, így a jelszó NEM lett megváltoztatva!',
// Original text: "OK"
changePasswordOk: 'OK',
@@ -3282,7 +3339,8 @@ export default {
deleteSshKeyConfirm: 'SSH kulcs törlése',
// Original text: "Are you sure you want to delete the SSH key {title}?"
deleteSshKeyConfirmMessage: 'Biztos benne, hogy törli a(z) {title} SSH kulcsot?',
deleteSshKeyConfirmMessage:
'Biztos benne, hogy törli a(z) {title} SSH kulcsot?',
// Original text: "Others"
others: 'Egyebek',
@@ -3435,7 +3493,8 @@ export default {
noConfigFile: 'Nincs kiválasztott konfigurációs fájl',
// Original text: "Try dropping a config file here, or click to select a config file to upload."
importTip: 'Try dropping a config file here, or click to choose a config file to upload.',
importTip:
'Try dropping a config file here, or click to choose a config file to upload.',
// Original text: "Config"
config: 'Konfiguráció',
@@ -3456,25 +3515,30 @@ export default {
downloadConfig: 'Download current config',
// Original text: "No config import available for Community Edition"
noConfigImportCommunity: 'No config import available for Community Szerkesztésion',
noConfigImportCommunity:
'No config import available for Community Szerkesztésion',
// Original text: "Reconnect all hosts"
srReconnectAllModalTitle: 'Reconnect all hosts',
// Original text: "This will reconnect this SR to all its hosts."
srReconnectAllModalMessage: 'This will reconnecting this Storage to all its hosts.',
srReconnectAllModalMessage:
'This will reconnecting this Storage to all its hosts.',
// Original text: "This will reconnect each selected SR to its host (local SR) or to every hosts of its pool (shared SR)."
srsReconnectAllModalMessage: 'This will reconnecz each kiválasztott SR to its Kiszolgáló (local SR) or to every kiszolgálók of its pool (Megosztva Adattároló).',
srsReconnectAllModalMessage:
'This will reconnecz each kiválasztott SR to its Kiszolgáló (local SR) or to every kiszolgálók of its pool (Megosztva Adattároló).',
// Original text: "Disconnect all hosts"
srDisconnectAllModalTitle: 'Lecsatlakozás all kiszolgálók',
// Original text: "This will disconnect this SR from all its hosts."
srDisconnectAllModalMessage: 'This will Lecsatlakozás this Adattároló from all its kiszolgálók.',
srDisconnectAllModalMessage:
'This will Lecsatlakozás this Adattároló from all its kiszolgálók.',
// Original text: "This will disconnect each selected SR from its host (local SR) or from every hosts of its pool (shared SR)."
srsDisconnectAllModalMessage: 'This will Lecsatlakozás each kiválasztott SR from its Kiszolgáló (local SR) or from every kiszolgálók of its pool (Megosztva Adattároló).',
srsDisconnectAllModalMessage:
'This will Lecsatlakozás each kiválasztott SR from its Kiszolgáló (local SR) or from every kiszolgálók of its pool (Megosztva Adattároló).',
// Original text: "Forget SR"
srForgetModalTitle: 'Elfelejt Adattároló',
@@ -3483,10 +3547,12 @@ export default {
srsForgetModalTitle: 'Elfelejt kiválasztott Adattárolók',
// Original text: "Are you sure you want to forget this SR? VDIs on this storage won't be removed."
srForgetModalMessage: "Biztos benne, hogyi to Elfelejt this Adattároló? VDIs on this storage won't be Eltávolításd.",
srForgetModalMessage:
"Biztos benne, hogyi to Elfelejt this Adattároló? VDIs on this storage won't be Eltávolításd.",
// Original text: "Are you sure you want to forget all the selected SRs? VDIs on these storages won't be removed."
srsForgetModalMessage: "Biztos benne, hogyi to Elfelejt all the kiválasztott Adattárolók? VDIs on these storages won't be Eltávolításd.",
srsForgetModalMessage:
"Biztos benne, hogyi to Elfelejt all the kiválasztott Adattárolók? VDIs on these storages won't be Eltávolításd.",
// Original text: "Disconnected"
srAllDisconnected: 'Lekapcsolódva',
@@ -3557,7 +3623,7 @@ export default {
// Original text: 'Create'
xosanCreate: undefined,
// Original text: 'Installing XOSAN. Please wait...'
// Original text: 'Installing XOSAN. Please wait'
xosanInstalling: undefined,
// Original text: 'You need XenServer 7.0 to install XOSAN'
@@ -3572,7 +3638,7 @@ export default {
// Original text: 'Load cloud plugin first'
xosanLoadCloudPlugin: undefined,
// Original text: 'Loading...'
// Original text: 'Loading'
xosanLoading: undefined,
// Original text: 'XOSAN is not available at the moment'
@@ -3588,5 +3654,5 @@ export default {
xosanInstallPackOnHosts: undefined,
// Original text: 'Install {pack} v{version}?'
xosanInstallPack: undefined
xosanInstallPack: undefined,
}

View File

@@ -327,7 +327,8 @@ export default {
homeDisplayedItems: '{displayed, number}x {icon} (w {total, number})',
// Original text: "{selected, number}x {icon} selected (on {total, number})"
homeSelectedItems: '{selected, number}x {icon} wybrane {selected, plural, one {} other {s}} (w {total, number})',
homeSelectedItems:
'{selected, number}x {icon} wybrane {selected, plural, one {} other {s}} (w {total, number})',
// Original text: "More"
homeMore: 'Więcej',
@@ -561,16 +562,19 @@ export default {
deleteBackupSchedule: 'Usuń zadanie kopii zapasowej',
// Original text: "Are you sure you want to delete this backup job?"
deleteBackupScheduleQuestion: 'Jesteś pewny że chcesz usunąć zadanie kopii zapasowej?',
deleteBackupScheduleQuestion:
'Jesteś pewny że chcesz usunąć zadanie kopii zapasowej?',
// Original text: "Enable immediately after creation"
scheduleEnableAfterCreation: 'Enable immediately after creation',
// Original text: "You are editing Schedule {name} ({id}). Saving will override previous schedule state."
scheduleEditMessage: 'Edytujesz harmonogram{name} ({id}). Zapisanie zastąpi poprzedni stan harmonogramu',
scheduleEditMessage:
'Edytujesz harmonogram{name} ({id}). Zapisanie zastąpi poprzedni stan harmonogramu',
// Original text: "You are editing job {name} ({id}). Saving will override previous job state."
jobEditMessage: 'Edytujesz zadanie {name} ({id}). Zapisanie zastąpi poprzednie zadanie',
jobEditMessage:
'Edytujesz zadanie {name} ({id}). Zapisanie zastąpi poprzednie zadanie',
// Original text: "No scheduled jobs."
noScheduledJobs: 'Brak zaplanowanych zadań',
@@ -609,7 +613,8 @@ export default {
localRemoteWarningTitle: 'Local remote selected',
// Original text: "Warning: local remotes will use limited XOA disk space. Only for advanced users."
localRemoteWarningMessage: 'Warning: local remotes will use limited XOA disk space. Only for advanced users.',
localRemoteWarningMessage:
'Warning: local remotes will use limited XOA disk space. Only for advanced users.',
// Original text: "VMs"
editBackupVmsTitle: 'VMs',
@@ -963,7 +968,8 @@ export default {
pluginConfigurationPresetTitle: 'Wstępnie zdefiniowana konfiguracja',
// Original text: "Choose a predefined configuration."
pluginConfigurationChoosePreset: 'Wybierz wstępnie zdefiniowaną konfigurację.',
pluginConfigurationChoosePreset:
'Wybierz wstępnie zdefiniowaną konfigurację.',
// Original text: "Apply"
applyPluginPreset: 'Akceptuj',
@@ -1515,7 +1521,8 @@ export default {
tipLabel: 'Wskazówka:',
// Original text: "non-US keyboard could have issues with console: switch your own layout to US."
tipConsoleLabel: 'non-US keyboard could have issues with console: switch your own layout to US.',
tipConsoleLabel:
'non-US keyboard could have issues with console: switch your own layout to US.',
// Original text: "Hide infos"
hideHeaderTooltip: 'Ukryj informacje',
@@ -1701,7 +1708,8 @@ export default {
vifLockedNetwork: 'Sieć zablokowana',
// Original text: "Network locked and no IPs are allowed for this interface"
vifLockedNetworkNoIps: 'Sieć zablokowana i żadne IPs nie są dopuszczone do tego interfejsu',
vifLockedNetworkNoIps:
'Sieć zablokowana i żadne IPs nie są dopuszczone do tego interfejsu',
// Original text: "Network not locked"
vifUnLockedNetwork: 'Sieć niezablokowana',
@@ -1863,7 +1871,8 @@ export default {
templateDelete: 'Usuń szablon',
// Original text: "Delete VM template{templates, plural, one {} other {s}}"
templateDeleteModalTitle: 'Usuń szablon VM{templates, plural, one {} other {s}} de VMs',
templateDeleteModalTitle:
'Usuń szablon VM{templates, plural, one {} other {s}} de VMs',
// Original text: "Are you sure you want to delete {templates, plural, one {this} other {these}} template{templates, plural, one {} other {s}}?"
templateDeleteModalBody: 'Jesteś pewien że chcesz usunąć?',
@@ -1932,7 +1941,8 @@ export default {
srTopUsageStatePanel: 'Top 5 SRs (w %)',
// Original text: "{running} running ({halted} halted)"
vmsStates: '{running} uruchomiona{halted, plural, one {} other {s}} ({halted} zatrzymana{halted, plural, one {} other {s}})',
vmsStates:
'{running} uruchomiona{halted, plural, one {} other {s}} ({halted} zatrzymana{halted, plural, one {} other {s}})',
// Original text: "Clear selection"
dashboardStatsButtonRemoveAll: 'Clear selection',
@@ -2211,7 +2221,8 @@ export default {
deleteResourceSetWarning: 'Delete resource set',
// Original text: "Are you sure you want to delete this resource set?"
deleteResourceSetQuestion: 'Are you sure you want to delete this resource set?',
deleteResourceSetQuestion:
'Are you sure you want to delete this resource set?',
// Original text: "Missing objects:"
resourceSetMissingObjects: 'Brakujące obiekty:',
@@ -2238,7 +2249,8 @@ export default {
noHostsAvailable: 'Brak dostępnych hostów.',
// Original text: "VMs created from this resource set shall run on the following hosts."
availableHostsDescription: 'VMs created from this resource set shall run on the following hosts.',
availableHostsDescription:
'VMs created from this resource set shall run on the following hosts.',
// Original text: "Maximum CPUs"
maxCpus: 'Maximum CPUs',
@@ -2271,7 +2283,8 @@ export default {
resourceSetNew: 'Nowy',
// Original text: "Try dropping some VMs files here, or click to select VMs to upload. Accept only .xva/.ova files."
importVmsList: 'Try dropping some VMs files here, or click to select VMs to upload. Accept only .xva/.ova files.',
importVmsList:
'Try dropping some VMs files here, or click to select VMs to upload. Accept only .xva/.ova files.',
// Original text: "No selected VMs."
noSelectedVms: 'No selected VMs.',
@@ -2400,16 +2413,19 @@ export default {
vmsToBackup: 'VMs do kopii zapasowej',
// Original text: "Emergency shutdown Host{nHosts, plural, one {} other {s}}"
emergencyShutdownHostsModalTitle: 'Wyłączenie awaryjne hosta {nHosts, plural, one {} other {s}}',
emergencyShutdownHostsModalTitle:
'Wyłączenie awaryjne hosta {nHosts, plural, one {} other {s}}',
// Original text: "Are you sure you want to shutdown {nHosts} Host{nHosts, plural, one {} other {s}}?"
emergencyShutdownHostsModalMessage: 'Jesteś peweny że chcesz wyłączyć {nHosts} hosta{nHosts, plural, one {} other {s}}?',
emergencyShutdownHostsModalMessage:
'Jesteś peweny że chcesz wyłączyć {nHosts} hosta{nHosts, plural, one {} other {s}}?',
// Original text: "Shutdown host"
stopHostModalTitle: 'Wyłączenie hosta',
// Original text: "This will shutdown your host. Do you want to continue? If it's the pool master, your connection to the pool will be lost"
stopHostModalMessage: 'To wyłączy twojego hosta. Chcesz kontynuować? Jeżeli jest to zarządca puli, twoje połaczenie do puli zostanie utracone',
stopHostModalMessage:
'To wyłączy twojego hosta. Chcesz kontynuować? Jeżeli jest to zarządca puli, twoje połaczenie do puli zostanie utracone',
// Original text: "Add host"
addHostModalTitle: 'Dodaj hosta',
@@ -2424,34 +2440,40 @@ export default {
restartHostModalMessage: 'To zrestartuje twojego hosta. Chcesz kontynuować?',
// Original text: "Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}"
restartHostsAgentsModalTitle: 'Zrestartuj hosta{nHosts, plural, one {} other {s}}',
restartHostsAgentsModalTitle:
'Zrestartuj hosta{nHosts, plural, one {} other {s}}',
// Original text: "Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?"
restartHostsAgentsModalMessage: "Êtes-vous sûr de vouloir redémarrer les agents {nHosts, plural, one {de l'hôte} other {des hôtes}} ?",
restartHostsAgentsModalMessage:
"Êtes-vous sûr de vouloir redémarrer les agents {nHosts, plural, one {de l'hôte} other {des hôtes}} ?",
// Original text: "Restart Host{nHosts, plural, one {} other {s}}"
restartHostsModalTitle: 'Restart hosta{nHosts, plural, one {} other {s}}',
// Original text: "Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}}?"
restartHostsModalMessage: 'Czy na pewno chcesz zrestartować {nHosts} Host{nHosts, plural, one {} other {s}}?',
restartHostsModalMessage:
'Czy na pewno chcesz zrestartować {nHosts} Host{nHosts, plural, one {} other {s}}?',
// Original text: "Start VM{vms, plural, one {} other {s}}"
startVmsModalTitle: 'Uruchom VM{vms, plural, one {} other {s}}',
// Original text: "Are you sure you want to start {vms} VM{vms, plural, one {} other {s}}?"
startVmsModalMessage: 'Are you sure you want to start {vms} VM{vms, plural, one {} other {s}}?',
startVmsModalMessage:
'Are you sure you want to start {vms} VM{vms, plural, one {} other {s}}?',
// Original text: "Stop Host{nHosts, plural, one {} other {s}}"
stopHostsModalTitle: 'Zatrzymaj hosta{nHosts, plural, one {} other {s}}',
// Original text: "Are you sure you want to stop {nHosts} Host{nHosts, plural, one {} other {s}}?"
stopHostsModalMessage: 'Jesteś pewny że chcesz zatrzymać {nHosts} Host{nHosts, plural, one {} other {s}}?',
stopHostsModalMessage:
'Jesteś pewny że chcesz zatrzymać {nHosts} Host{nHosts, plural, one {} other {s}}?',
// Original text: "Stop VM{vms, plural, one {} other {s}}"
stopVmsModalTitle: 'Zatrzymaj VM {vms, plural, one {} other {s}}',
// Original text: "Are you sure you want to stop {vms} VM{vms, plural, one {} other {s}}?"
stopVmsModalMessage: 'Jesteś pewien że chcesz zatrzymać {vms} VM{vms, plural, one {} other {s}}?',
stopVmsModalMessage:
'Jesteś pewien że chcesz zatrzymać {vms} VM{vms, plural, one {} other {s}}?',
// Original text: "Restart VM"
restartVmModalTitle: 'Restart VM',
@@ -2469,25 +2491,29 @@ export default {
restartVmsModalTitle: 'Restart VM{vms, plural, one {} other {s}}',
// Original text: "Are you sure you want to restart {vms} VM{vms, plural, one {} other {s}}?"
restartVmsModalMessage: 'Are you sure you want to restart {vms} VM{vms, plural, one {} other {s}}?',
restartVmsModalMessage:
'Are you sure you want to restart {vms} VM{vms, plural, one {} other {s}}?',
// Original text: "Snapshot VM{vms, plural, one {} other {s}}"
snapshotVmsModalTitle: 'Snapshot VM{vms, plural, one {} other {s}}',
// Original text: "Are you sure you want to snapshot {vms} VM{vms, plural, one {} other {s}}?"
snapshotVmsModalMessage: 'Jesteś pewny że chcesz zrobić snapshot {vms} VM{vms, plural, one {} other {s}}?',
snapshotVmsModalMessage:
'Jesteś pewny że chcesz zrobić snapshot {vms} VM{vms, plural, one {} other {s}}?',
// Original text: "Delete VM{vms, plural, one {} other {s}}"
deleteVmsModalTitle: 'Delete VM{vms, plural, one {} other {s}}',
// Original text: "Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED"
deleteVmsModalMessage: 'Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
deleteVmsModalMessage:
'Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
// Original text: "Delete VM"
deleteVmModalTitle: 'Usuń VM',
// Original text: "Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED"
deleteVmModalMessage: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
deleteVmModalMessage:
'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
// Original text: "Migrate VM"
migrateVmModalTitle: 'Migruj VM',
@@ -2538,7 +2564,8 @@ export default {
deleteVdiModalTitle: 'Usuń VDI',
// Original text: "Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST"
deleteVdiModalMessage: 'Jesteś pewien że chcesz usunąć dysk? Wszystkie dane na dysku zostaną utracone',
deleteVdiModalMessage:
'Jesteś pewien że chcesz usunąć dysk? Wszystkie dane na dysku zostaną utracone',
// Original text: "Revert your VM"
revertVmModalTitle: 'Revert your VM',
@@ -2550,7 +2577,8 @@ export default {
deleteSnapshotModalMessage: 'Are you sure you want to delete this snapshot?',
// Original text: "Are you sure you want to revert this VM to the snapshot state? This operation is irreversible."
revertVmModalMessage: 'Are you sure you want to revert this VM to the snapshot state? This operation is irreversible.',
revertVmModalMessage:
'Are you sure you want to revert this VM to the snapshot state? This operation is irreversible.',
// Original text: "Snapshot before"
revertVmModalSnapshotBefore: 'Snapshot before',
@@ -2565,7 +2593,8 @@ export default {
importBackupModalSelectBackup: 'Wybierz swój backup…',
// Original text: "Are you sure you want to remove all orphaned snapshot VDIs?"
removeAllOrphanedModalWarning: 'Are you sure you want to remove all orphaned snapshot VDIs?',
removeAllOrphanedModalWarning:
'Are you sure you want to remove all orphaned snapshot VDIs?',
// Original text: "Remove all logs"
removeAllLogsModalTitle: 'Usuń wszystkie logi',
@@ -2580,25 +2609,29 @@ export default {
existingSrModalTitle: 'Previous SR Usage',
// Original text: "This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation."
existingSrModalText: 'This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation.',
existingSrModalText:
'This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation.',
// Original text: "Previous LUN Usage"
existingLunModalTitle: 'Previous LUN Usage',
// Original text: "This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation."
existingLunModalText: 'This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation.',
existingLunModalText:
'This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation.',
// Original text: "Replace current registration?"
alreadyRegisteredModal: 'Replace current registration?',
// Original text: "Your XO appliance is already registered to {email}, do you want to forget and replace this registration ?"
alreadyRegisteredModalText: 'Your XO appliance is already registered to {email}, do you want to forget and replace this registration ?',
alreadyRegisteredModalText:
'Your XO appliance is already registered to {email}, do you want to forget and replace this registration ?',
// Original text: "Ready for trial?"
trialReadyModal: 'Ready for trial?',
// Original text: "During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!"
trialReadyModalText: 'During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!',
trialReadyModalText:
'During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!',
// Original text: "Host"
serverHost: 'Host',
@@ -2664,7 +2697,8 @@ export default {
detachHostModalTitle: 'Detach host',
// Original text: "Are you sure you want to detach {host} from its pool? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND REBOOT THE HOST."
detachHostModalMessage: 'Are you sure you want to detach {host} from its pool? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND REBOOT THE HOST.',
detachHostModalMessage:
'Are you sure you want to detach {host} from its pool? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND REBOOT THE HOST.',
// Original text: "Detach"
detachHost: 'Detach',
@@ -2802,7 +2836,8 @@ export default {
availableIn: 'This feature is available starting from {plan} Edition',
// Original text: "This feature is not available in your version, contact your administrator to know more."
notAvailable: 'This feature is not available in your version, contact your administrator to know more.',
notAvailable:
'This feature is not available in your version, contact your administrator to know more.',
// Original text: "Updates"
updateTitle: 'Aktualizuj',
@@ -2850,10 +2885,12 @@ export default {
noUpdaterCommunity: 'No updater available for Community Edition',
// Original text: "Please consider subscribe and try it with all features for free during 15 days on"
considerSubscribe: 'Please consider subscribe and try it with all features for free during 15 days on',
considerSubscribe:
'Please consider subscribe and try it with all features for free during 15 days on',
// Original text: "Manual update could break your current installation due to dependencies issues, do it with caution"
noUpdaterWarning: 'Manual update could break your current installation due to dependencies issues, do it with caution',
noUpdaterWarning:
'Manual update could break your current installation due to dependencies issues, do it with caution',
// Original text: "Current version:"
currentVersion: 'Obecna wersja:',
@@ -2865,19 +2902,23 @@ export default {
editRegistration: 'Edit registration',
// Original text: "Please, take time to register in order to enjoy your trial."
trialRegistration: 'Please, take time to register in order to enjoy your trial.',
trialRegistration:
'Please, take time to register in order to enjoy your trial.',
// Original text: "Start trial"
trialStartButton: 'Start trial',
// Original text: "You can use a trial version until {date, date, medium}. Upgrade your appliance to get it."
trialAvailableUntil: 'You can use a trial version until {date, date, medium}. Upgrade your appliance to get it.',
trialAvailableUntil:
'You can use a trial version until {date, date, medium}. Upgrade your appliance to get it.',
// Original text: "Your trial has been ended. Contact us or downgrade to Free version"
trialConsumed: 'Twoja wersja demonstracyjna właśnie się zakończyła. Skontaktuj się z nami żeby pobrać darmową wersję',
trialConsumed:
'Twoja wersja demonstracyjna właśnie się zakończyła. Skontaktuj się z nami żeby pobrać darmową wersję',
// Original text: "Your xoa-updater service appears to be down. Your XOA cannot run fully without reaching this service."
trialLocked: 'Your xoa-updater service appears to be down. Your XOA cannot run fully without reaching this service.',
trialLocked:
'Your xoa-updater service appears to be down. Your XOA cannot run fully without reaching this service.',
// Original text: "No update information available"
noUpdateInfo: 'No update information available',
@@ -2901,19 +2942,23 @@ export default {
promptUpgradeReloadTitle: 'Aktualizacja zakończona sukcesem',
// Original text: "Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?"
promptUpgradeReloadMessage: 'Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?',
promptUpgradeReloadMessage:
'Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?',
// Original text: "Xen Orchestra from the sources"
disclaimerTitle: 'Xen Orchestra z źródeł',
// Original text: "You are using XO from the sources! That's great for a personal/non-profit usage."
disclaimerText1: 'Używasz XO z źródeł!. To dobre rozwiązanie tylko do prywatnego/nieprodukcyjnego użytku',
disclaimerText1:
'Używasz XO z źródeł!. To dobre rozwiązanie tylko do prywatnego/nieprodukcyjnego użytku',
// Original text: "If you are a company, it's better to use it with our appliance + pro support included:"
disclaimerText2: "If you are a company, it's better to use it with our appliance + pro support included:",
disclaimerText2:
"If you are a company, it's better to use it with our appliance + pro support included:",
// Original text: "This version is not bundled with any support nor updates. Use it with caution for critical tasks."
disclaimerText3: 'This version is not bundled with any support nor updates. Use it with caution for critical tasks.',
disclaimerText3:
'This version is not bundled with any support nor updates. Use it with caution for critical tasks.',
// Original text: "Connect PIF"
connectPif: 'Connect PIF',
@@ -2967,7 +3012,8 @@ export default {
pwdChangeError: 'Nieprawidłowe hasło',
// Original text: "The old password provided is incorrect. Your password has not been changed."
pwdChangeErrorBody: 'The old password provided is incorrect. Your password has not been changed.',
pwdChangeErrorBody:
'The old password provided is incorrect. Your password has not been changed.',
// Original text: "OK"
changePasswordOk: 'OK',
@@ -3003,7 +3049,8 @@ export default {
deleteSshKeyConfirm: 'Usuń klucz SSH',
// Original text: "Are you sure you want to delete the SSH key {title}?"
deleteSshKeyConfirmMessage: 'Are you sure you want to delete the SSH key {title}?',
deleteSshKeyConfirmMessage:
'Are you sure you want to delete the SSH key {title}?',
// Original text: "Others"
others: 'Inne',
@@ -3135,5 +3182,5 @@ export default {
settingsAclsButtonTooltipSR: 'SR',
// Original text: "Network"
settingsAclsButtonTooltipnetwork: 'Sieć'
settingsAclsButtonTooltipnetwork: 'Sieć',
}

View File

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

View File

@@ -285,7 +285,7 @@ export default {
homeMore: '更多',
// Original text: "Migrate to…"
homeMigrateTo: '迁移至...',
homeMigrateTo: '迁移至',
// Original text: "Missing patches"
homeMissingPaths: '缺少补丁',
@@ -1467,7 +1467,7 @@ export default {
statsDashboardSelectObjects: '选择',
// Original text: "Loading…"
metricsLoading: '加载中....',
metricsLoading: '加载中.',
// Original text: "Coming soon!"
comingSoon: '即将呈现',
@@ -1728,7 +1728,8 @@ export default {
usedResource: '已使用',
// Original text: "Try dropping some backups here, or click to select backups to upload. Accept only .xva files."
importVmsList: '尝试将备份文件拖拽到这里,或点击选择备份文件上传,仅支持.xva格式的文件',
importVmsList:
'尝试将备份文件拖拽到这里,或点击选择备份文件上传,仅支持.xva格式的文件',
// Original text: "No selected VMs."
noSelectedVms: '没有选择虚拟机',
@@ -1812,10 +1813,12 @@ export default {
importBackupMessage: '开始你的备份导入',
// Original text: "Emergency shutdown Host{nHosts, plural, one {} other {s}}"
emergencyShutdownHostsModalTitle: '紧急关闭主机{nHosts, plural, one {} other {s}}',
emergencyShutdownHostsModalTitle:
'紧急关闭主机{nHosts, plural, one {} other {s}}',
// Original text: "Are you sure you want to shutdown {nHosts} Host{nHosts, plural, one {} other {s}}?"
emergencyShutdownHostsModalMessage: '你确定要关闭 {nHosts} 主机{nHosts, plural, one {} other {s}}',
emergencyShutdownHostsModalMessage:
'你确定要关闭 {nHosts} 主机{nHosts, plural, one {} other {s}}',
// Original text: "Shutdown host"
stopHostModalTitle: '关闭主机',
@@ -1830,34 +1833,40 @@ export default {
restartHostModalMessage: '此操作将重启你的主机,你确定要继续吗?',
// Original text: "Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}"
restartHostsAgentsModalTitle: '重启主机{nHosts, plural, one {} other {s}} 代理{nHosts, plural, one {} other {s}}',
restartHostsAgentsModalTitle:
'重启主机{nHosts, plural, one {} other {s}} 代理{nHosts, plural, one {} other {s}}',
// Original text: "Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?"
restartHostsAgentsModalMessage: '你确定要重启{nHosts}主机{nHosts, plural, one {} other {s}} 代理{nHosts, plural, one {} other {s}}',
restartHostsAgentsModalMessage:
'你确定要重启{nHosts}主机{nHosts, plural, one {} other {s}} 代理{nHosts, plural, one {} other {s}}',
// Original text: "Restart Host{nHosts, plural, one {} other {s}}"
restartHostsModalTitle: '重启主机{nHosts, plural, one {} other {s}}',
// Original text: "Are you sure you want to restart {nHosts} Host{nHosts, plural, one {} other {s}}?"
restartHostsModalMessage: '你确定要重启{nHosts}主机{nHosts, plural, one {} other {s}}',
restartHostsModalMessage:
'你确定要重启{nHosts}主机{nHosts, plural, one {} other {s}}',
// Original text: "Start VM{vms, plural, one {} other {s}}"
startVmsModalTitle: '启动虚拟机{vms, plural, one {} other {s}}',
// Original text: "Are you sure you want to start {vms} VM{vms, plural, one {} other {s}}?"
startVmsModalMessage: '你确定要启动 {vms} 虚拟机{vms, plural, one {} other {s}}',
startVmsModalMessage:
'你确定要启动 {vms} 虚拟机{vms, plural, one {} other {s}}',
// Original text: "Stop Host{nHosts, plural, one {} other {s}}"
stopHostsModalTitle: '停止主机{nHosts, plural, one {} other {s}}',
// Original text: "Are you sure you want to stop {nHosts} Host{nHosts, plural, one {} other {s}}?"
stopHostsModalMessage: '你确定要停止{nHosts}主机{nHosts, plural, one {} other {s}}',
stopHostsModalMessage:
'你确定要停止{nHosts}主机{nHosts, plural, one {} other {s}}',
// Original text: "Stop VM{vms, plural, one {} other {s}}"
stopVmsModalTitle: '停止虚拟机{vms, plural, one {} other {s}}',
// Original text: "Are you sure you want to stop {vms} VM{vms, plural, one {} other {s}}?"
stopVmsModalMessage: '你确定要停止{vms}虚拟机{vms, plural, one {} other {s}}',
stopVmsModalMessage:
'你确定要停止{vms}虚拟机{vms, plural, one {} other {s}}',
// Original text: "Restart VM"
restartVmModalTitle: '重新启动虚拟机',
@@ -1875,13 +1884,15 @@ export default {
restartVmsModalTitle: '重新启动虚拟机{vms, plural, one {} other {s}}',
// Original text: "Are you sure you want to restart {vms} VM{vms, plural, one {} other {s}}?"
restartVmsModalMessage: '你确定要重新启动{vms}虚拟机{vms, plural, one {} other {s}}',
restartVmsModalMessage:
'你确定要重新启动{vms}虚拟机{vms, plural, one {} other {s}}',
// Original text: "Snapshot VM{vms, plural, one {} other {s}}"
snapshotVmsModalTitle: '执行虚拟机快照{vms, plural, one {} other {s}}',
// Original text: "Are you sure you want to snapshot {vms} VM{vms, plural, one {} other {s}}?"
snapshotVmsModalMessage: '你确定要执行虚拟机{vms}快照{vms, plural, one {} other {s}}',
snapshotVmsModalMessage:
'你确定要执行虚拟机{vms}快照{vms, plural, one {} other {s}}',
// Original text: "Delete VM"
deleteVmModalTitle: '删除虚拟机',
@@ -1893,7 +1904,8 @@ export default {
deleteVmModalMessage: '你确定要删除此虚拟机?所有的虚拟机磁盘将被删除',
// Original text: "Are you sure you want to delete {vms} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED"
deleteVmsModalMessage: '你确定要删除 {vms}虚拟机{vms, plural, one {} other {s}}?所有的虚拟机磁盘将被删除',
deleteVmsModalMessage:
'你确定要删除 {vms}虚拟机{vms, plural, one {} other {s}}?所有的虚拟机磁盘将被删除',
// Original text: "Migrate VM"
migrateVmModalTitle: '迁移虚拟机',
@@ -1947,7 +1959,7 @@ export default {
importBackupModalStart: '恢复后启动虚拟机',
// Original text: "Select your backup…"
importBackupModalSelectBackup: '选择你的备份...',
importBackupModalSelectBackup: '选择你的备份',
// Original text: "Are you sure you want to remove all orphaned VDIs?"
removeAllOrphanedModalWarning: '你确定要删除所有孤立的虚拟磁盘?',
@@ -1965,25 +1977,29 @@ export default {
existingSrModalTitle: '之前存储库的使用情况',
// Original text: "This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation."
existingSrModalText: '这条路径之前已经被一台XenServer主机用来连接存储。如果你选择继续创建存储库所有的数据将丢失。',
existingSrModalText:
'这条路径之前已经被一台XenServer主机用来连接存储。如果你选择继续创建存储库所有的数据将丢失。',
// Original text: "Previous LUN Usage"
existingLunModalTitle: '之前LUN使用情况',
// Original text: "This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation."
existingLunModalText: '这个LUN之前已经被一台XenServer主机使用。如果你选择继续创建存储库所有的数据将丢失。',
existingLunModalText:
'这个LUN之前已经被一台XenServer主机使用。如果你选择继续创建存储库所有的数据将丢失。',
// Original text: "Replace current registration?"
alreadyRegisteredModal: '替换当前的注册?',
// Original text: "Your XO appliance is already registered to {email}, do you want to forget and replace this registration ?"
alreadyRegisteredModalText: '你的XO设备已经注册给{email},你确定要删除并替换这个注册信息?',
alreadyRegisteredModalText:
'你的XO设备已经注册给{email},你确定要删除并替换这个注册信息?',
// Original text: "Ready for trial?"
trialReadyModal: '准备试用?',
// Original text: "During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!"
trialReadyModalText: '在试用期内XOA需要Internet连接才能正常使用如果您正式付费将不受此限制',
trialReadyModalText:
'在试用期内XOA需要Internet连接才能正常使用如果您正式付费将不受此限制',
// Original text: "Host"
serverHost: '主机',
@@ -2160,7 +2176,8 @@ export default {
noUpdaterSubscribe: '请考虑订购或在15天内免费试用所有功能',
// Original text: "Manual update could break your current installation due to dependencies issues, do it with caution"
noUpdaterWarning: '由于相关依赖关系的问题,手动更新将跑坏你当前的安全,请小心使用',
noUpdaterWarning:
'由于相关依赖关系的问题,手动更新将跑坏你当前的安全,请小心使用',
// Original text: "Current version:"
currentVersion: '当前版本',
@@ -2175,7 +2192,8 @@ export default {
trialStartButton: '开始试用',
// Original text: "You can use a trial version until {date, date, medium}. Upgrade your appliance to get it."
trialAvailableUntil: '你可以使用试用版本直到{date, date, medium}。更新你的设备来获取',
trialAvailableUntil:
'你可以使用试用版本直到{date, date, medium}。更新你的设备来获取',
// Original text: "Your trial has been ended. Contact us or downgrade to Free version"
trialConsumed: '你的使用已经结束,联系我们或下载免费版本',
@@ -2205,7 +2223,8 @@ export default {
promptUpgradeReloadTitle: '更新成功',
// Original text: "Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?"
promptUpgradeReloadMessage: '你的XOA已经成功更新你的浏览器必须重新加载你要现在重新加载吗',
promptUpgradeReloadMessage:
'你的XOA已经成功更新你的浏览器必须重新加载你要现在重新加载吗',
// Original text: "Xen Orchestra from the sources"
disclaimerTitle: 'Xen Orchestra 源码版',
@@ -2277,5 +2296,5 @@ export default {
changePasswordOk: '确认',
// Original text: "Others"
others: '其他'
others: '其他',
}

View File

@@ -1,10 +1,12 @@
// This file is coded in ES5 and CommonJS to be compatible with
// `create-locale`.
var forEach = require('lodash/forEach')
var isString = require('lodash/isString')
const forEach = require('lodash/forEach')
const isString = require('lodash/isString')
const messages = {
keyValue: '{key}: {value}',
var messages = {
statusConnecting: 'Connecting',
statusDisconnected: 'Disconnected',
statusLoading: 'Loading…',
@@ -28,6 +30,8 @@ var messages = {
filterOnlyRegular: 'Normal disks',
filterOnlySnapshots: 'Snapshot disks',
filterOnlyUnmanaged: 'Unmanaged disks',
filterSaveAs: 'Save…',
filterSyntaxLinkTooltip: 'Explore the search syntax in the documentation',
// ----- Copiable component -----
copyToClipboard: 'Copy to clipboard',
@@ -101,9 +105,10 @@ var messages = {
// ----- Home view ------
homeFetchingData: 'Fetching data…',
homeWelcome: 'Welcome on Xen Orchestra!',
homeWelcome: 'Welcome to Xen Orchestra!',
homeWelcomeText: 'Add your XenServer hosts or pools',
homeConnectServerText: 'Some XenServers have been registered but are not connected',
homeConnectServerText:
'Some XenServers have been registered but are not connected',
homeHelp: 'Want some help?',
homeAddServer: 'Add server',
homeConnectServer: 'Connect servers',
@@ -127,6 +132,7 @@ var messages = {
homeAllPools: 'Pools',
homeAllHosts: 'Hosts',
homeAllTags: 'Tags',
homeAllResourceSets: 'Resource sets',
homeNewVm: 'New VM',
homeFilterNone: 'None',
homeFilterRunningHosts: 'Running hosts',
@@ -137,15 +143,16 @@ var messages = {
homeFilterHvmGuests: 'HVM guests',
homeFilterTags: 'Tags',
homeSortBy: 'Sort by',
homeSortByCpus: 'CPUs',
homeSortByName: 'Name',
homeSortByPowerstate: 'Power state',
homeSortByRAM: 'RAM',
homeSortByvCPUs: 'vCPUs',
homeSortByCpus: 'CPUs',
homeSortByShared: 'Shared/Not shared',
homeSortBySize: 'Size',
homeSortByUsage: 'Usage',
homeSortByType: 'Type',
homeSortByUsage: 'Usage',
homeSortByvCPUs: 'vCPUs',
homeSortVmsBySnapshots: 'Snapshots',
homeDisplayedItems: '{displayed, number}x {icon} (on {total, number})',
homeSelectedItems: '{selected, number}x {icon} selected (on {total, number})',
homeMore: 'More',
@@ -157,6 +164,15 @@ var messages = {
srSharedType: 'Shared {type}',
srNotSharedType: 'Not shared {type}',
// ----- Common components -----
sortedTableAllItemsSelected: 'All of them are selected',
sortedTableNoItems: 'No items found',
sortedTableNumberOfFilteredItems:
'{nFiltered, number} of {nTotal, number} items',
sortedTableNumberOfItems: '{nTotal, number} items',
sortedTableNumberOfSelectedItems: '{nSelected, number} selected',
sortedTableSelectAllItems: 'Click here to select all items',
// ----- Forms -----
add: 'Add',
selectAll: 'Select all',
@@ -187,6 +203,7 @@ var messages = {
selectTimezone: 'Select timezone…',
selectIp: 'Select IP(s)…',
selectIpPool: 'Select IP pool(s)…',
selectVgpuType: 'Select VGPU type(s)…',
fillRequiredInformations: 'Fill required informations.',
fillOptionalInformations: 'Fill informations (optional)',
selectTableReset: 'Reset',
@@ -221,8 +238,11 @@ var messages = {
successfulJobCall: 'Successful',
failedJobCall: 'Failed',
jobCallInProgess: 'In progress',
jobTransferredDataSize: 'size:',
jobTransferredDataSpeed: 'speed:',
jobTransferredDataSize: 'Transfer size:',
jobTransferredDataSpeed: 'Transfer speed:',
jobMergedDataSize: 'Merge size:',
jobMergedDataSpeed: 'Merge speed:',
allJobCalls: 'All',
job: 'Job',
jobModalTitle: 'Job {job}',
jobId: 'ID',
@@ -247,21 +267,25 @@ var messages = {
jobFinished: 'Finished',
saveBackupJob: 'Save',
deleteBackupSchedule: 'Remove backup job',
deleteBackupScheduleQuestion: 'Are you sure you want to delete this backup job?',
deleteBackupScheduleQuestion:
'Are you sure you want to delete this backup job?',
scheduleEnableAfterCreation: 'Enable immediately after creation',
scheduleEditMessage: 'You are editing Schedule {name} ({id}). Saving will override previous schedule state.',
jobEditMessage: 'You are editing job {name} ({id}). Saving will override previous job state.',
scheduleEditMessage:
'You are editing Schedule {name} ({id}). Saving will override previous schedule state.',
jobEditMessage:
'You are editing job {name} ({id}). Saving will override previous job state.',
noScheduledJobs: 'No scheduled jobs.',
noJobs: 'No jobs found.',
noSchedules: 'No schedules found',
jobActionPlaceHolder: 'Select a xo-server API command',
jobTimeoutPlaceHolder: ' Timeout (number of seconds after which a VM is considered failed)',
jobTimeoutPlaceHolder:
'Timeout (number of seconds after which a VM is considered failed)',
jobSchedules: 'Schedules',
jobScheduleNamePlaceHolder: 'Name of your schedule',
jobScheduleJobPlaceHolder: 'Select a Job',
jobOwnerPlaceholder: 'Job owner',
jobUserNotFound: 'This job\'s creator no longer exists',
backupUserNotFound: 'This backup\'s creator no longer exists',
jobUserNotFound: "This job's creator no longer exists",
backupUserNotFound: "This backup's creator no longer exists",
backupOwner: 'Backup owner',
// ------ New backup -----
@@ -270,8 +294,10 @@ var messages = {
normalBackup: 'Normal backup',
smartBackup: 'Smart backup',
localRemoteWarningTitle: 'Local remote selected',
localRemoteWarningMessage: 'Warning: local remotes will use limited XOA disk space. Only for advanced users.',
backupVersionWarning: 'Warning: this feature works only with XenServer 6.5 or newer.',
localRemoteWarningMessage:
'Warning: local remotes will use limited XOA disk space. Only for advanced users.',
backupVersionWarning:
'Warning: this feature works only with XenServer 6.5 or newer.',
editBackupVmsTitle: 'VMs',
editBackupSmartStatusTitle: 'VMs statuses',
editBackupSmartResidentOn: 'Resident on',
@@ -282,7 +308,7 @@ var messages = {
editBackupTagTitle: 'Tag',
editBackupReportTitle: 'Report',
editBackupScheduleEnabled: 'Automatically run as scheduled',
editBackupDepthTitle: 'Depth',
editBackupRetentionTitle: 'Retention',
editBackupRemoteTitle: 'Remote',
deleteOldBackupsFirst: 'Delete the old backups first',
@@ -293,6 +319,8 @@ var messages = {
remoteTypeNfs: 'NFS',
remoteTypeSmb: 'SMB',
remoteType: 'Type',
remoteSmbWarningMessage:
'SMB remotes are meant to work on Windows Server. For other systems (Linux Samba, which means almost all NAS), please use NFS.',
remoteTestTip: 'Test your remote',
testRemote: 'Test Remote',
remoteTestFailure: 'Test failed for {name}',
@@ -404,7 +432,8 @@ var messages = {
pluginError: 'Plugin error',
unknownPluginError: 'Unknown error',
purgePluginConfiguration: 'Purge plugin configuration',
purgePluginConfigurationQuestion: 'Are you sure you want to purge this configuration ?',
purgePluginConfigurationQuestion:
'Are you sure you want to purge this configuration ?',
editPluginConfiguration: 'Edit',
cancelPluginEdition: 'Cancel',
pluginConfigurationSuccess: 'Plugin configuration',
@@ -448,7 +477,13 @@ var messages = {
convertVmToTemplateLabel: 'Convert to template',
vmConsoleLabel: 'Console',
// ----- SR tabs -----
// ----- SR advanced tab -----
srUnhealthyVdiNameLabel: 'Name',
srUnhealthyVdiSize: 'Size',
srUnhealthyVdiDepth: 'Depth',
srUnhealthyVdiTitle: 'VDI to coalesce ({total, number})',
// ----- SR actions -----
srRescan: 'Rescan all disks',
srReconnectAll: 'Connect to all hosts',
@@ -472,6 +507,8 @@ var messages = {
poolHaStatus: 'High Availability',
poolHaEnabled: 'Enabled',
poolHaDisabled: 'Disabled',
setpoolMaster: 'Master',
poolGpuGroups: 'GPU groups',
// ----- Pool host tab -----
hostNameLabel: 'Name',
hostDescription: 'Description',
@@ -498,8 +535,10 @@ var messages = {
addSrLabel: 'Add SR',
addVmLabel: 'Add VM',
addHostLabel: 'Add Host',
hostNeedsPatchUpdate: 'This host needs to install {patches, number} patch{patches, plural, one {} other {es}} before it can be added to the pool. This operation may be long.',
hostNeedsPatchUpdateNoInstall: 'This host cannot be added to the pool because it\'s missing some patches.',
hostNeedsPatchUpdate:
'This host needs to install {patches, number} patch{patches, plural, one {} other {es}} before it can be added to the pool. This operation may be long.',
hostNeedsPatchUpdateNoInstall:
"This host cannot be added to the pool because it's missing some patches.",
addHostErrorTitle: 'Adding host failed',
addHostNotHomogeneousErrorMessage: 'Host patches could not be homogenized.',
disconnectServer: 'Disconnect',
@@ -513,17 +552,19 @@ var messages = {
forceRebootHostLabel: 'Force reboot',
rebootHostLabel: 'Reboot',
noHostsAvailableErrorTitle: 'Error while restarting host',
noHostsAvailableErrorMessage: 'Some VMs cannot be migrated before restarting this host. Please try force reboot.',
noHostsAvailableErrorMessage:
'Some VMs cannot be migrated before restarting this host. Please try force reboot.',
failHostBulkRestartTitle: 'Error while restarting hosts',
failHostBulkRestartMessage: '{failedHosts, number}/{totalHosts, number} host{failedHosts, plural, one {} other {s}} could not be restarted.',
failHostBulkRestartMessage:
'{failedHosts, number}/{totalHosts, number} host{failedHosts, plural, one {} other {s}} could not be restarted.',
rebootUpdateHostLabel: 'Reboot to apply updates',
emergencyModeLabel: 'Emergency mode',
// ----- Host tabs -----
storageTabName: 'Storage',
patchesTabName: 'Patches',
// ----- host stat tab -----
// ----- host stat tab -----
statLoad: 'Load average',
// ----- host advanced tab -----
// ----- host advanced tab -----
memoryHostState: 'RAM Usage: {memoryUsed}',
hardwareHostSettingsLabel: 'Hardware',
hostAddress: 'Address',
@@ -537,10 +578,11 @@ var messages = {
hostStartedSince: 'Host uptime',
hostStackStartedSince: 'Toolstack uptime',
hostCpusModel: 'CPU model',
hostGpus: 'GPUs',
hostCpusNumber: 'Core (socket)',
hostManufacturerinfo: 'Manufacturer info',
hostBiosinfo: 'BIOS info',
licenseHostSettingsLabel: 'Licence',
licenseHostSettingsLabel: 'License',
hostLicenseType: 'Type',
hostLicenseSocket: 'Socket',
hostLicenseExpiry: 'Expiry',
@@ -549,11 +591,13 @@ var messages = {
supplementalPackPoolNew: 'Install supplemental pack on every host',
supplementalPackTitle: '{name} (by {author})',
supplementalPackInstallStartedTitle: 'Installation started',
supplementalPackInstallStartedMessage: 'Installing new supplemental pack...',
supplementalPackInstallStartedMessage: 'Installing new supplemental pack',
supplementalPackInstallErrorTitle: 'Installation error',
supplementalPackInstallErrorMessage: 'The installation of the supplemental pack failed.',
supplementalPackInstallErrorMessage:
'The installation of the supplemental pack failed.',
supplementalPackInstallSuccessTitle: 'Installation success',
supplementalPackInstallSuccessMessage: 'Supplemental pack successfully installed.',
supplementalPackInstallSuccessMessage:
'Supplemental pack successfully installed.',
// ----- Host net tabs -----
networkCreateButton: 'Add a network',
networkCreateBondedButton: 'Add a bonded network',
@@ -601,7 +645,7 @@ var messages = {
patchStatus: 'Status',
patchStatusApplied: 'Applied',
patchStatusNotApplied: 'Missing patches',
patchNothing: 'No patch detected',
patchNothing: 'No patches detected',
patchReleaseDate: 'Release date',
patchGuidance: 'Guidance',
patchAction: 'Action',
@@ -609,7 +653,8 @@ var messages = {
hostMissingPatches: 'Missing patches',
hostUpToDate: 'Host up-to-date!',
installPatchWarningTitle: 'Non-recommended patch install',
installPatchWarningContent: 'This will install a patch only on this host. This is NOT the recommended way: please go into the Pool patch view and follow instructions there. If you are sure about this, you can continue anyway',
installPatchWarningContent:
'This will install a patch only on this host. This is NOT the recommended way: please go into the Pool patch view and follow instructions there. If you are sure about this, you can continue anyway',
installPatchWarningReject: 'Go to pool',
installPatchWarningResolve: 'Install',
// ----- Pool patch tabs -----
@@ -720,6 +765,12 @@ var messages = {
cdDriveInstallation: 'Stop and start the VM to install the CD drive',
saveBootOption: 'Save',
resetBootOption: 'Reset',
deleteSelectedVdis: 'Delete selected VDIs',
deleteSelectedVdi: 'Delete selected VDI',
useQuotaWarning:
'Creating this disk will use the disk space quota from the resource set {resourceSet} ({spaceLeft} left)',
notEnoughSpaceInResourceSet:
'Not enough space in resource set {resourceSet} ({spaceLeft} left)',
// ----- VM network tab -----
vifCreateDeviceButton: 'New device',
@@ -739,7 +790,8 @@ var messages = {
vifAllowedIps: 'Allowed IPs',
vifNoIps: 'No IPs',
vifLockedNetwork: 'Network locked',
vifLockedNetworkNoIps: 'Network locked and no IPs are allowed for this interface',
vifLockedNetworkNoIps:
'Network locked and no IPs are allowed for this interface',
vifUnLockedNetwork: 'Network not locked',
vifUnknownNetwork: 'Unknown network',
vifAction: 'Action',
@@ -755,6 +807,7 @@ var messages = {
exportSnapshot: 'Export this snapshot',
snapshotDate: 'Creation date',
snapshotName: 'Name',
snapshotDescription: 'Description',
snapshotAction: 'Action',
snapshotQuiesce: 'Quiesced snapshot',
@@ -782,7 +835,8 @@ var messages = {
xenToolsStatus: 'Xen tools status',
xenToolsStatusValue: {
defaultMessage: '{status}',
description: 'status can be `not-installed`, `unknown`, `out-of-date` & `up-to-date`'
description:
'status can be `not-installed`, `unknown`, `out-of-date` & `up-to-date`',
},
osName: 'OS name',
osKernel: 'OS kernel',
@@ -800,12 +854,19 @@ var messages = {
vmCpuLimitsLabel: 'CPU limits',
vmCpuTopology: 'Topology',
vmChooseCoresPerSocket: 'Default behavior',
vmCoresPerSocket: '{nSockets, number} socket{nSockets, plural, one {} other {s}} with {nCores, number} core{nCores, plural, one {} other {s}} per socket',
vmCoresPerSocket:
'{nSockets, number} socket{nSockets, plural, one {} other {s}} with {nCores, number} core{nCores, plural, one {} other {s}} per socket',
vmCoresPerSocketIncorrectValue: 'Incorrect cores per socket value',
vmCoresPerSocketIncorrectValueSolution: 'Please change the selected value to fix it.',
vmCoresPerSocketIncorrectValueSolution:
'Please change the selected value to fix it.',
vmMemoryLimitsLabel: 'Memory limits (min/max)',
vmMaxVcpus: 'vCPUs max:',
vmMaxRam: 'Memory max:',
vmVgpu: 'vGPU',
vmVgpus: 'GPUs',
vmVgpuNone: 'None',
vmAddVgpu: 'Add vGPU',
vmSelectVgpuType: 'Select vGPU type',
// ----- VM placeholders -----
@@ -819,8 +880,10 @@ var messages = {
templateHomeNamePlaceholder: 'Click to add a name',
templateHomeDescriptionPlaceholder: 'Click to add a description',
templateDelete: 'Delete template',
templateDeleteModalTitle: 'Delete VM template{templates, plural, one {} other {s}}',
templateDeleteModalBody: 'Are you sure you want to delete {templates, plural, one {this} other {these}} template{templates, plural, one {} other {s}}?',
templateDeleteModalTitle:
'Delete VM template{templates, plural, one {} other {s}}',
templateDeleteModalBody:
'Are you sure you want to delete {templates, plural, one {this} other {these}} template{templates, plural, one {} other {s}}?',
// ----- Dashboard -----
poolPanel: 'Pool{pools, plural, one {} other {s}}',
@@ -884,7 +947,6 @@ var messages = {
// ----- New VM -----
newVmCreateNewVmOn: 'Create a new VM on {select}',
newVmCreateNewVmOn2: 'Create a new VM on {select1} or {select2}',
newVmCreateNewVmNoPermission: 'You have no permission to create a VM',
newVmInfoPanel: 'Infos',
newVmNameLabel: 'Name',
@@ -949,7 +1011,8 @@ var messages = {
editResourceSet: 'Edit',
deleteResourceSet: 'Delete',
deleteResourceSetWarning: 'Delete resource set',
deleteResourceSetQuestion: 'Are you sure you want to delete this resource set?',
deleteResourceSetQuestion:
'Are you sure you want to delete this resource set?',
resourceSetMissingObjects: 'Missing objects:',
resourceSetVcpus: 'vCPUs',
resourceSetMemory: 'Memory',
@@ -958,7 +1021,8 @@ var messages = {
availableHosts: 'Available hosts',
excludedHosts: 'Excluded hosts',
noHostsAvailable: 'No hosts available.',
availableHostsDescription: 'VMs created from this resource set shall run on the following hosts.',
availableHostsDescription:
'VMs created from this resource set shall run on the following hosts.',
maxCpus: 'Maximum CPUs',
maxRam: 'Maximum RAM (GiB)',
maxDiskSpace: 'Maximum disk space',
@@ -971,7 +1035,8 @@ var messages = {
resourceSetNew: 'New',
// ---- VM import ---
importVmsList: 'Try dropping some VMs files here, or click to select VMs to upload. Accept only .xva/.ova files.',
importVmsList:
'Try dropping some VMs files here, or click to select VMs to upload. Accept only .xva/.ova files.',
noSelectedVms: 'No selected VMs.',
vmImportToPool: 'To Pool:',
vmImportToSr: 'To SR:',
@@ -1005,7 +1070,8 @@ var messages = {
delta: 'delta',
restoreBackups: 'Restore Backups',
restoreBackupsInfo: 'Click on a VM to display restore options',
restoreDeltaBackupsInfo: 'Only the files of Delta Backup which are not on a SMB remote can be restored',
restoreDeltaBackupsInfo:
'Only the files of Delta Backup which are not on a SMB remote can be restored',
remoteEnabled: 'Enabled',
remoteError: 'Error',
noBackup: 'No backup available',
@@ -1038,47 +1104,63 @@ var messages = {
restoreFilesNoFilesSelected: 'No files selected',
restoreFilesSelectedFiles: 'Selected files ({files}):',
restoreFilesDiskError: 'Error while scanning disk',
restoreFilesSelectAllFiles: 'Select all this folder\'s files',
restoreFilesSelectAllFiles: "Select all this folder's files",
restoreFilesUnselectAll: 'Unselect all files',
// ----- Modals -----
emergencyShutdownHostsModalTitle: 'Emergency shutdown Host{nHosts, plural, one {} other {s}}',
emergencyShutdownHostsModalMessage: 'Are you sure you want to shutdown {nHosts, number} Host{nHosts, plural, one {} other {s}}?',
emergencyShutdownHostsModalTitle:
'Emergency shutdown Host{nHosts, plural, one {} other {s}}',
emergencyShutdownHostsModalMessage:
'Are you sure you want to shutdown {nHosts, number} Host{nHosts, plural, one {} other {s}}?',
stopHostModalTitle: 'Shutdown host',
stopHostModalMessage: 'This will shutdown your host. Do you want to continue? If it\'s the pool master, your connection to the pool will be lost',
stopHostModalMessage:
"This will shutdown your host. Do you want to continue? If it's the pool master, your connection to the pool will be lost",
addHostModalTitle: 'Add host',
addHostModalMessage: 'Are you sure you want to add {host} to {pool}?',
restartHostModalTitle: 'Restart host',
restartHostModalMessage: 'This will restart your host. Do you want to continue?',
restartHostsAgentsModalTitle: 'Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}',
restartHostsAgentsModalMessage: 'Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?',
restartHostModalMessage:
'This will restart your host. Do you want to continue?',
restartHostsAgentsModalTitle:
'Restart Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}',
restartHostsAgentsModalMessage:
'Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}} agent{nHosts, plural, one {} other {s}}?',
restartHostsModalTitle: 'Restart Host{nHosts, plural, one {} other {s}}',
restartHostsModalMessage: 'Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}}?',
restartHostsModalMessage:
'Are you sure you want to restart {nHosts, number} Host{nHosts, plural, one {} other {s}}?',
startVmsModalTitle: 'Start VM{vms, plural, one {} other {s}}',
cloneAndStartVM: 'Start a copy',
forceStartVm: 'Force start',
forceStartVmModalTitle: 'Forbidden operation',
blockedStartVmModalMessage: 'Start operation for this vm is blocked.',
blockedStartVmsModalMessage: 'Forbidden operation start for {nVms, number} vm{nVms, plural, one {} other {s}}.',
startVmsModalMessage: 'Are you sure you want to start {vms, number} VM{vms, plural, one {} other {s}}?',
failedVmsErrorMessage: '{nVms, number} vm{nVms, plural, one {} other {s}} are failed. Please see your logs to get more information',
blockedStartVmsModalMessage:
'Forbidden operation start for {nVms, number} vm{nVms, plural, one {} other {s}}.',
startVmsModalMessage:
'Are you sure you want to start {vms, number} VM{vms, plural, one {} other {s}}?',
failedVmsErrorMessage:
'{nVms, number} vm{nVms, plural, one {} other {s}} are failed. Please see your logs to get more information',
failedVmsErrorTitle: 'Start failed',
stopHostsModalTitle: 'Stop Host{nHosts, plural, one {} other {s}}',
stopHostsModalMessage: 'Are you sure you want to stop {nHosts, number} Host{nHosts, plural, one {} other {s}}?',
stopHostsModalMessage:
'Are you sure you want to stop {nHosts, number} Host{nHosts, plural, one {} other {s}}?',
stopVmsModalTitle: 'Stop VM{vms, plural, one {} other {s}}',
stopVmsModalMessage: 'Are you sure you want to stop {vms, number} VM{vms, plural, one {} other {s}}?',
stopVmsModalMessage:
'Are you sure you want to stop {vms, number} VM{vms, plural, one {} other {s}}?',
restartVmModalTitle: 'Restart VM',
restartVmModalMessage: 'Are you sure you want to restart {name}?',
stopVmModalTitle: 'Stop VM',
stopVmModalMessage: 'Are you sure you want to stop {name}?',
restartVmsModalTitle: 'Restart VM{vms, plural, one {} other {s}}',
restartVmsModalMessage: 'Are you sure you want to restart {vms, number} VM{vms, plural, one {} other {s}}?',
restartVmsModalMessage:
'Are you sure you want to restart {vms, number} VM{vms, plural, one {} other {s}}?',
snapshotVmsModalTitle: 'Snapshot VM{vms, plural, one {} other {s}}',
snapshotVmsModalMessage: 'Are you sure you want to snapshot {vms, number} VM{vms, plural, one {} other {s}}?',
snapshotVmsModalMessage:
'Are you sure you want to snapshot {vms, number} VM{vms, plural, one {} other {s}}?',
deleteVmsModalTitle: 'Delete VM{vms, plural, one {} other {s}}',
deleteVmsModalMessage: 'Are you sure you want to delete {vms, number} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
deleteVmsModalMessage:
'Are you sure you want to delete {vms, number} VM{vms, plural, one {} other {s}}? ALL VM DISKS WILL BE REMOVED',
deleteVmModalTitle: 'Delete VM',
deleteVmModalMessage: 'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
deleteVmModalMessage:
'Are you sure you want to delete this VM? ALL VM DISKS WILL BE REMOVED',
migrateVmModalTitle: 'Migrate VM',
migrateVmSelectHost: 'Select a destination host:',
migrateVmSelectMigrationNetwork: 'Select a migration network:',
@@ -1099,27 +1181,37 @@ var messages = {
chooseSrForEachVdisModalSrLabel: 'SR*',
chooseSrForEachVdisModalOptionalEntry: '* optional',
deleteVdiModalTitle: 'Delete VDI',
deleteVdiModalMessage: 'Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST',
deleteVdiModalMessage:
'Are you sure you want to delete this disk? ALL DATA ON THIS DISK WILL BE LOST',
deleteVdisModalTitle: 'Delete VDI{nVdis, plural, one {} other {s}}',
deleteVdisModalMessage:
'Are you sure you want to delete {nVdis, number} disk{nVdis, plural, one {} other {s}}? ALL DATA ON THESE DISKS WILL BE LOST',
revertVmModalTitle: 'Revert your VM',
deleteSnapshotModalTitle: 'Delete snapshot',
deleteSnapshotModalMessage: 'Are you sure you want to delete this snapshot?',
revertVmModalMessage: 'Are you sure you want to revert this VM to the snapshot state? This operation is irreversible.',
revertVmModalMessage:
'Are you sure you want to revert this VM to the snapshot state? This operation is irreversible.',
revertVmModalSnapshotBefore: 'Snapshot before',
importBackupModalTitle: 'Import a {name} Backup',
importBackupModalStart: 'Start VM after restore',
importBackupModalSelectBackup: 'Select your backup…',
removeAllOrphanedModalWarning: 'Are you sure you want to remove all orphaned snapshot VDIs?',
removeAllOrphanedModalWarning:
'Are you sure you want to remove all orphaned snapshot VDIs?',
removeAllLogsModalTitle: 'Remove all logs',
removeAllLogsModalWarning: 'Are you sure you want to remove all logs?',
definitiveMessageModal: 'This operation is definitive.',
existingSrModalTitle: 'Previous SR Usage',
existingSrModalText: 'This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation.',
existingSrModalText:
'This path has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation.',
existingLunModalTitle: 'Previous LUN Usage',
existingLunModalText: 'This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation.',
existingLunModalText:
'This LUN has been previously used as a Storage by a XenServer host. All data will be lost if you choose to continue the SR creation.',
alreadyRegisteredModal: 'Replace current registration?',
alreadyRegisteredModalText: 'Your XO appliance is already registered to {email}, do you want to forget and replace this registration ?',
alreadyRegisteredModalText:
'Your XO appliance is already registered to {email}, do you want to forget and replace this registration ?',
trialReadyModal: 'Ready for trial?',
trialReadyModalText: 'During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!',
trialReadyModalText:
'During the trial period, XOA need to have a working internet connection. This limitation does not apply for our paid plans!',
// ----- Servers -----
serverLabel: 'Label',
@@ -1130,7 +1222,8 @@ var messages = {
serverReadOnly: 'Read Only',
serverUnauthorizedCertificates: 'Unauthorized Certificates',
serverAllowUnauthorizedCertificates: 'Allow Unauthorized Certificates',
serverUnauthorizedCertificatesInfo: 'Enable it if your certificate is rejected, but it\'s not recommended because your connection will not be secured.',
serverUnauthorizedCertificatesInfo:
"Enable it if your certificate is rejected, but it's not recommended because your connection will not be secured.",
serverDisconnect: 'Disconnect server',
serverPlaceHolderUser: 'username',
serverPlaceHolderPassword: 'password',
@@ -1141,13 +1234,14 @@ var messages = {
serverAddFailed: 'Adding server failed',
serverStatus: 'Status',
serverConnectionFailed: 'Connection failed. Click for more information.',
serverConnecting: 'Connecting...',
serverConnecting: 'Connecting',
serverConnected: 'Connected',
serverDisconnected: 'Disconnected',
serverAuthFailed: 'Authentication error',
serverUnknownError: 'Unknown error',
serverSelfSignedCertError: 'Invalid self-signed certificate',
serverSelfSignedCertQuestion: 'Do you want to accept self-signed certificate for this server even though it would decrease security?',
serverSelfSignedCertQuestion:
'Do you want to accept self-signed certificate for this server even though it would decrease security?',
// ----- Copy VM -----
copyVm: 'Copy VM',
@@ -1163,14 +1257,22 @@ var messages = {
// ----- Detach host -----
detachHostModalTitle: 'Detach host',
detachHostModalMessage: 'Are you sure you want to detach {host} from its pool? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND REBOOT THE HOST.',
detachHostModalMessage:
'Are you sure you want to detach {host} from its pool? THIS WILL REMOVE ALL VMs ON ITS LOCAL STORAGE AND REBOOT THE HOST.',
detachHost: 'Detach',
// ----- Forget host -----
forgetHostModalTitle: 'Forget host',
forgetHostModalMessage: 'Are you sure you want to forget {host} from its pool? Be sure this host can\'t be back online, or use detach instead.',
forgetHostModalMessage:
"Are you sure you want to forget {host} from its pool? Be sure this host can't be back online, or use detach instead.",
forgetHost: 'Forget',
// ----- Set pool master -----
setPoolMasterModalTitle: 'Designate a new master',
setPoolMasterModalMessage:
'This operation may take several minutes. Do you want to continue?',
// ----- Network -----
newNetworkCreate: 'Create network',
newBondedNetworkCreate: 'Create bonded network',
@@ -1222,7 +1324,8 @@ var messages = {
or: 'Or',
tryIt: 'Try it for free!',
availableIn: 'This feature is available starting from {plan} Edition',
notAvailable: 'This feature is not available in your version, contact your administrator to know more.',
notAvailable:
'This feature is not available in your version, contact your administrator to know more.',
// ----- Updates View -----
updateTitle: 'Updates',
@@ -1236,34 +1339,45 @@ var messages = {
proxySettingsPasswordPlaceHolder: 'Password',
updateRegistrationEmailPlaceHolder: 'Your email account',
updateRegistrationPasswordPlaceHolder: 'Your password',
updaterTroubleshootingLink: 'Troubleshooting documentation',
update: 'Update',
refresh: 'Refresh',
upgrade: 'Upgrade',
noUpdaterCommunity: 'No updater available for Community Edition',
considerSubscribe: 'Please consider subscribe and try it with all features for free during 15 days on {link}.',
noUpdaterWarning: 'Manual update could break your current installation due to dependencies issues, do it with caution',
considerSubscribe:
'Please consider subscribing and trying it with all the features for free during 15 days on {link}.',
noUpdaterWarning:
'Manual update could break your current installation due to dependencies issues, do it with caution',
currentVersion: 'Current version:',
register: 'Register',
editRegistration: 'Edit registration',
trialRegistration: 'Please, take time to register in order to enjoy your trial.',
trialRegistration:
'Please, take time to register in order to enjoy your trial.',
trialStartButton: 'Start trial',
trialAvailableUntil: 'You can use a trial version until {date, date, medium}. Upgrade your appliance to get it.',
trialConsumed: 'Your trial has been ended. Contact us or downgrade to Free version',
trialLocked: 'Your xoa-updater service appears to be down. Your XOA cannot run fully without reaching this service.',
trialAvailableUntil:
'You can use a trial version until {date, date, medium}. Upgrade your appliance to get it.',
trialConsumed:
'Your trial has been ended. Contact us or downgrade to Free version',
trialLocked:
'Your xoa-updater service appears to be down. Your XOA cannot run fully without reaching this service.',
noUpdateInfo: 'No update information available',
waitingUpdateInfo: 'Update information may be available',
upToDate: 'Your XOA is up-to-date',
mustUpgrade: 'You need to update your XOA (new version is available)',
registerNeeded: 'Your XOA is not registered for updates',
updaterError: 'Can\'t fetch update information',
updaterError: "Can't fetch update information",
promptUpgradeReloadTitle: 'Upgrade successful',
promptUpgradeReloadMessage: 'Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?',
promptUpgradeReloadMessage:
'Your XOA has successfully upgraded, and your browser must reload the application. Do you want to reload now ?',
// ----- OS Disclaimer -----
disclaimerTitle: 'Xen Orchestra from the sources',
disclaimerText1: 'You are using XO from the sources! That\'s great for a personal/non-profit usage.',
disclaimerText2: 'If you are a company, it\'s better to use it with our appliance + pro support included:',
disclaimerText3: 'This version is not bundled with any support nor updates. Use it with caution for critical tasks.',
disclaimerText1:
"You are using XO from the sources! That's great for a personal/non-profit usage.",
disclaimerText2:
"If you are a company, it's better to use it with our appliance + pro support included:",
disclaimerText3:
'This version is not bundled with any support nor updates. Use it with caution for critical tasks.',
// ----- PIF -----
connectPif: 'Connect PIF',
@@ -1285,11 +1399,13 @@ var messages = {
newPasswordPlaceholder: 'New password',
confirmPasswordPlaceholder: 'Confirm new password',
confirmationPasswordError: 'Confirmation password incorrect',
confirmationPasswordErrorBody: 'Password does not match the confirm password.',
confirmationPasswordErrorBody:
'Password does not match the confirm password.',
pwdChangeSuccess: 'Password changed',
pwdChangeSuccessBody: 'Your password has been successfully changed.',
pwdChangeError: 'Incorrect password',
pwdChangeErrorBody: 'The old password provided is incorrect. Your password has not been changed.',
pwdChangeErrorBody:
'The old password provided is incorrect. Your password has not been changed.',
changePasswordOk: 'OK',
sshKeys: 'SSH keys',
newSshKey: 'New SSH key',
@@ -1301,7 +1417,8 @@ var messages = {
title: 'Title',
key: 'Key',
deleteSshKeyConfirm: 'Delete SSH key',
deleteSshKeyConfirmMessage: 'Are you sure you want to delete the SSH key {title}?',
deleteSshKeyConfirmMessage:
'Are you sure you want to delete the SSH key {title}?',
// ----- Usage -----
others: 'Others',
@@ -1342,19 +1459,25 @@ var messages = {
// ----- Shortcuts -----
shortcutModalTitle: 'Keyboard shortcuts',
shortcut_XoApp: 'Global',
shortcut_GO_TO_HOSTS: 'Go to hosts list',
shortcut_GO_TO_POOLS: 'Go to pools list',
shortcut_GO_TO_VMS: 'Go to VMs list',
shortcut_GO_TO_SRS: 'Go to SRs list',
shortcut_CREATE_VM: 'Create a new VM',
shortcut_UNFOCUS: 'Unfocus field',
shortcut_HELP: 'Show shortcuts key bindings',
shortcut_XoApp_GO_TO_HOSTS: 'Go to hosts list',
shortcut_XoApp_GO_TO_POOLS: 'Go to pools list',
shortcut_XoApp_GO_TO_VMS: 'Go to VMs list',
shortcut_XoApp_GO_TO_SRS: 'Go to SRs list',
shortcut_XoApp_CREATE_VM: 'Create a new VM',
shortcut_XoApp_UNFOCUS: 'Unfocus field',
shortcut_XoApp_HELP: 'Show shortcuts key bindings',
shortcut_Home: 'Home',
shortcut_SEARCH: 'Focus search bar',
shortcut_NAV_DOWN: 'Next item',
shortcut_NAV_UP: 'Previous item',
shortcut_SELECT: 'Select item',
shortcut_JUMP_INTO: 'Open',
shortcut_Home_SEARCH: 'Focus search bar',
shortcut_Home_NAV_DOWN: 'Next item',
shortcut_Home_NAV_UP: 'Previous item',
shortcut_Home_SELECT: 'Select item',
shortcut_Home_JUMP_INTO: 'Open',
shortcut_SortedTable: 'Supported tables',
shortcut_SortedTable_SEARCH: 'Focus the table search bar',
shortcut_SortedTable_NAV_DOWN: 'Next item',
shortcut_SortedTable_NAV_UP: 'Previous item',
shortcut_SortedTable_SELECT: 'Select item',
shortcut_SortedTable_ROW_ACTION: 'Action',
// ----- Settings/ACLs -----
settingsAclsButtonTooltipVM: 'VM',
@@ -1365,7 +1488,8 @@ var messages = {
// ----- Config -----
noConfigFile: 'No config file selected',
importTip: 'Try dropping a config file here, or click to select a config file to upload.',
importTip:
'Try dropping a config file here, or click to select a config file to upload.',
config: 'Config',
importConfig: 'Import',
importConfigSuccess: 'Config file successfully imported',
@@ -1377,14 +1501,19 @@ var messages = {
// ----- SR -----
srReconnectAllModalTitle: 'Reconnect all hosts',
srReconnectAllModalMessage: 'This will reconnect this SR to all its hosts.',
srsReconnectAllModalMessage: 'This will reconnect each selected SR to its host (local SR) or to every hosts of its pool (shared SR).',
srsReconnectAllModalMessage:
'This will reconnect each selected SR to its host (local SR) or to every hosts of its pool (shared SR).',
srDisconnectAllModalTitle: 'Disconnect all hosts',
srDisconnectAllModalMessage: 'This will disconnect this SR from all its hosts.',
srsDisconnectAllModalMessage: 'This will disconnect each selected SR from its host (local SR) or from every hosts of its pool (shared SR).',
srDisconnectAllModalMessage:
'This will disconnect this SR from all its hosts.',
srsDisconnectAllModalMessage:
'This will disconnect each selected SR from its host (local SR) or from every hosts of its pool (shared SR).',
srForgetModalTitle: 'Forget SR',
srsForgetModalTitle: 'Forget selected SRs',
srForgetModalMessage: 'Are you sure you want to forget this SR? VDIs on this storage won\'t be removed.',
srsForgetModalMessage: 'Are you sure you want to forget all the selected SRs? VDIs on these storages won\'t be removed.',
srForgetModalMessage:
"Are you sure you want to forget this SR? VDIs on this storage won't be removed.",
srsForgetModalMessage:
"Are you sure you want to forget all the selected SRs? VDIs on these storages won't be removed.",
srAllDisconnected: 'Disconnected',
srSomeConnected: 'Partially connected',
srAllConnected: 'Connected',
@@ -1396,13 +1525,15 @@ var messages = {
xosanSuggestions: 'Suggestions',
xosanName: 'Name',
xosanHost: 'Host',
xosanHosts: 'Hosts',
xosanHosts: 'Connected Hosts',
xosanPool: 'Pool',
xosanVolumeId: 'Volume ID',
xosanSize: 'Size',
xosanUsedSpace: 'Used space',
xosanNeedPack: 'XOSAN pack needs to be installed on each host of the pool.',
xosanInstallIt: 'Install it now!',
xosanNeedRestart: 'Some hosts need their toolstack to be restarted before you can create an XOSAN',
xosanNeedRestart:
'Some hosts need their toolstack to be restarted before you can create an XOSAN',
xosanRestartAgents: 'Restart toolstacks',
xosanMasterOffline: 'Pool master is not running',
xosanInstallPackTitle: 'Install XOSAN pack on {pool}',
@@ -1413,26 +1544,90 @@ var messages = {
xosanAvailableSpace: 'Available space',
xosanDiskLossLegend: '* Can fail without data loss',
xosanCreate: 'Create',
xosanInstalling: 'Installing XOSAN. Please wait...',
xosanAdd: 'Add',
xosanInstalling: 'Installing XOSAN. Please wait…',
xosanCommunity: 'No XOSAN available for Community Edition',
xosanNew: 'New',
xosanAdvanced: 'Advanced',
xosanRemoveSubvolumes: 'Remove subvolumes',
xosanAddSubvolume: 'Add subvolume…',
xosanWarning:
"This version of XOSAN SR is from the first beta phase. You can keep using it, but to modify it you'll have to save your disks and re-create it.",
xosanVlan: 'VLAN',
xosanNoSrs: 'No XOSAN found',
xosanPbdsDetached: 'Some SRs are detached from the XOSAN',
xosanBadStatus: 'Something is wrong with: {badStatuses}',
xosanRunning: 'Running',
xosanDelete: 'Delete XOSAN',
xosanFixIssue: 'Fix',
xosanCreatingOn: 'Creating XOSAN on {pool}',
xosanState_configuringNetwork: 'Configuring network…',
xosanState_importingVm: 'Importing VM…',
xosanState_copyingVms: 'Copying VMs…',
xosanState_configuringVms: 'Configuring VMs…',
xosanState_configuringGluster: 'Configuring gluster…',
xosanState_creatingSr: 'Creating SR…',
xosanState_scanningSr: 'Scanning SR…',
// Pack download modal
xosanInstallCloudPlugin: 'Install cloud plugin first',
xosanLoadCloudPlugin: 'Load cloud plugin first',
xosanLoading: 'Loading...',
xosanLoading: 'Loading',
xosanNotAvailable: 'XOSAN is not available at the moment',
xosanRegisterBeta: 'Register for the XOSAN beta',
xosanSuccessfullyRegistered: 'You have successfully registered for the XOSAN beta. Please wait until your request has been approved.',
xosanSuccessfullyRegistered:
'You have successfully registered for the XOSAN beta. Please wait until your request has been approved.',
xosanInstallPackOnHosts: 'Install XOSAN pack on these hosts:',
xosanInstallPack: 'Install {pack} v{version}?',
xosanNoPackFound: 'No compatible XOSAN pack found for your XenServer versions.',
xosanPackRequirements: 'At least one of these version requirements must be satisfied by all the hosts in this pool:'
xosanNoPackFound:
'No compatible XOSAN pack found for your XenServer versions.',
xosanPackRequirements:
'At least one of these version requirements must be satisfied by all the hosts in this pool:',
// SR tab XOSAN
xosanVmsNotRunning: 'Some XOSAN Virtual Machines are not running',
xosanVmsNotFound: 'Some XOSAN Virtual Machines could not be found',
xosanFilesNeedingHealing: 'Files needing healing',
xosanFilesNeedHealing:
'Some XOSAN Virtual Machines have files needing healing',
xosanHostNotInNetwork: 'Host {hostName} is not in XOSAN network',
xosanVm: 'VM controller',
xosanUnderlyingStorage: 'SR',
xosanReplace: 'Replace…',
xosanOnSameVm: 'On same VM',
xosanBrickName: 'Brick name',
xosanBrickUuid: 'Brick UUID',
xosanBrickSize: 'Brick size',
xosanMemorySize: 'Memory size',
xosanStatus: 'Status',
xosanArbiter: 'Arbiter',
xosanUsedInodes: 'Used Inodes',
xosanBlockSize: 'Block size',
xosanDevice: 'Device',
xosanFsName: 'FS name',
xosanMountOptions: 'Mount options',
xosanPath: 'Path',
xosanJob: 'Job',
xosanPid: 'PID',
xosanPort: 'Port',
xosanReplaceBrickErrorTitle: 'Missing values',
xosanReplaceBrickErrorMessage: 'You need to select a SR and a size',
xosanAddSubvolumeErrorTitle: 'Bad values',
xosanAddSubvolumeErrorMessage: 'You need to select {nSrs, number} and a size',
xosanSelectNSrs: 'Select {nSrs, number} SRs',
xosanRun: 'Run',
xosanRemove: 'Remove',
xosanVolume: 'Volume',
xosanVolumeOptions: 'Volume options',
xosanCouldNotFindVM: 'Could not find VM',
xosanUnderlyingStorageUsage: 'Using {usage}',
xosanCustomIpNetwork: 'Custom IP network (/24)',
xosanIssueHostNotInNetwork:
'Will configure the host xosan network device with a static IP address and plug it in.',
}
forEach(messages, function (message, id) {
if (isString(message)) {
messages[id] = {
id,
defaultMessage: message
defaultMessage: message,
}
} else if (!message.id) {
message.id = id

View File

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

View File

@@ -13,36 +13,29 @@ 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 {
@@ -77,7 +70,7 @@ export default class IsoDevice extends Component {
_showWarning = () => alert(_('cdDriveNotInstalled'), _('cdDriveInstallation'))
render () {
const {cdDrive, mountedIso} = this.props
const { cdDrive, mountedIso } = this.props
return (
<div className='input-group'>
@@ -93,19 +86,17 @@ export default class IsoDevice extends Component {
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>
}
{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

@@ -9,10 +9,7 @@ import propTypes from '../prop-types-decorator'
import { EMPTY_ARRAY } from '../utils'
import GenericInput from './generic-input'
import {
descriptionRender,
forceDisplayOptionalAttr
} from './helpers'
import { descriptionRender, forceDisplayOptionalAttr } from './helpers'
@propTypes({
depth: propTypes.number,
@@ -20,12 +17,12 @@ import {
label: propTypes.any.isRequired,
required: propTypes.bool,
schema: propTypes.object.isRequired,
uiSchema: propTypes.object
uiSchema: propTypes.object,
})
@uncontrollableInput()
export default class ObjectInput extends Component {
state = {
use: this.props.required || forceDisplayOptionalAttr(this.props)
use: this.props.required || forceDisplayOptionalAttr(this.props),
}
_onAddItem = () => {
@@ -56,9 +53,9 @@ export default class ObjectInput extends Component {
required,
schema,
uiSchema,
value = EMPTY_ARRAY
value = EMPTY_ARRAY,
},
state: { use }
state: { use },
} = this
const childDepth = depth + 2
@@ -68,56 +65,61 @@ export default class ObjectInput extends Component {
const itemLabel = itemSchema.title || _('item')
return (
<div style={{'paddingLeft': `${depth}em`}}>
<div style={{ paddingLeft: `${depth}em` }}>
<legend>{label}</legend>
{descriptionRender(schema.description)}
<hr />
{!required && <div className='checkbox'>
<label>
<input
checked={use}
{!required && (
<div className='checkbox'>
<label>
<input
checked={use}
disabled={disabled}
onChange={this.linkState('use')}
type='checkbox'
/>{' '}
{_('fillOptionalInformations')}
</label>
</div>
)}
{use && (
<div className='card-block'>
<ul style={{ paddingLeft: 0 }}>
{map(value, (value, key) => (
<li className='list-group-item clearfix' key={key}>
<GenericInput
depth={childDepth}
disabled={disabled}
label={itemLabel}
name={key}
onChange={this._onChangeItem}
required
schema={itemSchema}
uiSchema={itemUiSchema}
value={value}
/>
<Button
btnStyle='danger'
className='pull-right'
disabled={disabled}
name={key}
onClick={() => this._onRemoveItem(key)}
>
{_('remove')}
</Button>
</li>
))}
</ul>
<Button
btnStyle='primary'
className='pull-right mt-1 mr-1'
disabled={disabled}
onChange={this.linkState('use')}
type='checkbox'
/> {_('fillOptionalInformations')}
</label>
</div>}
{use && <div className='card-block'>
<ul style={{'paddingLeft': 0}} >
{map(value, (value, key) =>
<li className='list-group-item clearfix' key={key}>
<GenericInput
depth={childDepth}
disabled={disabled}
label={itemLabel}
name={key}
onChange={this._onChangeItem}
required
schema={itemSchema}
uiSchema={itemUiSchema}
value={value}
/>
<Button
btnStyle='danger'
className='pull-right'
disabled={disabled}
name={key}
onClick={() => this._onRemoveItem(key)}
>
{_('remove')}
</Button>
</li>
)}
</ul>
<Button
btnStyle='primary'
className='pull-right mt-1 mr-1'
disabled={disabled}
onClick={this._onAddItem}
>
{_('add')}
</Button>
</div>}
onClick={this._onAddItem}
>
{_('add')}
</Button>
</div>
)}
</div>
)
}

View File

@@ -11,21 +11,12 @@ import { PrimitiveInputWrapper } from './helpers'
@uncontrollableInput()
export default class BooleanInput extends Component {
render () {
const {
disabled,
onChange,
value,
...props
} = this.props
const { disabled, onChange, value, ...props } = this.props
return (
<PrimitiveInputWrapper {...props}>
<div className='checkbox form-control'>
<Toggle
disabled={disabled}
onChange={onChange}
value={value}
/>
<Toggle disabled={disabled} onChange={onChange} value={value} />
</div>
</PrimitiveInputWrapper>
)

View File

@@ -14,10 +14,7 @@ export default class EnumInput extends Component {
_getSelectedIndex = createSelector(
() => this.props.schema.enum,
() => {
const {
schema,
value = schema.default
} = this.props
const { schema, value = schema.default } = this.props
return value
},
(enumValues, value) => {
@@ -34,7 +31,7 @@ export default class EnumInput extends Component {
const {
disabled,
schema: { enum: enumValues, enumNames = enumValues },
required
required,
} = this.props
return (
@@ -47,9 +44,11 @@ export default class EnumInput extends Component {
value={this._getSelectedIndex()}
>
{_('noSelectedValue', message => <option value=''>{message}</option>)}
{map(enumNames, (name, index) =>
<option value={index} key={index}>{name}</option>
)}
{map(enumNames, (name, index) => (
<option value={index} key={index}>
{name}
</option>
))}
</select>
</PrimitiveInputWrapper>
)

View File

@@ -23,7 +23,7 @@ const InputByType = {
integer: IntegerInput,
number: NumberInput,
object: ObjectInput,
string: StringInput
string: StringInput,
}
// ===================================================================
@@ -34,7 +34,7 @@ const InputByType = {
label: propTypes.any.isRequired,
required: propTypes.bool,
schema: propTypes.object.isRequired,
uiSchema: propTypes.object
uiSchema: propTypes.object,
})
@uncontrollableInput()
export default class GenericInput extends Component {
@@ -56,7 +56,7 @@ export default class GenericInput extends Component {
onChange: this._onChange,
schema,
uiSchema,
value
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,9 +63,7 @@ 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>
)

View File

@@ -9,10 +9,7 @@ import propTypes from '../prop-types-decorator'
import { EMPTY_OBJECT } from '../utils'
import GenericInput from './generic-input'
import {
descriptionRender,
forceDisplayOptionalAttr
} from './helpers'
import { descriptionRender, forceDisplayOptionalAttr } from './helpers'
@propTypes({
depth: propTypes.number,
@@ -20,26 +17,24 @@ import {
label: propTypes.any.isRequired,
required: propTypes.bool,
schema: propTypes.object.isRequired,
uiSchema: propTypes.object
uiSchema: propTypes.object,
})
@uncontrollableInput()
export default class ObjectInput extends Component {
state = {
use: this.props.required || forceDisplayOptionalAttr(this.props)
use: this.props.required || forceDisplayOptionalAttr(this.props),
}
_onChildChange = (value, key) => {
this.props.onChange({
...this.props.value,
[key]: value
[key]: value,
})
}
_getRequiredProps = createSelector(
() => this.props.schema.required,
required => required
? keyBy(required)
: EMPTY_OBJECT
required => (required ? keyBy(required) : EMPTY_OBJECT)
)
render () {
@@ -51,9 +46,9 @@ export default class ObjectInput extends Component {
required,
schema,
uiSchema,
value = EMPTY_OBJECT
value = EMPTY_OBJECT,
},
state: { use }
state: { use },
} = this
const childDepth = depth + 2
@@ -61,37 +56,42 @@ export default class ObjectInput extends Component {
const requiredProps = this._getRequiredProps()
return (
<div style={{'paddingLeft': `${depth}em`}}>
<div style={{ paddingLeft: `${depth}em` }}>
<legend>{label}</legend>
{descriptionRender(schema.description)}
<hr />
{!required && <div className='checkbox'>
<label>
<input
checked={use}
disabled={disabled}
onChange={this.linkState('use')}
type='checkbox'
/> {_('fillOptionalInformations')}
</label>
</div>}
{use && <div className='card-block'>
{map(schema.properties, (childSchema, key) =>
<div className='pb-1' key={key}>
<GenericInput
depth={childDepth}
{!required && (
<div className='checkbox'>
<label>
<input
checked={use}
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>}
onChange={this.linkState('use')}
type='checkbox'
/>{' '}
{_('fillOptionalInformations')}
</label>
</div>
)}
{use && (
<div className='card-block'>
{map(schema.properties, (childSchema, key) => (
<div className='pb-1' key={key}>
<GenericInput
depth={childDepth}
disabled={disabled}
label={childSchema.title || key}
name={key}
onChange={this._onChildChange}
required={Boolean(requiredProps[key])}
schema={childSchema}
uiSchema={properties[key]}
value={value[key]}
/>
</div>
))}
</div>
)}
</div>
)
}

View File

@@ -11,7 +11,7 @@ import { PrimitiveInputWrapper } from './helpers'
// ===================================================================
@propTypes({
password: propTypes.bool
password: propTypes.bool,
})
@uncontrollableInput()
export default class StringInput extends Component {

View File

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

@@ -11,7 +11,7 @@ import propTypes from './prop-types-decorator'
import Tooltip from './tooltip'
import {
disable as disableShortcuts,
enable as enableShortcuts
enable as enableShortcuts,
} from './shortcuts'
let instance
@@ -26,16 +26,18 @@ const modal = (content, onClose) => {
}
@propTypes({
buttons: propTypes.arrayOf(propTypes.shape({
btnStyle: propTypes.string,
icon: propTypes.string,
label: propTypes.string.isRequired,
tooltip: propTypes.node,
value: propTypes.any
})).isRequired,
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
title: propTypes.node.isRequired,
})
class GenericModal extends Component {
_getBodyValue = () => {
@@ -58,78 +60,62 @@ class GenericModal extends Component {
}
render () {
const { Body, Footer, Header, Title } = ReactModal
const {
buttons,
icon,
title
} = this.props
const { buttons, icon, title } = 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>
{map(buttons, ({
label,
tooltip,
value,
icon,
...props
}) => {
const button = <Button
onClick={() => this._resolve(value)}
key={value}
{...props}
>
{icon !== undefined && <Icon icon={icon} fixedWidth />}
{label}
</Button>
return <span>
{tooltip !== undefined
? <Tooltip content={tooltip}>{button}</Tooltip>
: button
}
{' '}
</span>
})}
{this.props.reject !== undefined &&
<Button onClick={this._reject} >
{_('genericCancel')}
</Button>
}
</Footer>
</div>
return (
<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' } ]
const ALERT_BUTTONS = [{ label: _('alertOk'), value: 'ok' }]
export const alert = (title, body) => (
export const alert = (title, body) =>
new Promise(resolve => {
modal(
<GenericModal
buttons={ALERT_BUTTONS}
resolve={resolve}
title={title}
>
<GenericModal buttons={ALERT_BUTTONS} resolve={resolve} title={title}>
{body}
</GenericModal>,
resolve
)
})
)
const _addRef = (component, ref) => {
if (isString(component) || isArray(component)) {
@@ -142,27 +128,17 @@ const _addRef = (component, ref) => {
return component
}
const CONFIRM_BUTTONS = [ { btnStyle: 'primary', label: _('confirmOk') } ]
const CONFIRM_BUTTONS = [{ btnStyle: 'primary', label: _('confirmOk') }]
export const confirm = ({
body,
icon = 'alarm',
title
}) => (
export const confirm = ({ body, icon = 'alarm', title }) =>
chooseAction({
body,
buttons: CONFIRM_BUTTONS,
icon,
title
title,
})
)
export const chooseAction = ({
body,
buttons,
icon,
title
}) => {
export const chooseAction = ({ body, buttons, icon, title }) => {
return new Promise((resolve, reject) => {
modal(
<GenericModal
@@ -209,14 +185,8 @@ export default class Modal extends Component {
}
render () {
const { showModal } = this.state
/* TODO: remove this work-around and use
* ReactModal.Body, ReactModal.Header, ...
* after this issue has been fixed:
* https://phabricator.babeljs.io/T6976
*/
return (
<ReactModal show={showModal} onHide={this._onHide}>
<ReactModal show={this.state.showModal} onHide={this._onHide}>
{this.state.content}
</ReactModal>
)

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

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

View File

@@ -25,15 +25,19 @@ export class Notification extends Component {
}
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, body, 3e3)
info = (title, body) => notification.info(title, body, 3e3)
success = (title, body) => notification.success(title, body, 3e3)
}}
/>
)
}
}

View File

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

View File

@@ -16,13 +16,13 @@ const propTypes = (propTypes, contextTypes) => target => {
if (propTypes !== undefined) {
target.propTypes = {
...target.propTypes,
...propTypes
...propTypes,
}
}
if (contextTypes !== undefined) {
target.contextTypes = {
...target.contextTypes,
...contextTypes
...contextTypes,
}
}

View File

@@ -1,19 +1,20 @@
import React, { Component } from 'react'
import RFB from '@nraynaud/novnc/lib/rfb'
import { createBackoff } from 'jsonrpc-websocket-client'
import { parse as parseUrl, resolve as resolveUrl } from 'url'
import {
parse as parseUrl,
resolve as resolveUrl
} from 'url'
import { enable as enableShortcuts, disable as disableShortcuts } from 'shortcuts'
enable as enableShortcuts,
disable as disableShortcuts,
} from 'shortcuts'
import propTypes from './prop-types-decorator'
const parseRelativeUrl = url => parseUrl(resolveUrl(String(window.location), url))
const parseRelativeUrl = url =>
parseUrl(resolveUrl(String(window.location), url))
const PROTOCOL_ALIASES = {
'http:': 'ws:',
'https:': 'wss:'
'https:': 'wss:',
}
const fixProtocol = url => {
const protocol = PROTOCOL_ALIASES[url.protocol]
@@ -24,7 +25,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 +42,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
)
}
}
@@ -87,14 +91,16 @@ export default class NoVnc extends Component {
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,
onClipboard: onClipboardChange && ((_, text) => {
onClipboardChange(text)
}),
onUpdateState: this._onUpdateState
})
onClipboard:
onClipboardChange &&
((_, text) => {
onClipboardChange(text)
}),
onUpdateState: this._onUpdateState,
}))
// remove leading slashes from the path
//
@@ -104,9 +110,9 @@ export default class NoVnc extends Component {
// a port is required
//
// if not available from the URL, use the default ones
const { port = isSecure ? 443 : 80 } = url
const port = url.port || (isSecure ? 443 : 80)
rfb.connect(url.hostname, url, null, clippedPath)
rfb.connect(url.hostname, port, null, clippedPath)
disableShortcuts()
}
@@ -152,13 +158,15 @@ export default class NoVnc extends Component {
}
render () {
return <canvas
className='center-block'
height='480'
onMouseEnter={this._focus}
onMouseLeave={this._unfocus}
ref='canvas'
width='640'
/>
return (
<canvas
className='center-block'
height='480'
onMouseEnter={this._focus}
onMouseLeave={this._unfocus}
ref='canvas'
width='640'
/>
)
}
}

View File

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

View File

@@ -2,13 +2,7 @@ import classNames from 'classnames'
import later from 'later'
import React from 'react'
import { FormattedDate, FormattedTime } from 'react-intl'
import {
forEach,
includes,
isArray,
map,
sortedIndex
} from 'lodash'
import { forEach, includes, isArray, map, sortedIndex } from 'lodash'
import _ from './intl'
import Button from './button'
@@ -33,7 +27,7 @@ const PREVIEW_SLIDER_STYLE = { width: '400px' }
// ===================================================================
const UNITS = [ 'minute', 'hour', 'monthDay', 'month', 'weekDay' ]
const UNITS = ['minute', 'hour', 'monthDay', 'month', 'weekDay']
const MINUTES_RANGE = [2, 30]
const HOURS_RANGE = [2, 12]
@@ -43,12 +37,7 @@ 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 = []
@@ -66,20 +55,16 @@ const DAYS = (() => {
return days
})()
const WEEK_DAYS = [
[ 0, 1, 2 ],
[ 3, 4, 5 ],
[ 6 ]
]
const WEEK_DAYS = [[0, 1, 2], [3, 4, 5], [6]]
const HOURS = (() => {
const hours = []
for (let i = 0; i < 3; i++) {
for (let i = 0; i < 4; i++) {
hours[i] = []
for (let j = 0; j < 8; j++) {
hours[i].push(8 * i + j)
for (let j = 0; j < 6; j++) {
hours[i].push(6 * i + j)
}
}
@@ -105,7 +90,7 @@ const PICKTIME_TO_ID = {
hour: 1,
monthDay: 2,
month: 3,
weekDay: 4
weekDay: 4,
}
const TIME_FORMAT = {
@@ -122,24 +107,30 @@ const TIME_FORMAT = {
// Therefore we can use UTC everywhere and say to the user that the
// previews are in the configured timezone.
timeZone: 'UTC'
timeZone: 'UTC',
}
// ===================================================================
// monthNum: [ 0 : 11 ]
const getMonthName = (monthNum) =>
const getMonthName = monthNum => (
<FormattedDate value={Date.UTC(1970, monthNum)} month='long' timeZone='UTC' />
)
// dayNum: [ 0 : 6 ]
const getDayName = (dayNum) =>
const getDayName = dayNum => (
// January, 1970, 5th => Monday
<FormattedDate value={Date.UTC(1970, 0, 4 + dayNum)} weekday='long' timeZone='UTC' />
<FormattedDate
value={Date.UTC(1970, 0, 4 + dayNum)}
weekday='long'
timeZone='UTC'
/>
)
// ===================================================================
@propTypes({
cronPattern: propTypes.string.isRequired
cronPattern: propTypes.string.isRequired,
})
export class SchedulePreview extends Component {
render () {
@@ -147,6 +138,13 @@ export class SchedulePreview extends Component {
const { value } = this.state
const cronSched = later.parse.cron(cronPattern)
// 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 (
@@ -155,7 +153,12 @@ export class SchedulePreview extends Component {
{_('cronPattern')} <strong>{cronPattern}</strong>
</div>
<div className='mb-1' style={PREVIEW_SLIDER_STYLE}>
<Range min={MIN_PREVIEWS} max={MAX_PREVIEWS} onChange={this.linkState('value')} value={+value} />
<Range
min={MIN_PREVIEWS}
max={MAX_PREVIEWS}
onChange={this.linkState('value')}
value={+value}
/>
</div>
<ul className='list-group'>
{map(dates, (date, id) => (
@@ -176,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 = () => {
@@ -187,7 +190,11 @@ class ToggleTd extends Component {
render () {
const { props } = this
return (
<td style={CLICKABLE} 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>
)
@@ -201,11 +208,11 @@ class ToggleTd extends Component {
options: propTypes.array.isRequired,
optionRenderer: propTypes.func,
onChange: propTypes.func.isRequired,
value: propTypes.array.isRequired
value: propTypes.array.isRequired,
})
class TableSelect extends Component {
static defaultProps = {
optionRenderer: value => value
optionRenderer: value => value,
}
_reset = () => {
@@ -234,38 +241,33 @@ class TableSelect extends Component {
}
render () {
const {
labelId,
options,
optionRenderer,
value
} = this.props
const { labelId, options, optionRenderer, value } = this.props
return <div>
<table className='table table-bordered table-sm'>
<tbody>
{map(options, (line, i) => (
<tr key={i}>
{map(line, tdOption => (
<ToggleTd
children={optionRenderer(tdOption)}
tdId={tdOption}
key={tdOption}
onChange={this._handleChange}
value={includes(value, tdOption)}
/>
))}
</tr>
))}
</tbody>
</table>
<Button
className='pull-right'
onClick={this._reset}
>
{_(`selectTableAll${labelId}`)} {value && !value.length && <Icon icon='success' />}
</Button>
</div>
return (
<div>
<table className='table table-bordered table-sm'>
<tbody>
{map(options, (line, i) => (
<tr key={i}>
{map(line, tdOption => (
<ToggleTd
children={optionRenderer(tdOption)}
tdId={tdOption}
key={tdOption}
onChange={this._handleChange}
value={includes(value, tdOption)}
/>
))}
</tr>
))}
</tbody>
</table>
<Button className='pull-right' onClick={this._reset}>
{_(`selectTableAll${labelId}`)}{' '}
{value && !value.length && <Icon icon='success' />}
</Button>
</div>
)
}
}
@@ -303,7 +305,7 @@ const valueToCron = value => {
onChange: propTypes.func.isRequired,
range: propTypes.array,
labelId: propTypes.string.isRequired,
value: propTypes.any.isRequired
value: propTypes.any.isRequired,
})
class TimePicker extends Component {
_update = cron => {
@@ -315,7 +317,7 @@ class TimePicker extends Component {
this.setState({
periodic,
tableValue: periodic ? tableValue : newValue,
rangeValue: periodic ? newValue : rangeValue
rangeValue: periodic ? newValue : rangeValue,
})
}
@@ -334,53 +336,63 @@ class TimePicker extends Component {
}
_tableTab = () => this._onChange(this.state.tableValue || [])
_periodicTab = () => this._onChange(this.state.rangeValue || this.props.range[0])
_periodicTab = () =>
this._onChange(this.state.rangeValue || this.props.range[0])
render () {
const {
headerAddon,
labelId,
options,
optionRenderer,
range
} = this.props
const { headerAddon, labelId, options, optionRenderer, range } = this.props
const {
periodic,
tableValue,
rangeValue
} = this.state
const { periodic, tableValue, rangeValue } = this.state
return <Card>
<CardHeader>
{_(`scheduling${labelId}`)}
{headerAddon}
</CardHeader>
<CardBlock>
{range && <ul className='nav nav-tabs mb-1'>
<li className='nav-item'>
<a onClick={this._tableTab} className={classNames('nav-link', !periodic && 'active')} style={CLICKABLE}>
{_(`schedulingEachSelected${labelId}`)}
</a>
</li>
<li className='nav-item'>
<a onClick={this._periodicTab} className={classNames('nav-link', periodic && 'active')} style={CLICKABLE}>
{_(`schedulingEveryN${labelId}`)}
</a>
</li>
</ul>}
{periodic
? <Range ref='range' min={range[0]} max={range[1]} onChange={this._onChange} value={rangeValue} />
: <TableSelect
labelId={labelId}
onChange={this._onChange}
options={options}
optionRenderer={optionRenderer}
value={tableValue || []}
/>
}
</CardBlock>
</Card>
return (
<Card>
<CardHeader>
{_(`scheduling${labelId}`)}
{headerAddon}
</CardHeader>
<CardBlock>
{range && (
<ul className='nav nav-tabs mb-1'>
<li className='nav-item'>
<a
onClick={this._tableTab}
className={classNames('nav-link', !periodic && 'active')}
style={CLICKABLE}
>
{_(`schedulingEachSelected${labelId}`)}
</a>
</li>
<li className='nav-item'>
<a
onClick={this._periodicTab}
className={classNames('nav-link', periodic && 'active')}
style={CLICKABLE}
>
{_(`schedulingEveryN${labelId}`)}
</a>
</li>
</ul>
)}
{periodic ? (
<Range
ref='range'
min={range[0]}
max={range[1]}
onChange={this._onChange}
value={rangeValue}
/>
) : (
<TableSelect
labelId={labelId}
onChange={this._onChange}
options={options}
optionRenderer={optionRenderer}
value={tableValue || []}
/>
)}
</CardBlock>
</Card>
)
}
}
@@ -394,11 +406,11 @@ const isWeekDayMode = ({ monthDayPattern, weekDayPattern }) => {
@propTypes({
monthDayPattern: propTypes.string.isRequired,
weekDayPattern: propTypes.string.isRequired
weekDayPattern: propTypes.string.isRequired,
})
class DayPicker extends Component {
state = {
weekDayMode: isWeekDayMode(this.props)
weekDayMode: isWeekDayMode(this.props),
}
componentWillReceiveProps (props) {
@@ -410,7 +422,7 @@ class DayPicker extends Component {
}
_setWeekDayMode = weekDayMode => {
this.props.onChange([ '*', '*' ])
this.props.onChange(['*', '*'])
this.setState({ weekDayMode })
}
@@ -419,7 +431,7 @@ class DayPicker extends Component {
this.props.onChange([
isMonthDayPattern ? cron : '*',
isMonthDayPattern ? '*' : cron
isMonthDayPattern ? '*' : cron,
])
}
@@ -428,22 +440,34 @@ class DayPicker extends Component {
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
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}
/>
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}
/>
)
}
}
@@ -455,8 +479,8 @@ class DayPicker extends Component {
timezone: propTypes.string,
value: propTypes.shape({
cronPattern: propTypes.string.isRequired,
timezone: propTypes.string
})
timezone: propTypes.string,
}),
})
export default class Scheduler extends Component {
constructor (props) {
@@ -470,20 +494,21 @@ export default class Scheduler extends Component {
this.props.onChange({
cronPattern: cronPattern.join(' '),
timezone: this._getTimezone()
timezone: this._getTimezone(),
})
}
forEach(UNITS, unit => {
this[`_${unit}Change`] = cron => this._onCronChange({ [unit]: cron })
})
this._dayChange = ([ monthDay, weekDay ]) => this._onCronChange({ monthDay, weekDay })
this._dayChange = ([monthDay, weekDay]) =>
this._onCronChange({ monthDay, weekDay })
}
_onTimezoneChange = timezone => {
this.props.onChange({
cronPattern: this._getCronPattern(),
timezone
timezone,
})
}
@@ -504,7 +529,7 @@ export default class Scheduler extends Component {
return (
<div className='card-block'>
<Row>
<Col mediumSize={6}>
<Col largeSize={6}>
<TimePicker
labelId='Month'
optionRenderer={getMonthName}
@@ -513,13 +538,17 @@ export default class Scheduler extends Component {
range={MONTHS_RANGE}
value={cronPatternArr[PICKTIME_TO_ID['month']]}
/>
</Col>
<Col largeSize={6}>
<DayPicker
onChange={this._dayChange}
monthDayPattern={cronPatternArr[PICKTIME_TO_ID['monthDay']]}
weekDayPattern={cronPatternArr[PICKTIME_TO_ID['weekDay']]}
/>
</Col>
<Col mediumSize={6}>
</Row>
<Row>
<Col largeSize={6}>
<TimePicker
labelId='Hour'
options={HOURS}
@@ -527,6 +556,8 @@ export default class Scheduler extends Component {
onChange={this._hourChange}
value={cronPatternArr[PICKTIME_TO_ID['hour']]}
/>
</Col>
<Col largeSize={6}>
<TimePicker
labelId='Minute'
options={MINS}
@@ -539,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

@@ -8,7 +8,7 @@ import { omit } from 'lodash'
@propTypes({
multi: propTypes.bool,
label: propTypes.node,
onChange: propTypes.func.isRequired
onChange: propTypes.func.isRequired,
})
export default class SelectFiles extends Component {
_onChange = e => {
@@ -19,14 +19,16 @@ export default class SelectFiles extends Component {
}
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>
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

@@ -14,7 +14,7 @@ import {
orderBy,
pickBy,
size,
slice
slice,
} from 'lodash'
import invoke from './invoke'
@@ -26,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'
// -------------------------------------------------------------------
@@ -93,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)
@@ -106,23 +103,19 @@ 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.
@@ -130,19 +123,18 @@ export const createCounter = (collection, predicate) =>
// Should only be used with a reasonable number of properties.
export const createPicker = (object, props) =>
_create2(
object, props,
_createCollectionWrapper(
(object, props) => {
const values = {}
forEach(props, prop => {
const value = object[prop]
if (value) {
values[prop] = value
}
})
return values
}
)
object,
props,
_createCollectionWrapper((object, props) => {
const values = {}
forEach(props, prop => {
const value = object[prop]
if (value) {
values[prop] = value
}
})
return values
})
)
// Special cases:
@@ -153,52 +145,38 @@ export const createFilter = (collection, predicate) =>
collection,
predicate,
_createCollectionWrapper(
(collection, predicate) => predicate === false
? (isArrayLike(collection) ? EMPTY_ARRAY : EMPTY_OBJECT)
: predicate
? (isArrayLike(collection) ? filter : pickBy)(collection, predicate)
: collection
(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) =>
_create2(
array,
page,
n,
_createCollectionWrapper(
(array, page, n) => {
const start = (page - 1) * n
return slice(array, start, start + n)
}
)
_createCollectionWrapper((array, page, n) => {
const start = (page - 1) * n
return slice(array, start, start + n)
})
)
export const createSort = (
collection,
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)
_create2(itemsSelector, iterateeSelector, (items, iteratee) =>
map(items, iteratee).reduce(add, 0)
)
export const createTop = (collection, iteratee, n) =>
@@ -206,15 +184,13 @@ export const createTop = (collection, iteratee, n) =>
collection,
iteratee,
n,
_createCollectionWrapper(
(objects, iteratee, n) => {
let results = orderBy(objects, iteratee, 'desc')
if (n < results.length) {
results.length = n
}
return results
_createCollectionWrapper((objects, iteratee, n) => {
const results = orderBy(objects, iteratee, 'desc')
if (n < results.length) {
results.length = n
}
)
return results
})
)
// ===================================================================
@@ -222,9 +198,8 @@ export const createTop = (collection, iteratee, n) =>
export const areObjectsFetched = state => state.objects.fetched
const _getId = (state, { routeParams, id }) => routeParams
? routeParams.id
: id
const _getId = (state, { routeParams, id }) =>
routeParams ? routeParams.id : id
export const getLang = state => state.lang
@@ -238,9 +213,10 @@ export const getCheckPermissions = invoke(() => {
state => state.objects,
(permissions, objects) => {
objects = objects.all
const getObject = id => (objects[id] || EMPTY_OBJECT)
const getObject = id => objects[id] || EMPTY_OBJECT
return (id, permission) => checkPermissions(permissions, getObject, id, permission)
return (id, permission) =>
checkPermissions(permissions, getObject, id, permission)
}
)
@@ -268,7 +244,7 @@ const _getPermissionsPredicate = invoke(() => {
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')
}
@@ -298,33 +274,36 @@ export const isAdmin = (...args) => {
// Common selector creators.
// Creates an object selector from an id selector.
export const createGetObject = (idSelector = _getId) =>
(state, props, useResourceSet) => {
const object = state.objects.all[idSelector(state, props)]
if (!object) {
return
}
if (useResourceSet) {
return object
}
const predicate = _getPermissionsPredicate(state)
if (!predicate) {
if (predicate == null) {
return object // no filtering
}
// predicate is false.
return
}
if (predicate(object)) {
return object
}
export const createGetObject = (idSelector = _getId) => (
state,
props,
useResourceSet
) => {
const object = state.objects.all[idSelector(state, props)]
if (!object) {
return
}
if (useResourceSet) {
return object
}
const predicate = _getPermissionsPredicate(state)
if (!predicate) {
if (predicate == null) {
return object // no filtering
}
// predicate is false.
return
}
if (predicate(object)) {
return object
}
}
// Specialized createSort() configured for a given type.
export const createSortForType = invoke(() => {
const iterateesByType = {
@@ -335,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.
@@ -390,17 +366,17 @@ const _extendCollectionSelector = (selector, objectsType) => {
// count, groupBy and sort can be chained.
const _addFilter = selector => {
selector.filter = predicate => _addCount(_addGroupBy(_addSort(
createFilter(selector, predicate)
)))
selector.filter = predicate =>
_addCount(_addGroupBy(_addSort(createFilter(selector, predicate))))
return selector
}
_addFilter(selector)
// filter, groupBy and sort can be chained.
selector.pick = idsSelector => _addFind(_addFilter(_addGroupBy(_addSort(
createPicker(selector, idsSelector)
))))
selector.pick = idsSelector =>
_addFind(
_addFilter(_addGroupBy(_addSort(createPicker(selector, idsSelector))))
)
return selector
}
@@ -426,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 => {
@@ -437,31 +413,34 @@ 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) => {
export const createGetVmLastShutdownTime = (
getVmId = (_, { vm }) => (vm != null ? vm.id : undefined)
) =>
create(getVmId, createGetObjectsOfType('message'), (vmId, messages) => {
let max = null
forEach(messages, message => {
if (
@@ -473,16 +452,17 @@ export const createGetVmLastShutdownTime = (getVmId = (_, {vm}) => vm != null ?
}
})
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'
@@ -492,27 +472,35 @@ export const getObject = createGetObject((_, id) => id)
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)
const patchRequiresReboot = createGetObjectsOfType('pool_patch')
.pick(
// Returns the first patch of the host which requires it to be
// restarted.
create(
createGetObjectsOfType('host_patch')
.pick((state, props) => {
const host = hostSelector(state, props)
return host && host.patches
})
.filter(
create(
(state, props) => {
const host = hostSelector(state, props)
return host && host.startTime
},
startTime => patch => patch.time > startTime
)
),
hostPatches => map(hostPatches, hostPatch => hostPatch.pool_patch)
)
)
).find([ ({ guidance }) => find(guidance, action =>
action === 'restartHost' || action === 'restartXapi'
) ])
.find([
({ guidance }) =>
find(
guidance,
action => action === 'restartHost' || action === 'restartXapi'
),
])
return create(
hostSelector,
@@ -524,23 +512,21 @@ export const createDoesHostNeedRestart = hostSelector => {
export const createGetHostMetrics = hostSelector =>
create(
hostSelector,
_createCollectionWrapper(
hosts => {
const metrics = {
count: 0,
cpus: 0,
memoryTotal: 0,
memoryUsage: 0
}
forEach(hosts, host => {
metrics.count++
metrics.cpus += host.cpus.cores
metrics.memoryTotal += host.memory.size
metrics.memoryUsage += host.memory.usage
})
return metrics
_createCollectionWrapper(hosts => {
const metrics = {
count: 0,
cpus: 0,
memoryTotal: 0,
memoryUsage: 0,
}
)
forEach(hosts, host => {
metrics.count++
metrics.cpus += host.cpus.cores
metrics.memoryTotal += host.memory.size
metrics.memoryUsage += host.memory.usage
})
return metrics
})
)
export const createGetVmDisks = vmSelector =>
@@ -549,10 +535,8 @@ export const createGetVmDisks = vmSelector =>
createGetObjectsOfType('VBD').pick(
(state, props) => vmSelector(state, props).$VBDs
),
_createCollectionWrapper(vbds => map(vbds, vbd =>
vbd.is_cd_drive
? undefined
: vbd.VDI
))
_createCollectionWrapper(vbds =>
map(vbds, vbd => (vbd.is_cd_drive ? undefined : vbd.VDI))
)
)
)

View File

@@ -32,7 +32,8 @@ const shallowEqual = (c1, c2) => {
}
let n = 0
for (const _ in c2) { // eslint-disable-line no-unused-vars
// eslint-disable-next-line no-unused-vars
for (const _ in c2) {
++n
}

View File

@@ -6,14 +6,13 @@ const SINGLE_LINE_STYLE = { display: 'flex' }
const COL_STYLE = { marginTop: 'auto', marginBottom: 'auto' }
const SingleLineRow = propTypes({
className: propTypes.string
})(({
children,
className
}) => <div
className={`${className || ''} row`}
style={SINGLE_LINE_STYLE}
>
{React.Children.map(children, child => child && cloneElement(child, { style: COL_STYLE }))}
</div>)
className: propTypes.string,
})(({ children, className }) => (
<div className={`${className || ''} row`} style={SINGLE_LINE_STYLE}>
{React.Children.map(
children,
child => child && cloneElement(child, { style: COL_STYLE })
)}
</div>
))
export { SingleLineRow as default }

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
import React from 'react'
import propTypes from '../prop-types-decorator'
const Null = () => null
export const Column = propTypes(Null)({
component: propTypes.func,
default: propTypes.bool,
name: propTypes.node,
itemRenderer: propTypes.func,
sortCriteria: propTypes.oneOfType([propTypes.func, propTypes.string]),
sortOrder: propTypes.string,
textAlign: propTypes.string,
})
export const SortedTable = Null
export const render = ({ vdis }) => (
<div>
<SortedTable collection={vdis}>
<Column name='Nom du VDI' sortCriteria='name_label' />
</SortedTable>
</div>
)

View File

@@ -1,12 +1,16 @@
import React from 'react'
import styled from 'styled-components'
import { omit } from 'lodash'
import ActionButton from './action-button'
import propTypes from './prop-types-decorator'
const Button = styled(ActionButton)`
background-color: ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateBg`]};
border: 2px solid ${p => p.theme[`${p.state ? 'enabled' : 'disabled'}StateColor`]};
// 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`]};
`
@@ -23,7 +27,7 @@ const StateButton = ({
state,
...props
}) =>
}) => (
<Button
handler={state ? enabledHandler : disabledHandler}
handlerParam={state ? enabledHandlerParam : disabledHandlerParam}
@@ -35,7 +39,8 @@ const StateButton = ({
>
{state ? enabledLabel : disabledLabel}
</Button>
)
export default propTypes({
state: propTypes.bool.isRequired
state: propTypes.bool.isRequired,
})(StateButton)

View File

@@ -1,22 +1,24 @@
const createAction = (() => {
const { defineProperty } = Object
return (type, payloadCreator) => defineProperty(
payloadCreator
? (...args) => ({
type,
payload: payloadCreator(...args)
})
: (action => function () {
if (arguments.length) {
throw new Error('this action expects no payload!')
}
return (type, payloadCreator) =>
defineProperty(
payloadCreator
? (...args) => ({
type,
payload: payloadCreator(...args),
})
: (action =>
function () {
if (arguments.length) {
throw new Error('this action expects no payload!')
}
return action
})({ type }),
'toString',
{ value: () => type }
)
return action
})({ type }),
'toString',
{ value: () => type }
)
})()
// ===================================================================
@@ -29,7 +31,10 @@ export const connected = createAction('CONNECTED')
export const disconnected = createAction('DISCONNECTED')
export const updateObjects = createAction('UPDATE_OBJECTS', updates => updates)
export const updatePermissions = createAction('UPDATE_PERMISSIONS', permissions => permissions)
export const updatePermissions = createAction(
'UPDATE_PERMISSIONS',
permissions => permissions
)
export const signedIn = createAction('SIGNED_IN', user => user)
export const signedOut = createAction('SIGNED_OUT')
@@ -37,5 +42,11 @@ export const signedOut = createAction('SIGNED_OUT')
export const xoaUpdaterState = createAction('XOA_UPDATER_STATE', state => state)
export const xoaTrialState = createAction('XOA_TRIAL_STATE', state => state)
export const xoaUpdaterLog = createAction('XOA_UPDATER_LOG', log => log)
export const xoaRegisterState = createAction('XOA_REGISTER_STATE', registration => registration)
export const xoaConfiguration = createAction('XOA_CONFIGURATION', configuration => configuration)
export const xoaRegisterState = createAction(
'XOA_REGISTER_STATE',
registration => registration
)
export const xoaConfiguration = createAction(
'XOA_CONFIGURATION',
configuration => configuration
)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,5 +2,5 @@ export default {
disabledStateBg: '#fff',
disabledStateColor: '#c0392b',
enabledStateBg: '#fff',
enabledStateColor: '#27ae60'
enabledStateColor: '#27ae60',
}

View File

@@ -16,22 +16,23 @@ const LOCAL_TIMEZONE = moment.tz.guess()
defaultValue: propTypes.string,
onChange: propTypes.func.isRequired,
required: propTypes.bool,
value: propTypes.string
value: propTypes.string,
})
export default class TimezonePicker extends Component {
componentDidMount () {
getXoServerTimezone.then(serverTimezone => {
this.setState({
timezone: this.props.value || this.props.defaultValue || SERVER_TIMEZONE_TAG,
timezone:
this.props.value || this.props.defaultValue || SERVER_TIMEZONE_TAG,
options: [
...map(moment.tz.names(), value => ({ label: value, value })),
{
label: _('serverTimezoneOption', {
value: serverTimezone
value: serverTimezone,
}),
value: SERVER_TIMEZONE_TAG
}
]
value: SERVER_TIMEZONE_TAG,
},
],
})
})
}
@@ -43,7 +44,9 @@ export default class TimezonePicker extends Component {
}
get value () {
return this.state.timezone === SERVER_TIMEZONE_TAG ? null : this.state.timezone
return this.state.timezone === SERVER_TIMEZONE_TAG
? null
: this.state.timezone
}
set value (value) {
@@ -55,10 +58,16 @@ export default class TimezonePicker extends Component {
return
}
this.setState({
timezone: (option != null && option.value) || SERVER_TIMEZONE_TAG
}, () =>
this.props.onChange(this.state.timezone === SERVER_TIMEZONE_TAG ? null : this.state.timezone)
this.setState(
{
timezone: (option != null && option.value) || SERVER_TIMEZONE_TAG,
},
() =>
this.props.onChange(
this.state.timezone === SERVER_TIMEZONE_TAG
? null
: this.state.timezone
)
)
}
@@ -80,10 +89,7 @@ export default class TimezonePicker extends Component {
value={timezone}
/>
<div className='pull-right'>
<ActionButton
handler={this._useLocalTime}
icon='time'
>
<ActionButton handler={this._useLocalTime} icon='time'>
{_('timezonePickerUseLocalTime')}
</ActionButton>
</div>

View File

@@ -19,29 +19,35 @@
export default function (e, target, node, place, effect, offset) {
const tipWidth = node.clientWidth
const tipHeight = node.clientHeight
const {mouseX, mouseY} = getCurrentOffset(e, target, effect)
const defaultOffset = getDefaultPosition(effect, target.clientWidth, target.clientHeight, tipWidth, tipHeight)
const {extraOffsetX, extraOffsetY} = calculateOffset(offset)
const { mouseX, mouseY } = getCurrentOffset(e, target, effect)
const defaultOffset = getDefaultPosition(
effect,
target.clientWidth,
target.clientHeight,
tipWidth,
tipHeight
)
const { extraOffsetX, extraOffsetY } = calculateOffset(offset)
const windowWidth = window.innerWidth
const windowHeight = window.innerHeight
const {parentTop, parentLeft} = getParent(target)
const { parentTop, parentLeft } = getParent(target)
// Get the edge offset of the tooltip
const getTipOffsetLeft = (place) => {
const getTipOffsetLeft = place => {
const offsetX = defaultOffset[place].l
return mouseX + offsetX + extraOffsetX
}
const getTipOffsetRight = (place) => {
const getTipOffsetRight = place => {
const offsetX = defaultOffset[place].r
return mouseX + offsetX + extraOffsetX
}
const getTipOffsetTop = (place) => {
const getTipOffsetTop = place => {
const offsetY = defaultOffset[place].t
return mouseY + offsetY + extraOffsetY
}
const getTipOffsetBottom = (place) => {
const getTipOffsetBottom = place => {
const offsetY = defaultOffset[place].b
return mouseY + offsetY + extraOffsetY
}
@@ -50,79 +56,103 @@ export default function (e, target, node, place, effect, offset) {
const outsideVertical = () => {
let result = false
let newPlace
if (getTipOffsetTop('left') < 0 &&
if (
getTipOffsetTop('left') < 0 &&
getTipOffsetBottom('left') <= windowHeight &&
getTipOffsetBottom('bottom') <= windowHeight) {
getTipOffsetBottom('bottom') <= windowHeight
) {
result = true
newPlace = 'bottom'
} else if (getTipOffsetBottom('left') > windowHeight &&
} else if (
getTipOffsetBottom('left') > windowHeight &&
getTipOffsetTop('left') >= 0 &&
getTipOffsetTop('top') >= 0) {
getTipOffsetTop('top') >= 0
) {
result = true
newPlace = 'top'
}
return {result, newPlace}
return { result, newPlace }
}
const outsideLeft = () => {
let {result, newPlace} = outsideVertical() // Deal with vertical as first priority
let { result, newPlace } = outsideVertical() // Deal with vertical as first priority
if (result && outsideHorizontal().result) {
return {result: false} // No need to change, if change to vertical will out of space
return { result: false } // No need to change, if change to vertical will out of space
}
if (!result && getTipOffsetLeft('left') < 0 && getTipOffsetRight('right') <= windowWidth) {
if (
!result &&
getTipOffsetLeft('left') < 0 &&
getTipOffsetRight('right') <= windowWidth
) {
result = true // If vertical ok, but let out of side and right won't out of side
newPlace = 'right'
}
return {result, newPlace}
return { result, newPlace }
}
const outsideRight = () => {
let {result, newPlace} = outsideVertical()
let { result, newPlace } = outsideVertical()
if (result && outsideHorizontal().result) {
return {result: false} // No need to change, if change to vertical will out of space
return { result: false } // No need to change, if change to vertical will out of space
}
if (!result && getTipOffsetRight('right') > windowWidth && getTipOffsetLeft('left') >= 0) {
if (
!result &&
getTipOffsetRight('right') > windowWidth &&
getTipOffsetLeft('left') >= 0
) {
result = true
newPlace = 'left'
}
return {result, newPlace}
return { result, newPlace }
}
const outsideHorizontal = () => {
let result = false
let newPlace
if (getTipOffsetLeft('top') < 0 &&
if (
getTipOffsetLeft('top') < 0 &&
getTipOffsetRight('top') <= windowWidth &&
getTipOffsetRight('right') <= windowWidth) {
getTipOffsetRight('right') <= windowWidth
) {
result = true
newPlace = 'right'
} else if (getTipOffsetRight('top') > windowWidth &&
} else if (
getTipOffsetRight('top') > windowWidth &&
getTipOffsetLeft('top') >= 0 &&
getTipOffsetLeft('left') >= 0) {
getTipOffsetLeft('left') >= 0
) {
result = true
newPlace = 'left'
}
return {result, newPlace}
return { result, newPlace }
}
const outsideTop = () => {
let {result, newPlace} = outsideHorizontal()
let { result, newPlace } = outsideHorizontal()
if (result && outsideVertical().result) {
return {result: false}
return { result: false }
}
if (!result && getTipOffsetTop('top') < 0 && getTipOffsetBottom('bottom') <= windowHeight) {
if (
!result &&
getTipOffsetTop('top') < 0 &&
getTipOffsetBottom('bottom') <= windowHeight
) {
result = true
newPlace = 'bottom'
}
return {result, newPlace}
return { result, newPlace }
}
const outsideBottom = () => {
let {result, newPlace} = outsideHorizontal()
let { result, newPlace } = outsideHorizontal()
if (result && outsideVertical().result) {
return {result: false}
return { result: false }
}
if (!result && getTipOffsetBottom('bottom') > windowHeight && getTipOffsetTop('top') >= 0) {
if (
!result &&
getTipOffsetBottom('bottom') > windowHeight &&
getTipOffsetTop('top') >= 0
) {
result = true
newPlace = 'top'
}
return {result, newPlace}
return { result, newPlace }
}
// Return new state to change the placement to the reverse if possible
@@ -134,22 +164,22 @@ export default function (e, target, node, place, effect, offset) {
if (place === 'left' && outsideLeftResult.result) {
return {
isNewState: true,
newState: {place: outsideLeftResult.newPlace}
newState: { place: outsideLeftResult.newPlace },
}
} else if (place === 'right' && outsideRightResult.result) {
return {
isNewState: true,
newState: {place: outsideRightResult.newPlace}
newState: { place: outsideRightResult.newPlace },
}
} else if (place === 'top' && outsideTopResult.result) {
return {
isNewState: true,
newState: {place: outsideTopResult.newPlace}
newState: { place: outsideTopResult.newPlace },
}
} else if (place === 'bottom' && outsideBottomResult.result) {
return {
isNewState: true,
newState: {place: outsideBottomResult.newPlace}
newState: { place: outsideBottomResult.newPlace },
}
}
@@ -158,8 +188,8 @@ export default function (e, target, node, place, effect, offset) {
isNewState: false,
position: {
left: getTipOffsetLeft(place) - parentLeft,
top: getTipOffsetTop(place) - parentTop
}
top: getTipOffsetTop(place) - parentTop,
},
}
}
@@ -174,18 +204,24 @@ const getCurrentOffset = (e, currentTarget, effect) => {
if (effect === 'float') {
return {
mouseX: e.clientX,
mouseY: e.clientY
mouseY: e.clientY,
}
}
return {
mouseX: targetLeft + (targetWidth / 2),
mouseY: targetTop + (targetHeight / 2)
mouseX: targetLeft + targetWidth / 2,
mouseY: targetTop + targetHeight / 2,
}
}
// List all possibility of tooltip final offset
// This is useful in judging if it is necessary for tooltip to switch position when out of window
const getDefaultPosition = (effect, targetWidth, targetHeight, tipWidth, tipHeight) => {
const getDefaultPosition = (
effect,
targetWidth,
targetHeight,
tipWidth,
tipHeight
) => {
let top
let right
let bottom
@@ -199,65 +235,65 @@ const getDefaultPosition = (effect, targetWidth, targetHeight, tipWidth, tipHeig
l: -(tipWidth / 2),
r: tipWidth / 2,
t: -(tipHeight + disToMouse + triangleHeight),
b: -disToMouse
b: -disToMouse,
}
bottom = {
l: -(tipWidth / 2),
r: tipWidth / 2,
t: disToMouse + cursorHeight,
b: tipHeight + disToMouse + triangleHeight + cursorHeight
b: tipHeight + disToMouse + triangleHeight + cursorHeight,
}
left = {
l: -(tipWidth + disToMouse + triangleHeight),
r: -disToMouse,
t: -(tipHeight / 2),
b: tipHeight / 2
b: tipHeight / 2,
}
right = {
l: disToMouse,
r: tipWidth + disToMouse + triangleHeight,
t: -(tipHeight / 2),
b: tipHeight / 2
b: tipHeight / 2,
}
} else if (effect === 'solid') {
top = {
l: -(tipWidth / 2),
r: tipWidth / 2,
t: -(targetHeight / 2 + tipHeight + triangleHeight),
b: -(targetHeight / 2)
b: -(targetHeight / 2),
}
bottom = {
l: -(tipWidth / 2),
r: tipWidth / 2,
t: targetHeight / 2,
b: targetHeight / 2 + tipHeight + triangleHeight
b: targetHeight / 2 + tipHeight + triangleHeight,
}
left = {
l: -(tipWidth + targetWidth / 2 + triangleHeight),
r: -(targetWidth / 2),
t: -(tipHeight / 2),
b: tipHeight / 2
b: tipHeight / 2,
}
right = {
l: targetWidth / 2,
r: tipWidth + targetWidth / 2 + triangleHeight,
t: -(tipHeight / 2),
b: tipHeight / 2
b: tipHeight / 2,
}
}
return {top, bottom, left, right}
return { top, bottom, left, right }
}
// Consider additional offset into position calculation
const calculateOffset = (offset) => {
const calculateOffset = offset => {
let extraOffsetX = 0
let extraOffsetY = 0
if (Object.prototype.toString.apply(offset) === '[object String]') {
offset = JSON.parse(offset.toString().replace(/'/g, '"'))
}
for (let key in offset) {
for (const key in offset) {
if (key === 'top') {
extraOffsetY -= parseInt(offset[key], 10)
} else if (key === 'bottom') {
@@ -269,11 +305,11 @@ const calculateOffset = (offset) => {
}
}
return {extraOffsetX, extraOffsetY}
return { extraOffsetX, extraOffsetY }
}
// Get the offset of the parent elements
const getParent = (currentTarget) => {
const getParent = currentTarget => {
let currentParent = currentTarget
while (currentParent) {
if (currentParent.style.transform.length > 0) break
@@ -283,5 +319,5 @@ const getParent = (currentTarget) => {
const parentTop = currentParent && currentParent.getBoundingClientRect().top
const parentLeft = currentParent && currentParent.getBoundingClientRect().left
return {parentTop, parentLeft}
return { parentTop, parentLeft }
}

View File

@@ -32,21 +32,20 @@ export class TooltipViewer extends Component {
}
render () {
const {
className,
content,
place,
show,
style
} = this.state
const { className, content, place, show, style } = this.state
return (
<div
className={classNames(show ? styles.tooltipEnabled : styles.tooltipDisabled, className)}
className={classNames(
show ? styles.tooltipEnabled : styles.tooltipDisabled,
className
)}
style={{
marginTop: (place === 'top' && '-10px') || (place === 'bottom' && '10px'),
marginLeft: (place === 'left' && '-10px') || (place === 'right' && '10px'),
...style
marginTop:
(place === 'top' && '-10px') || (place === 'bottom' && '10px'),
marginLeft:
(place === 'left' && '-10px') || (place === 'right' && '10px'),
...style,
}}
>
{content}
@@ -58,14 +57,11 @@ export class TooltipViewer extends Component {
// ===================================================================
@propTypes({
children: propTypes.oneOfType([
propTypes.element,
propTypes.string
]),
children: propTypes.oneOfType([propTypes.element, propTypes.string]),
className: propTypes.string,
content: propTypes.node,
style: propTypes.object,
tagName: propTypes.string
tagName: propTypes.string,
})
export default class Tooltip extends Component {
componentDidMount () {
@@ -89,7 +85,7 @@ export default class Tooltip extends Component {
}
_addListeners () {
const node = this._node = ReactDOM.findDOMNode(this)
const node = (this._node = ReactDOM.findDOMNode(this))
node.addEventListener('mouseenter', this._showTooltip)
node.addEventListener('mouseleave', this._hideTooltip)
@@ -118,7 +114,7 @@ export default class Tooltip extends Component {
className: props.className,
content: props.content,
show: true,
style: props.style
style: props.style,
})
}
@@ -128,10 +124,19 @@ export default class Tooltip extends Component {
_updateTooltip = event => {
const node = ReactDOM.findDOMNode(instance)
const result = getPosition(event, event.currentTarget, node, instance.state.place, 'solid', {})
const result = getPosition(
event,
event.currentTarget,
node,
instance.state.place,
'solid',
{}
)
if (result.isNewState) {
return instance.setState(result.newState, () => this._updateTooltip(event))
return instance.setState(result.newState, () =>
this._updateTooltip(event)
)
}
const { position } = result

View File

@@ -12,20 +12,19 @@ const Usage = ({ total, children }) => {
return value < limit && value
})
const othersTotal = sum(othersValues)
return <span className='usage'>
{React.Children.map(children, (child, index) =>
child.props.value > limit && cloneElement(child, { total })
)}
<Element
others
tooltip={_('others')}
total={total}
value={othersTotal}
/>
</span>
return (
<span className='usage'>
{React.Children.map(
children,
(child, index) =>
child.props.value > limit && cloneElement(child, { total })
)}
<Element others tooltip={_('others')} total={total} value={othersTotal} />
</span>
)
}
Usage.propTypes = {
total: PropTypes.number.isRequired
total: PropTypes.number.isRequired,
}
export { Usage as default }
@@ -38,7 +37,7 @@ const Element = ({ highlight, href, others, tooltip, total, value }) => (
highlight && 'usage-element-highlight',
others && 'usage-element-others'
)}
style={{ width: (value / total) * 100 + '%' }}
style={{ width: value / total * 100 + '%' }}
/>
</Tooltip>
)
@@ -47,26 +46,32 @@ Element.propTypes = {
href: PropTypes.string,
others: PropTypes.bool,
tooltip: PropTypes.node,
value: PropTypes.number.isRequired
value: PropTypes.number.isRequired,
}
export { Element as UsageElement }
export const Limits = ({ used, toBeUsed, limit }) => {
const available = limit - used
return <span className='limits'>
<span
className='limits-used'
style={{ width: ((used || 0) / limit) * 100 + '%' }}
/>
<span
className={toBeUsed > available ? 'limits-over-used' : 'limits-to-be-used'}
style={{ width: (Math.min((toBeUsed || 0), available) / limit) * 100 + '%' }}
/>
</span>
return (
<span className='limits'>
<span
className='limits-used'
style={{ width: (used || 0) / limit * 100 + '%' }}
/>
<span
className={
toBeUsed > available ? 'limits-over-used' : 'limits-to-be-used'
}
style={{
width: Math.min(toBeUsed || 0, available) / limit * 100 + '%',
}}
/>
</span>
)
}
Limits.propTypes = {
used: PropTypes.number,
toBeUsed: PropTypes.number,
limit: PropTypes.number.isRequired
limit: PropTypes.number.isRequired,
}

View File

@@ -1,22 +1,26 @@
import escapeRegExp from 'lodash/escapeRegExp'
import every from 'lodash/every'
import forEach from 'lodash/forEach'
import getStream from 'get-stream'
import humanFormat from 'human-format'
import isArray from 'lodash/isArray'
import isEmpty from 'lodash/isEmpty'
import isFunction from 'lodash/isFunction'
import isPlainObject from 'lodash/isPlainObject'
import isString from 'lodash/isString'
import join from 'lodash/join'
import keys from 'lodash/keys'
import map from 'lodash/map'
import mapValues from 'lodash/mapValues'
import React from 'react'
import ReadableStream from 'readable-stream'
import replace from 'lodash/replace'
import startsWith from 'lodash/startsWith'
import { connect } from 'react-redux'
import {
clone,
escapeRegExp,
every,
forEach,
isArray,
isEmpty,
isFunction,
isPlainObject,
isString,
join,
keys,
map,
mapValues,
replace,
sample,
startsWith,
} from 'lodash'
import _ from './intl'
import * as actions from './store/actions'
@@ -25,17 +29,17 @@ import invoke from './invoke'
import store from './store'
import { getObject } from './selectors'
export const EMPTY_ARRAY = Object.freeze([ ])
export const EMPTY_OBJECT = Object.freeze({ })
export const EMPTY_ARRAY = Object.freeze([])
export const EMPTY_OBJECT = Object.freeze({})
// ===================================================================
export const ensureArray = (value) => {
export const ensureArray = value => {
if (value === undefined) {
return []
}
return Array.isArray(value) ? value : [ value ]
return Array.isArray(value) ? value : [value]
}
export const propsEqual = (o1, o2, props) => {
@@ -63,8 +67,10 @@ export const addSubscriptions = subscriptions => Component => {
}
componentWillMount () {
this._unsubscribes = map(isFunction(subscriptions) ? subscriptions() : subscriptions, (subscribe, prop) =>
subscribe(value => this._setState({ [prop]: value }))
this._unsubscribes = map(
isFunction(subscriptions) ? subscriptions(this.props) : subscriptions,
(subscribe, prop) =>
subscribe(value => this._setState({ [prop]: value }))
)
}
@@ -83,10 +89,7 @@ export const addSubscriptions = subscriptions => Component => {
}
render () {
return <Component
{...this.props}
{...this.state}
/>
return <Component {...this.props} {...this.state} />
}
}
@@ -124,7 +127,7 @@ export const checkPropsState = (propsNames, stateNames) => Component => {
const _normalizeMapStateToProps = mapper => {
if (isFunction(mapper)) {
let factoryOrMapper = (state, props) => {
const factoryOrMapper = (state, props) => {
const result = mapper(state, props)
// Properly handles factory pattern.
@@ -140,7 +143,8 @@ const _normalizeMapStateToProps = mapper => {
}
if (every(result, isFunction)) {
indirection = (state, props) => mapValues(result, selector => selector(state, props))
indirection = (state, props) =>
mapValues(result, selector => selector(state, props))
return indirection(state, props)
}
}
@@ -176,7 +180,7 @@ export const connectStore = (mapStateToProps, opts = {}) => {
},
set (value) {
this.getWrappedInstance().value = value
}
},
})
}
@@ -190,19 +194,6 @@ export { default as Debug } from './debug'
// -------------------------------------------------------------------
// Returns the first defined (non-undefined) value.
export const firstDefined = function () {
const n = arguments.length
for (let i = 0; i < n; ++i) {
const arg = arguments[i]
if (arg !== undefined) {
return arg
}
}
}
// -------------------------------------------------------------------
// Returns the current XOA Plan or the Plan name if number given
export const getXoaPlan = plan => {
switch (plan || +process.env.XOA_PLAN) {
@@ -235,43 +226,49 @@ export const noop = () => {}
// -------------------------------------------------------------------
export const osFamily = invoke({
centos: [ 'centos' ],
debian: [ 'debian' ],
docker: [ 'coreos' ],
fedora: [ 'fedora' ],
freebsd: [ 'freebsd' ],
gentoo: [ 'gentoo' ],
'linux-mint': [ 'linux-mint' ],
netbsd: [ 'netbsd' ],
oracle: [ 'oracle' ],
osx: [ 'osx' ],
redhat: [ 'redhat', 'rhel' ],
solaris: [ 'solaris' ],
suse: [ 'sles', 'suse' ],
ubuntu: [ 'ubuntu' ],
windows: [ 'windows' ]
}, osByFamily => {
const osToFamily = Object.create(null)
forEach(osByFamily, (list, family) => {
forEach(list, os => {
osToFamily[os] = family
export const osFamily = invoke(
{
centos: ['centos'],
debian: ['debian'],
docker: ['coreos'],
fedora: ['fedora'],
freebsd: ['freebsd'],
gentoo: ['gentoo'],
'linux-mint': ['linux-mint'],
netbsd: ['netbsd'],
oracle: ['oracle'],
osx: ['osx'],
redhat: ['redhat', 'rhel'],
solaris: ['solaris'],
suse: ['sles', 'suse'],
ubuntu: ['ubuntu'],
windows: ['windows'],
},
osByFamily => {
const osToFamily = Object.create(null)
forEach(osByFamily, (list, family) => {
forEach(list, os => {
osToFamily[os] = family
})
})
})
return osName => osName && osToFamily[osName.toLowerCase()]
})
return osName => osName && osToFamily[osName.toLowerCase()]
}
)
// -------------------------------------------------------------------
export const formatSize = bytes => humanFormat(bytes, { scale: 'binary', unit: 'B' })
export const formatSize = bytes =>
humanFormat(bytes, { scale: 'binary', unit: 'B' })
export const formatSizeRaw = bytes => humanFormat.raw(bytes, { scale: 'binary', unit: 'B' })
export const formatSizeShort = bytes =>
humanFormat(bytes, { scale: 'binary', unit: 'B', decimals: 0 })
export const formatSpeed = (bytes, milliseconds) => humanFormat(
bytes * 1e3 / milliseconds,
{ scale: 'binary', unit: 'B/s' }
)
export const formatSizeRaw = bytes =>
humanFormat.raw(bytes, { scale: 'binary', unit: 'B' })
export const formatSpeed = (bytes, milliseconds) =>
humanFormat(bytes * 1e3 / milliseconds, { scale: 'binary', unit: 'B/s' })
export const parseSize = size => {
let bytes = humanFormat.parse.raw(size, { scale: 'binary' })
@@ -313,14 +310,14 @@ export const routes = (indexRoute, childRoutes) => target => {
indexRoute = undefined
} else if (isFunction(indexRoute)) {
indexRoute = {
component: indexRoute
component: indexRoute,
}
} else if (isString(indexRoute)) {
indexRoute = {
onEnter: invoke(indexRoute, pathname => (state, replace) => {
const current = state.location.pathname
replace((current === '/' ? '' : current) + '/' + pathname)
})
}),
}
}
@@ -341,7 +338,7 @@ export const routes = (indexRoute, childRoutes) => target => {
target.route = {
indexRoute,
childRoutes
childRoutes,
}
return target
@@ -357,11 +354,7 @@ export const routes = (indexRoute, childRoutes) => target => {
// function foo (param = throwFn('param is required')) {}
// ```
export const throwFn = error => () => {
throw (
isString(error)
? new Error(error)
: error
)
throw isString(error) ? new Error(error) : error
}
// ===================================================================
@@ -377,7 +370,7 @@ export const resolveResourceSet = resourceSet => {
...attrs,
missingObjects: [],
objectsByType: resolvedObjects,
ipPools
ipPools,
}
const state = store.getState()
@@ -393,7 +386,7 @@ export const resolveResourceSet = resourceSet => {
const { type } = object
if (!resolvedObjects[type]) {
resolvedObjects[type] = [ object ]
resolvedObjects[type] = [object]
} else {
resolvedObjects[type].push(object)
}
@@ -425,10 +418,11 @@ export const resolveResourceSets = resourceSets =>
// ```
export function buildTemplate (pattern, rules) {
const regExp = new RegExp(join(map(keys(rules), escapeRegExp), '|'), 'g')
return (...params) => replace(pattern, regExp, match => {
const rule = rules[match]
return isFunction(rule) ? rule(...params) : rule
})
return (...params) =>
replace(pattern, regExp, match => {
const rule = rules[match]
return isFunction(rule) ? rule(...params) : rule
})
}
// ===================================================================
@@ -467,9 +461,7 @@ export const htmlFileToStream = file => {
// ===================================================================
export const resolveId = value =>
(value != null && typeof value === 'object' && 'id' in value)
? value.id
: value
value != null && typeof value === 'object' && 'id' in value ? value.id : value
export const resolveIds = params => {
for (const key in params) {
@@ -488,28 +480,29 @@ const OPs = {
'<=': a => a <= 0,
'===': a => a === 0,
'>': a => a > 0,
'>=': a => a >= 0
'>=': a => a >= 0,
}
const makeNiceCompare = compare => function () {
const { length } = arguments
if (length === 2) {
return compare(arguments[0], arguments[1])
}
let i = 1
let v1 = arguments[0]
let op, v2
while (i < length) {
op = arguments[i++]
v2 = arguments[i++]
if (!OPs[op](compare(v1, v2))) {
return false
const makeNiceCompare = compare =>
function () {
const { length } = arguments
if (length === 2) {
return compare(arguments[0], arguments[1])
}
v1 = v2
let i = 1
let v1 = arguments[0]
let op, v2
while (i < length) {
op = arguments[i++]
v2 = arguments[i++]
if (!OPs[op](compare(v1, v2))) {
return false
}
v1 = v2
}
return true
}
return true
}
export const compareVersions = makeNiceCompare((v1, v2) => {
v1 = v1.split('.')
@@ -526,8 +519,7 @@ export const compareVersions = makeNiceCompare((v1, v2) => {
return 0
})
export const isXosanPack = ({ name }) =>
startsWith(name, 'XOSAN')
export const isXosanPack = ({ name }) => startsWith(name, 'XOSAN')
// ===================================================================
@@ -539,10 +531,54 @@ export const getCoresPerSocketPossibilities = (maxCoresPerSocket, vCPUs) => {
if (maxCoresPerSocket !== undefined && vCPUs !== '') {
const ratio = vCPUs / maxVCPUs
for (let coresPerSocket = maxCoresPerSocket; coresPerSocket >= ratio; coresPerSocket--) {
for (
let coresPerSocket = maxCoresPerSocket;
coresPerSocket >= ratio;
coresPerSocket--
) {
if (vCPUs % coresPerSocket === 0) options.push(coresPerSocket)
}
}
return options
}
// Generates a random human-readable string of length `length`
// Useful to generate random default names intended for the UI user
export const generateReadableRandomString = (() => {
const CONSONANTS = 'bdfgklmnprtvz'.split('')
const VOWELS = 'aeiou'.split('')
return (length = 8) => {
const result = new Array(length)
for (let i = 0; i < length; ++i) {
result[i] = sample((i & 1) === 0 ? VOWELS : CONSONANTS)
}
return result.join('')
}
})()
export const cowSet = (object, path, value, depth = 0) => {
if (depth >= path.length) {
return value
}
object = object != null ? clone(object) : {}
const prop = path[depth]
object[prop] = cowSet(object[prop], path, value, depth + 1)
return object
}
// Generates a function that returns a value between 0 and 1
// This function returns an estimated progress value between 0 and 1
// based on the elapsed time since the createFakeProgress call and
// the given estimated duration d
export const createFakeProgress = (() => {
const S = 0.95 // Progress value after d seconds
return d => {
const startTime = Date.now() / 1e3
return () => {
const x = Date.now() / 1e3 - startTime
return -Math.exp(x * Math.log(1 - S) / d) + 1
}
}
})()

View File

@@ -9,38 +9,36 @@ import propTypes from '../prop-types-decorator'
import styles from './index.css'
const Wizard = ({ children }) => {
const allDone = every(React.Children.toArray(children), child =>
child.props.done || child.props.summary
const allDone = every(
React.Children.toArray(children),
child => child.props.done || child.props.summary
)
return <ul className={styles.wizard}>
{React.Children.map(children, (child, key) =>
child && cloneElement(child, { allDone, key })
)}
</ul>
return (
<ul className={styles.wizard}>
{React.Children.map(
children,
(child, key) => child && cloneElement(child, { allDone, key })
)}
</ul>
)
}
export { Wizard as default }
@propTypes({
icon: propTypes.string.isRequired,
title: propTypes.string.isRequired
title: propTypes.string.isRequired,
})
export class Section extends Component {
componentWillMount () {
this.setState({isActive: false})
this.setState({ isActive: false })
}
_onFocus = () => this.setState({ isActive: true })
_onBlur = () => this.setState({ isActive: false })
render () {
const {
allDone,
icon,
title,
done,
children
} = this.props
const { allDone, icon, title, done, children } = this.props
return (
<li
className={classNames(
@@ -52,11 +50,15 @@ export class Section extends Component {
onBlur={this._onBlur}
>
{/* TITLE */}
<div className={classNames(
styles.title,
(done || allDone) && styles.success
)}>
<h4>{icon && <Icon icon={icon} />} {_(title)}</h4>
<div
className={classNames(
styles.title,
(done || allDone) && styles.success
)}
>
<h4>
{icon && <Icon icon={icon} />} {_(title)}
</h4>
</div>
{/* CONTENT */}
<div

62
src/common/xo-defined.js Normal file
View File

@@ -0,0 +1,62 @@
// Usage:
//
// ```js
// const httpProxy = defined(
// process.env.HTTP_PROXY,
// process.env.http_proxy
// )
//
// const httpProxy = defined([
// process.env.HTTP_PROXY,
// process.env.http_proxy
// ])
// ```
export default function defined () {
let args = arguments
let n = args.length
if (n === 1) {
args = arguments[0]
n = args.length
}
for (let i = 0; i < n; ++i) {
let arg = arguments[i]
if (typeof arg === 'function') {
arg = get(arg)
}
if (arg !== undefined) {
return arg
}
}
}
// Usage:
//
// ```js
// const friendName = get(() => props.user.friends[0].name)
//
// // this form can be used to avoid recreating functions:
// const getFriendName = _ => _.friends[0].name
// const friendName = get(getFriendName, props.user)
// ```
export const get = (accessor, arg) => {
try {
return accessor(arg)
} catch (error) {
if (!(error instanceof TypeError)) {
// avoid hiding other errors
throw error
}
}
}
// Usage:
//
// ```js
// const httpAgent = ifDef(
// process.env.HTTP_PROXY,
// _ => new ProxyAgent(_)
// )
// ```
export const ifDef = (value, thenFn) =>
value !== undefined ? thenFn(value) : value

View File

@@ -22,7 +22,7 @@ const XO_TYPE_TO_COMPONENT = {
subject: XoSubjectInput,
tag: XoTagInput,
vm: XoVmInput,
xoobject: XoHighLevelObjectInput
xoobject: XoHighLevelObjectInput,
}
// ===================================================================
@@ -41,10 +41,10 @@ const _generateUiSchema = (schema, uiSchema, key) => {
const type = getType(schema)
if (type === 'object') {
const properties = uiSchema.properties = {}
const properties = (uiSchema.properties = {})
forEach(schema.properties, (schema, key) => {
const subUiSchema = properties[key] = {}
const subUiSchema = (properties[key] = {})
_generateUiSchema(schema, subUiSchema, key)
})
} else if (type === 'array') {
@@ -54,7 +54,7 @@ const _generateUiSchema = (schema, uiSchema, key) => {
uiSchema.widget = widget
uiSchema.config = { multi: true }
} else {
const subUiSchema = uiSchema.items = {}
const subUiSchema = (uiSchema.items = {})
_generateUiSchema(schema.items, subUiSchema, key)
}
} else if (type === 'string') {

View File

@@ -13,9 +13,7 @@ export default class XoAbstractInput extends PureComponent {
const { props } = this
return props.onChange(
props.schema.type === 'array'
? map(value, getId)
: getId(value)
props.schema.type === 'array' ? map(value, getId) : getId(value)
)
}
}

View File

@@ -4,16 +4,7 @@ import ChartistTooltip from 'chartist-plugin-tooltip'
import React from 'react'
import { injectIntl } from 'react-intl'
import { messages } from 'intl'
import {
find,
flatten,
floor,
map,
max,
size,
sum,
values
} from 'lodash'
import { find, flatten, floor, map, max, size, sum, values } from 'lodash'
import propTypes from '../prop-types-decorator'
import { computeArraysSum } from '../xo-stats'
@@ -35,26 +26,37 @@ const getStatsLength = stats => size(find(stats, stats => stats != null))
// ===================================================================
const makeOptions = ({ intl, nValues, endTimestamp, interval, valueTransform }) => ({
const makeOptions = ({
intl,
nValues,
endTimestamp,
interval,
valueTransform,
}) => ({
showPoint: true,
lineSmooth: false,
showArea: true,
height: 300,
low: 0,
axisX: {
labelInterpolationFnc: makeLabelInterpolationFnc(intl, nValues, endTimestamp, interval),
offset: LABEL_OFFSET_X
labelInterpolationFnc: makeLabelInterpolationFnc(
intl,
nValues,
endTimestamp,
interval
),
offset: LABEL_OFFSET_X,
},
axisY: {
labelInterpolationFnc: valueTransform,
offset: LABEL_OFFSET_Y
offset: LABEL_OFFSET_Y,
},
plugins: [
ChartistLegend(),
ChartistTooltip({
valueTransform: value => valueTransform(+value) // '+value' because tooltip gives a string value...
})
]
valueTransform: value => valueTransform(+value), // '+value' because tooltip gives a string value...
}),
],
})
// ===================================================================
@@ -67,19 +69,22 @@ const makeLabelInterpolationFnc = (intl, nValues, endTimestamp, interval) => {
format = {
minute: 'numeric',
hour: 'numeric',
weekday: 'short'
weekday: 'short',
}
} else if (interval === 86400) {
format = {
day: 'numeric',
month: 'numeric',
year: 'numeric'
year: 'numeric',
}
}
return (value, index) =>
index % labelSpace === 0
? intl.formatTime((endTimestamp - (nValues - index - 1) * interval) * 1000, format)
? intl.formatTime(
(endTimestamp - (nValues - index - 1) * interval) * 1000,
format
)
: null
}
@@ -96,7 +101,7 @@ const buildSeries = ({ stats, label, addSumSeries }) => {
if (data) {
series.push({
name: `${label}${letter} (${io})`,
data
data,
})
}
}
@@ -105,7 +110,7 @@ const buildSeries = ({ stats, label, addSumSeries }) => {
series.push({
name: `All ${io}`,
data: computeArraysSum(values(ioData)),
className: styles.dashedLine
className: styles.dashedLine,
})
}
}
@@ -113,411 +118,436 @@ const buildSeries = ({ stats, label, addSumSeries }) => {
return series
}
const templateError =
<div>
No stats.
</div>
const templateError = <div>No stats.</div>
// ===================================================================
export const CpuLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const stats = data.stats.cpus
const length = getStatsLength(stats)
export const CpuLineChart = injectIntl(
propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object,
})(({ addSumSeries, data, options = {}, intl }) => {
const stats = data.stats.cpus
const length = getStatsLength(stats)
if (!length) {
return templateError
}
if (!length) {
return templateError
}
const series = map(stats, (data, id) => ({
name: `Cpu${id}`,
data
}))
const series = map(stats, (data, id) => ({
name: `Cpu${id}`,
data,
}))
if (addSumSeries) {
series.push({
name: 'All Cpus',
data: computeArraysSum(stats),
className: styles.dashedLine
})
}
if (addSumSeries) {
series.push({
name: 'All Cpus',
data: computeArraysSum(stats),
className: styles.dashedLine,
})
}
return (
<ChartistGraph
type='Line'
data={{
series
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: data.endTimestamp,
interval: data.interval,
valueTransform: value => `${floor(value)}%`
}),
high: !addSumSeries ? 100 : stats.length * 100,
...options
}}
/>
)
}))
return (
<ChartistGraph
type='Line'
data={{
series,
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: data.endTimestamp,
interval: data.interval,
valueTransform: value => `${floor(value)}%`,
}),
high: !addSumSeries ? 100 : stats.length * 100,
...options,
}}
/>
)
})
)
export const PoolCpuLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const firstHostData = data[0]
const length = getStatsLength(firstHostData.stats.cpus)
export const PoolCpuLineChart = injectIntl(
propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object,
})(({ addSumSeries, data, options = {}, intl }) => {
const firstHostData = data[0]
const length = getStatsLength(firstHostData.stats.cpus)
if (!length) {
return templateError
}
if (!length) {
return templateError
}
const series = map(data, ({ host, stats }) => ({
name: host,
data: computeArraysSum(stats.cpus)
}))
const series = map(data, ({ host, stats }) => ({
name: host,
data: computeArraysSum(stats.cpus),
}))
if (addSumSeries) {
series.push({
name: intl.formatMessage(messages.poolAllHosts),
data: computeArraysSum(map(series, 'data')),
className: styles.dashedLine
})
}
if (addSumSeries) {
series.push({
name: intl.formatMessage(messages.poolAllHosts),
data: computeArraysSum(map(series, 'data')),
className: styles.dashedLine,
})
}
const nbCpusByHost = map(data, ({ stats }) => stats.cpus.length)
const nbCpusByHost = map(data, ({ stats }) => stats.cpus.length)
return (
<ChartistGraph
type='Line'
data={{
series
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: firstHostData.endTimestamp,
interval: firstHostData.interval,
valueTransform: value => `${floor(value)}%`
}),
high: 100 * (addSumSeries ? sum(nbCpusByHost) : max(nbCpusByHost)),
...options
}}
/>
)
}))
return (
<ChartistGraph
type='Line'
data={{
series,
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: firstHostData.endTimestamp,
interval: firstHostData.interval,
valueTransform: value => `${floor(value)}%`,
}),
high: 100 * (addSumSeries ? sum(nbCpusByHost) : max(nbCpusByHost)),
...options,
}}
/>
)
})
)
export const MemoryLineChart = injectIntl(propTypes({
data: propTypes.object.isRequired,
options: propTypes.object
})(({ data, options = {}, intl }) => {
const {
memory,
memoryUsed
} = data.stats
export const MemoryLineChart = injectIntl(
propTypes({
data: propTypes.object.isRequired,
options: propTypes.object,
})(({ data, options = {}, intl }) => {
const { memory, memoryUsed } = data.stats
if (!memory || !memoryUsed) {
return templateError
}
if (!memory || !memoryUsed) {
return templateError
}
return (
<ChartistGraph
type='Line'
data={{
series: [{
name: 'RAM',
data: memoryUsed
}]
}}
options={{
...makeOptions({
intl,
nValues: memoryUsed.length,
endTimestamp: data.endTimestamp,
interval: data.interval,
valueTransform: formatSize
}),
high: memory[memory.length - 1],
...options
}}
/>
)
}))
return (
<ChartistGraph
type='Line'
data={{
series: [
{
name: 'RAM',
data: memoryUsed,
},
],
}}
options={{
...makeOptions({
intl,
nValues: memoryUsed.length,
endTimestamp: data.endTimestamp,
interval: data.interval,
valueTransform: formatSize,
}),
high: memory[memory.length - 1],
...options,
}}
/>
)
})
)
export const PoolMemoryLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const firstHostData = data[0]
const {
memory,
memoryUsed
} = firstHostData.stats
export const PoolMemoryLineChart = injectIntl(
propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object,
})(({ addSumSeries, data, options = {}, intl }) => {
const firstHostData = data[0]
const { memory, memoryUsed } = firstHostData.stats
if (!memory || !memoryUsed) {
return templateError
}
if (!memory || !memoryUsed) {
return templateError
}
const series = map(data, ({ host, stats }) => ({
name: host,
data: stats.memoryUsed
}))
const series = map(data, ({ host, stats }) => ({
name: host,
data: stats.memoryUsed,
}))
if (addSumSeries) {
series.push({
name: intl.formatMessage(messages.poolAllHosts),
data: computeArraysSum(map(data, 'stats.memoryUsed')),
className: styles.dashedLine
})
}
if (addSumSeries) {
series.push({
name: intl.formatMessage(messages.poolAllHosts),
data: computeArraysSum(map(data, 'stats.memoryUsed')),
className: styles.dashedLine,
})
}
const currentMemoryByHost = map(data, ({ stats }) => stats.memory[stats.memory.length - 1])
const currentMemoryByHost = map(
data,
({ stats }) => stats.memory[stats.memory.length - 1]
)
return (
<ChartistGraph
type='Line'
data={{
series
}}
options={{
...makeOptions({
intl,
nValues: firstHostData.stats.memoryUsed.length,
endTimestamp: firstHostData.endTimestamp,
interval: firstHostData.interval,
valueTransform: formatSize
}),
high: addSumSeries ? sum(currentMemoryByHost) : max(currentMemoryByHost),
...options
}}
/>
)
}))
return (
<ChartistGraph
type='Line'
data={{
series,
}}
options={{
...makeOptions({
intl,
nValues: firstHostData.stats.memoryUsed.length,
endTimestamp: firstHostData.endTimestamp,
interval: firstHostData.interval,
valueTransform: formatSize,
}),
high: addSumSeries
? sum(currentMemoryByHost)
: max(currentMemoryByHost),
...options,
}}
/>
)
})
)
export const XvdLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const stats = data.stats.xvds
const length = stats && getStatsLength(stats.r)
export const XvdLineChart = injectIntl(
propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object,
})(({ addSumSeries, data, options = {}, intl }) => {
const stats = data.stats.xvds
const length = stats && getStatsLength(stats.r)
if (!length) {
return templateError
}
if (!length) {
return templateError
}
return (
<ChartistGraph
type='Line'
data={{
series: buildSeries({ addSumSeries, stats, label: 'Xvd' })
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: data.endTimestamp,
interval: data.interval,
valueTransform: formatSize
}),
...options
}}
/>
)
}))
return (
<ChartistGraph
type='Line'
data={{
series: buildSeries({ addSumSeries, stats, label: 'Xvd' }),
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: data.endTimestamp,
interval: data.interval,
valueTransform: formatSize,
}),
...options,
}}
/>
)
})
)
export const VifLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const stats = data.stats.vifs
const length = stats && getStatsLength(stats.rx)
export const VifLineChart = injectIntl(
propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object,
})(({ addSumSeries, data, options = {}, intl }) => {
const stats = data.stats.vifs
const length = stats && getStatsLength(stats.rx)
if (!length) {
return templateError
}
if (!length) {
return templateError
}
return (
<ChartistGraph
type='Line'
data={{
series: buildSeries({ addSumSeries, stats, label: 'Vif' })
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: data.endTimestamp,
interval: data.interval,
valueTransform: formatSize
}),
...options
}}
/>
)
}))
return (
<ChartistGraph
type='Line'
data={{
series: buildSeries({ addSumSeries, stats, label: 'Vif' }),
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: data.endTimestamp,
interval: data.interval,
valueTransform: formatSize,
}),
...options,
}}
/>
)
})
)
export const PifLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const stats = data.stats.pifs
const length = stats && getStatsLength(stats.rx)
export const PifLineChart = injectIntl(
propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object,
})(({ addSumSeries, data, options = {}, intl }) => {
const stats = data.stats.pifs
const length = stats && getStatsLength(stats.rx)
if (!length) {
return templateError
}
if (!length) {
return templateError
}
return (
<ChartistGraph
type='Line'
data={{
series: buildSeries({ addSumSeries, stats, label: 'Pif' })
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: data.endTimestamp,
interval: data.interval,
valueTransform: formatSize
}),
...options
}}
/>
)
}))
return (
<ChartistGraph
type='Line'
data={{
series: buildSeries({ addSumSeries, stats, label: 'Pif' }),
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: data.endTimestamp,
interval: data.interval,
valueTransform: formatSize,
}),
...options,
}}
/>
)
})
)
const ios = ['rx', 'tx']
export const PoolPifLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const firstHostData = data[0]
const length = firstHostData.stats && getStatsLength(firstHostData.stats.pifs.rx)
export const PoolPifLineChart = injectIntl(
propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object,
})(({ addSumSeries, data, options = {}, intl }) => {
const firstHostData = data[0]
const length =
firstHostData.stats && getStatsLength(firstHostData.stats.pifs.rx)
if (!length) {
return templateError
}
if (!length) {
return templateError
}
const series = addSumSeries
? map(ios, io => ({
name: `${intl.formatMessage(messages.poolAllHosts)} (${io})`,
data: computeArraysSum(map(data, ({ stats }) => computeArraysSum(stats.pifs[io])))
}))
: flatten(map(data, ({ stats, host }) =>
map(ios, io => ({
name: `${host} (${io})`,
data: computeArraysSum(stats.pifs[io])
const series = addSumSeries
? map(ios, io => ({
name: `${intl.formatMessage(messages.poolAllHosts)} (${io})`,
data: computeArraysSum(
map(data, ({ stats }) => computeArraysSum(stats.pifs[io]))
),
}))
))
: flatten(
map(data, ({ stats, host }) =>
map(ios, io => ({
name: `${host} (${io})`,
data: computeArraysSum(stats.pifs[io]),
}))
)
)
return (
<ChartistGraph
type='Line'
data={{
series
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: firstHostData.endTimestamp,
interval: firstHostData.interval,
valueTransform: formatSize
}),
...options
}}
/>
)
}))
return (
<ChartistGraph
type='Line'
data={{
series,
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: firstHostData.endTimestamp,
interval: firstHostData.interval,
valueTransform: formatSize,
}),
...options,
}}
/>
)
})
)
export const LoadLineChart = injectIntl(propTypes({
data: propTypes.object.isRequired,
options: propTypes.object
})(({ data, options = {}, intl }) => {
const stats = data.stats.load
const { length } = stats || {}
export const LoadLineChart = injectIntl(
propTypes({
data: propTypes.object.isRequired,
options: propTypes.object,
})(({ data, options = {}, intl }) => {
const stats = data.stats.load
const { length } = stats || {}
if (!length) {
return templateError
}
if (!length) {
return templateError
}
return (
<ChartistGraph
type='Line'
data={{
series: [{
name: 'Load average',
data: stats
}]
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: data.endTimestamp,
interval: data.interval,
valueTransform: value => `${value.toPrecision(3)}`
}),
...options
}}
/>
)
}))
return (
<ChartistGraph
type='Line'
data={{
series: [
{
name: 'Load average',
data: stats,
},
],
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: data.endTimestamp,
interval: data.interval,
valueTransform: value => `${value.toPrecision(3)}`,
}),
...options,
}}
/>
)
})
)
export const PoolLoadLineChart = injectIntl(propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object
})(({ addSumSeries, data, options = {}, intl }) => {
const firstHostData = data[0]
const length = firstHostData.stats && firstHostData.stats.load.length
export const PoolLoadLineChart = injectIntl(
propTypes({
addSumSeries: propTypes.bool,
data: propTypes.object.isRequired,
options: propTypes.object,
})(({ addSumSeries, data, options = {}, intl }) => {
const firstHostData = data[0]
const length = firstHostData.stats && firstHostData.stats.load.length
if (!length) {
return templateError
}
if (!length) {
return templateError
}
const series = map(data, ({ host, stats }) => ({
name: host,
data: stats.load
}))
const series = map(data, ({ host, stats }) => ({
name: host,
data: stats.load,
}))
if (addSumSeries) {
series.push({
name: intl.formatMessage(messages.poolAllHosts),
data: computeArraysSum(map(data, 'stats.load')),
className: styles.dashedLine
})
}
if (addSumSeries) {
series.push({
name: intl.formatMessage(messages.poolAllHosts),
data: computeArraysSum(map(data, 'stats.load')),
className: styles.dashedLine,
})
}
return (
<ChartistGraph
type='Line'
data={{
series
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: firstHostData.endTimestamp,
interval: firstHostData.interval,
valueTransform: value => `${value.toPrecision(3)}`
}),
...options
}}
/>
)
}))
return (
<ChartistGraph
type='Line'
data={{
series,
}}
options={{
...makeOptions({
intl,
nValues: length,
endTimestamp: firstHostData.endTimestamp,
interval: firstHostData.interval,
valueTransform: value => `${value.toPrecision(3)}`,
}),
...options,
}}
/>
)
})
)

View File

@@ -33,7 +33,7 @@ const SVG_STYLE = {
left: 0,
position: 'absolute',
top: 0,
width: '100%'
width: '100%',
}
const SVG_CONTAINER_STYLE = {
@@ -41,34 +41,34 @@ const SVG_CONTAINER_STYLE = {
'vertical-align': 'middle',
overflow: 'hidden',
position: 'relative',
width: '100%'
width: '100%',
}
const SVG_CONTENT = {
'font-size': `${CHART_WIDTH / 100}px`
'font-size': `${CHART_WIDTH / 100}px`,
}
const COLUMN_TITLE_STYLE = {
'font-size': '100%',
'font-weight': 'bold',
'text-anchor': 'middle'
'text-anchor': 'middle',
}
const COLUMN_VALUES_STYLE = {
'font-size': '100%'
'font-size': '100%',
}
const LINES_CONTAINER_STYLE = {
'stroke-opacity': 0.5,
'stroke-width': CHART_WIDTH / DEFAULT_STROKE_WIDTH_FACTOR,
fill: 'none',
stroke: 'red'
stroke: 'red',
}
const TOOLTIP_STYLE = {
'fill': 'white',
fill: 'white',
'font-size': '125%',
'font-weight': 'bold'
'font-weight': 'bold',
}
// ===================================================================
@@ -78,11 +78,11 @@ const TOOLTIP_STYLE = {
propTypes.shape({
data: propTypes.object.isRequired,
label: propTypes.string.isRequired,
objectId: propTypes.string.isRequired
objectId: propTypes.string.isRequired,
})
).isRequired,
labels: propTypes.object.isRequired,
renderers: propTypes.object
renderers: propTypes.object,
})
export default class XoParallelChart extends Component {
_line = d3.line()
@@ -92,17 +92,17 @@ export default class XoParallelChart extends Component {
_handleBrush = () => {
// 1. Get selected brushes.
const brushes = []
this._svg.selectAll('.chartColumn')
this._svg
.selectAll('.chartColumn')
.selectAll('.brush')
.each((_1, _2, [ brush ]) => {
.each((_1, _2, [brush]) => {
if (d3.brushSelection(brush) != null) {
brushes.push(brush)
}
})
// 2. Change stroke of selected lines.
const lines = this._svg.select('.linesContainer')
.selectAll('path')
const lines = this._svg.select('.linesContainer').selectAll('path')
lines.each((elem, lineId, lines) => {
const { data } = elem
@@ -112,7 +112,10 @@ export default class XoParallelChart extends Component {
const columnId = brush.__data__
const { invert } = this._y[columnId] // Range to domain.
return invert(selection[1]) <= data[columnId] && data[columnId] <= invert(selection[0])
return (
invert(selection[1]) <= data[columnId] &&
data[columnId] <= invert(selection[0])
)
})
const line = d3.select(lines[lineId])
@@ -125,9 +128,13 @@ export default class XoParallelChart extends Component {
})
}
_brush = d3.brushY()
_brush = d3
.brushY()
// Brush area: (x0, y0), (x1, y1)
.extent([[-BRUSH_SELECTION_WIDTH / 2, 0], [BRUSH_SELECTION_WIDTH / 2, CHART_HEIGHT]])
.extent([
[-BRUSH_SELECTION_WIDTH / 2, 0],
[BRUSH_SELECTION_WIDTH / 2, CHART_HEIGHT],
])
.on('brush', this._handleBrush)
.on('end', this._handleBrush)
@@ -135,9 +142,7 @@ export default class XoParallelChart extends Component {
const svg = this._svg
// Reset tooltip.
svg
.selectAll('.objectTooltip')
.remove()
svg.selectAll('.objectTooltip').remove()
// Reset all lines.
svg
@@ -155,17 +160,19 @@ export default class XoParallelChart extends Component {
const { label } = elem
const tooltip = svg.append('g')
.attr('class', 'objectTooltip')
const tooltip = svg.append('g').attr('class', 'objectTooltip')
const bbox = tooltip.append('text')
const bbox = tooltip
.append('text')
.text(label)
.attr('x', position[0])
.attr('y', position[1] - 30)
::setStyles(TOOLTIP_STYLE)
.node().getBBox()
.node()
.getBBox()
tooltip.insert('rect', '*')
tooltip
.insert('rect', '*')
.attr('x', bbox.x - TOOLTIP_PADDING)
.attr('y', bbox.y - TOOLTIP_PADDING)
.attr('width', bbox.width + TOOLTIP_PADDING * 2)
@@ -177,7 +184,7 @@ export default class XoParallelChart extends Component {
this._highlight(elem, d3.mouse(paths[pathId]))
}
_handleMouseOut = (elem) => {
_handleMouseOut = elem => {
this._highlight()
}
@@ -187,47 +194,49 @@ export default class XoParallelChart extends Component {
const columnsIds = keys(labels)
const spacing = (CHART_WIDTH - 200) / (columnsIds.length - 1)
const x = d3.scaleOrdinal()
.domain(columnsIds).range(
times(columnsIds.length, n => n * spacing)
)
const x = d3
.scaleOrdinal()
.domain(columnsIds)
.range(times(columnsIds.length, n => n * spacing))
// 1. Remove old nodes.
svg
.selectAll('.chartColumn')
.remove()
svg.selectAll('.chartColumn').remove()
svg
.selectAll('.linesContainer')
.remove()
svg.selectAll('.linesContainer').remove()
// 2. Build Ys.
const y = this._y = {}
const y = (this._y = {})
forEach(columnsIds, (columnId, index) => {
const max = d3.max(dataSet, elem => elem.data[columnId])
y[columnId] = d3.scaleLinear()
y[columnId] = d3
.scaleLinear()
.domain([0, max])
.range([CHART_HEIGHT, 0])
})
// 3. Build columns.
const columns = svg.selectAll('.chartColumn')
const columns = svg
.selectAll('.chartColumn')
.data(columnsIds)
.enter().append('g')
.attr('class', 'chartColumn')
.attr('transform', d => `translate(${x(d)})`)
.enter()
.append('g')
.attr('class', 'chartColumn')
.attr('transform', d => `translate(${x(d)})`)
// 4. Draw titles.
columns.append('text')
;columns
.append('text')
.text(columnId => labels[columnId])
.attr('y', -50)
::setStyles(COLUMN_TITLE_STYLE)
// 5. Draw axis.
columns.append('g')
;columns
.append('g')
.each((columnId, axisId, axes) => {
const axis = d3.axisLeft()
const axis = d3
.axisLeft()
.ticks(N_TICKS, ',f')
.tickSize(TICK_SIZE)
.scale(y[columnId])
@@ -244,42 +253,54 @@ export default class XoParallelChart extends Component {
::setStyles(COLUMN_VALUES_STYLE)
// 6. Draw lines.
const path = elem => this._line(map(columnsIds.map(
columnId => [x(columnId), y[columnId](elem.data[columnId])]
)))
svg.append('g')
const path = elem =>
this._line(
map(
columnsIds.map(columnId => [
x(columnId),
y[columnId](elem.data[columnId]),
])
)
)
;svg
.append('g')
.attr('class', 'linesContainer')
::setStyles(LINES_CONTAINER_STYLE)
.selectAll('path')
.data(dataSet)
.enter().append('path')
.attr('d', path)
.attr('class', 'chartLine')
.attr('id', elem => 'chartLine-' + elem.objectId)
.attr('stroke', elem => this._color(elem.label))
.attr('shape-rendering', 'optimizeQuality')
.attr('stroke-linecap', 'round')
.attr('stroke-linejoin', 'round')
.on('mouseover', this._handleMouseOver)
.on('mouseout', this._handleMouseOut)
.data(dataSet)
.enter()
.append('path')
.attr('d', path)
.attr('class', 'chartLine')
.attr('id', elem => 'chartLine-' + elem.objectId)
.attr('stroke', elem => this._color(elem.label))
.attr('shape-rendering', 'optimizeQuality')
.attr('stroke-linecap', 'round')
.attr('stroke-linejoin', 'round')
.on('mouseover', this._handleMouseOver)
.on('mouseout', this._handleMouseOut)
// 7. Brushes.
columns.append('g')
columns
.append('g')
.attr('class', 'brush')
.each((_, brushId, brushes) => { d3.select(brushes[brushId]).call(this._brush) })
.each((_, brushId, brushes) => {
d3.select(brushes[brushId]).call(this._brush)
})
}
componentDidMount () {
this._svg = d3.select(this.refs.chart)
this._svg = d3
.select(this.refs.chart)
.append('div')
::setStyles(SVG_CONTAINER_STYLE)
.append('svg')
::setStyles(SVG_STYLE)
.attr('preserveAspectRatio', 'xMinYMin meet')
.attr('viewBox', `0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`)
.append('g')
.attr('transform', `translate(${100}, ${100})`)
::setStyles(SVG_CONTENT)
.append('svg')
::setStyles(SVG_STYLE)
.attr('preserveAspectRatio', 'xMinYMin meet')
.attr('viewBox', `0 0 ${CHART_WIDTH} ${CHART_HEIGHT}`)
.append('g')
.attr('transform', `translate(${100}, ${100})`)
::setStyles(SVG_CONTENT)
this._draw()
}

View File

@@ -1,14 +1,8 @@
import React from 'react'
import {
Sparklines,
SparklinesLine
} from 'react-sparklines'
import { Sparklines, SparklinesLine } from 'react-sparklines'
import propTypes from './prop-types-decorator'
import {
computeArraysAvg,
computeObjectsAvg
} from './xo-stats'
import { computeArraysAvg, computeObjectsAvg } from './xo-stats'
const STYLE = {}
@@ -18,15 +12,12 @@ const STROKE_WIDTH = 0.5
// ===================================================================
const templateError =
<div>
No stats.
</div>
const templateError = <div>No stats.</div>
// ===================================================================
export const CpuSparkLines = propTypes({
data: propTypes.object.isRequired
data: propTypes.object.isRequired,
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
const { cpus } = data.stats
@@ -35,14 +26,29 @@ export const CpuSparkLines = propTypes({
}
return (
<Sparklines style={STYLE} data={computeArraysAvg(cpus)} max={100} min={0} width={width} height={height}>
<SparklinesLine style={{ strokeWidth, stroke: '#366e98', fill: '#366e98', fillOpacity: 0.5 }} color='#2598d9' />
<Sparklines
style={STYLE}
data={computeArraysAvg(cpus)}
max={100}
min={0}
width={width}
height={height}
>
<SparklinesLine
style={{
strokeWidth,
stroke: '#366e98',
fill: '#366e98',
fillOpacity: 0.5,
}}
color='#2598d9'
/>
</Sparklines>
)
})
export const MemorySparkLines = propTypes({
data: propTypes.object.isRequired
data: propTypes.object.isRequired,
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
const { memory, memoryUsed } = data.stats
@@ -51,14 +57,29 @@ export const MemorySparkLines = propTypes({
}
return (
<Sparklines style={STYLE} data={memoryUsed} max={memory[memory.length - 1]} min={0} width={width} height={height}>
<SparklinesLine style={{ strokeWidth, stroke: '#990822', fill: '#990822', fillOpacity: 0.5 }} color='#cc0066' />
<Sparklines
style={STYLE}
data={memoryUsed}
max={memory[memory.length - 1]}
min={0}
width={width}
height={height}
>
<SparklinesLine
style={{
strokeWidth,
stroke: '#990822',
fill: '#990822',
fillOpacity: 0.5,
}}
color='#cc0066'
/>
</Sparklines>
)
})
export const XvdSparkLines = propTypes({
data: propTypes.object.isRequired
data: propTypes.object.isRequired,
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
const { xvds } = data.stats
@@ -67,46 +88,56 @@ export const XvdSparkLines = propTypes({
}
return (
<Sparklines style={STYLE} data={computeObjectsAvg(xvds)} min={0} width={width} height={height}>
<SparklinesLine style={{ strokeWidth, stroke: '#089944', fill: '#089944', fillOpacity: 0.5 }} color='#33cc33' />
<Sparklines
style={STYLE}
data={computeObjectsAvg(xvds)}
min={0}
width={width}
height={height}
>
<SparklinesLine
style={{
strokeWidth,
stroke: '#089944',
fill: '#089944',
fillOpacity: 0.5,
}}
color='#33cc33'
/>
</Sparklines>
)
})
export const VifSparkLines = propTypes({
data: propTypes.object.isRequired
export const NetworkSparkLines = propTypes({
data: propTypes.object.isRequired,
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
const { vifs } = data.stats
const { pifs, vifs: ifs = pifs } = data.stats
if (!vifs) {
return templateError
}
return (
<Sparklines style={STYLE} data={computeObjectsAvg(vifs)} min={0} width={width} height={height}>
<SparklinesLine style={{ strokeWidth, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
</Sparklines>
)
})
export const PifSparkLines = propTypes({
data: propTypes.object.isRequired
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
const { pifs } = data.stats
if (!pifs) {
return templateError
}
return (
<Sparklines style={STYLE} data={computeObjectsAvg(pifs)} min={0} width={width} height={height}>
<SparklinesLine style={{ strokeWidth, stroke: '#eca649', fill: '#eca649', fillOpacity: 0.5 }} color='#ffd633' />
return ifs === undefined ? (
templateError
) : (
<Sparklines
style={STYLE}
data={computeObjectsAvg(ifs)}
min={0}
width={width}
height={height}
>
<SparklinesLine
style={{
strokeWidth,
stroke: '#eca649',
fill: '#eca649',
fillOpacity: 0.5,
}}
color='#ffd633'
/>
</Sparklines>
)
})
export const LoadSparkLines = propTypes({
data: propTypes.object.isRequired
data: propTypes.object.isRequired,
})(({ data, width = WIDTH, height = HEIGHT, strokeWidth = STROKE_WIDTH }) => {
const { load } = data.stats
@@ -116,7 +147,15 @@ export const LoadSparkLines = propTypes({
return (
<Sparklines style={STYLE} data={load} min={0} width={width} height={height}>
<SparklinesLine style={{ strokeWidth, stroke: '#33cc33', fill: '#33cc33', fillOpacity: 0.5 }} color='#33cc33' />
<SparklinesLine
style={{
strokeWidth,
stroke: '#33cc33',
fill: '#33cc33',
fillOpacity: 0.5,
}}
color='#33cc33'
/>
</Sparklines>
)
})

View File

@@ -52,14 +52,17 @@ const _computeArraysAvg = arrays => {
//
// It's a fix to avoid error like `Uncaught TypeError: Cannot read property 'length' of null`.
// FIXME: Repare this bug in xo-server. (Warning: Can break the stats of xo-web v4.)
const removeUndefinedArrays = arrays => mapPlus(arrays, (array, push) => {
if (array != null) {
push(array)
}
})
const removeUndefinedArrays = arrays =>
mapPlus(arrays, (array, push) => {
if (array != null) {
push(array)
}
})
export const computeArraysSum = arrays => _computeArraysSum(removeUndefinedArrays(arrays))
export const computeArraysAvg = arrays => _computeArraysAvg(removeUndefinedArrays(arrays))
export const computeArraysSum = arrays =>
_computeArraysSum(removeUndefinedArrays(arrays))
export const computeArraysAvg = arrays =>
_computeArraysAvg(removeUndefinedArrays(arrays))
// More complex than computeArraysAvg.
//
@@ -72,8 +75,6 @@ export const computeArraysAvg = arrays => _computeArraysAvg(removeUndefinedArray
// Note: The parameter can be also an 3D array.
export const computeObjectsAvg = objects => {
return _computeArraysAvg(
map(objects, object =>
computeArraysAvg(values(object))
)
map(objects, object => computeArraysAvg(values(object)))
)
}

View File

@@ -8,14 +8,8 @@ import _ from '../intl'
import propTypes from '../prop-types-decorator'
import { Toggle } from '../form'
import { setStyles } from '../d3-utils'
import {
createGetObject,
createSelector
} from '../selectors'
import {
connectStore,
propsEqual
} from '../utils'
import { createGetObject, createSelector } from '../selectors'
import { connectStore, propsEqual } from '../utils'
import styles from './index.css'
@@ -24,33 +18,33 @@ import styles from './index.css'
const X_AXIS_STYLE = {
'shape-rendering': 'crispEdges',
fill: 'none',
stroke: '#000'
stroke: '#000',
}
const X_AXIS_TEXT_STYLE = {
'font-size': '125%',
fill: 'black',
stroke: 'transparent'
stroke: 'transparent',
}
const LABEL_STYLE = {
'font-size': '125%'
'font-size': '125%',
}
const MOUSE_AREA_STYLE = {
'pointer-events': 'all',
fill: 'none'
fill: 'none',
}
const HOVER_LINE_STYLE = {
'stroke-width': '2px',
'stroke-dasharray': '5 5',
stroke: 'red',
fill: 'none'
fill: 'none',
}
const HOVER_TEXT_STYLE = {
fill: 'black'
fill: 'black',
}
const HORIZON_AREA_N_STEPS = 4
@@ -59,7 +53,7 @@ const HORIZON_AREA_PATH_STYLE = {
'fill-opacity': 0.25,
'stroke-opacity': 0.3,
fill: 'darkgreen',
stroke: 'transparent'
stroke: 'transparent',
}
// ===================================================================
@@ -70,20 +64,18 @@ const HORIZON_AREA_PATH_STYLE = {
data: propTypes.arrayOf(
propTypes.shape({
date: propTypes.number.isRequired,
value: propTypes.number.isRequired
value: propTypes.number.isRequired,
})
).isRequired,
maxValue: propTypes.number,
objectId: propTypes.string.isRequired,
onTooltipChange: propTypes.func.isRequired,
tooltipX: propTypes.number.isRequired,
valueRenderer: propTypes.func
valueRenderer: propTypes.func,
})
@connectStore(() => {
const label = createSelector(
createGetObject(
(_, props) => props.objectId
),
createGetObject((_, props) => props.objectId),
object => object.name_label
)
@@ -93,7 +85,7 @@ class XoWeekChart extends Component {
static defaultProps = {
chartHeight: 70,
chartWidth: 300,
valueRenderer: value => value
valueRenderer: value => value,
}
_x = d3.scaleTime()
@@ -101,10 +93,10 @@ class XoWeekChart extends Component {
_bisectDate = d3.bisector(elem => elem.date).left
_xAxis = d3.axisBottom()
.scale(this._x)
_xAxis = d3.axisBottom().scale(this._x)
_line = d3.line()
_line = d3
.line()
.x(elem => this._x(elem.date))
.y(elem => this._y(elem.value))
@@ -115,10 +107,12 @@ class XoWeekChart extends Component {
// Start.
let date = new Date(data[0].date)
for (let i = 0; i < HORIZON_AREA_N_STEPS; i++) {
splittedData[i] = [{
date,
value: 0
}]
splittedData[i] = [
{
date,
value: 0,
},
]
}
// Middle.
@@ -127,7 +121,10 @@ class XoWeekChart extends Component {
for (let i = 0; i < HORIZON_AREA_N_STEPS; i++) {
splittedData[i].push({
date,
value: Math.min(Math.max(0, elem.value - intervalSize * i), intervalSize)
value: Math.min(
Math.max(0, elem.value - intervalSize * i),
intervalSize
),
})
}
})
@@ -137,7 +134,7 @@ class XoWeekChart extends Component {
for (let i = 0; i < HORIZON_AREA_N_STEPS; i++) {
splittedData[i].push({
date,
value: 0
value: 0,
})
}
@@ -146,13 +143,17 @@ class XoWeekChart extends Component {
const svg = this._svg
svg.select('.horizon-area').selectAll('path').remove()
svg
.select('.horizon-area')
.selectAll('path')
.remove()
forEach(splittedData, data => {
svg.select('.horizon-area')
;svg
.select('.horizon-area')
.append('path')
.datum(data)
.attr('d', this._line)
::setStyles(HORIZON_AREA_PATH_STYLE)
.datum(data)
.attr('d', this._line)
::setStyles(HORIZON_AREA_PATH_STYLE)
})
}
@@ -173,25 +174,28 @@ class XoWeekChart extends Component {
.attr('width', width)
.attr('height', height)
.select('.mouse-area')
.attr('width', horizonAreaWidth)
.attr('height', horizonAreaHeight)
.attr('width', horizonAreaWidth)
.attr('height', horizonAreaHeight)
svg.select('.hover-container')
svg
.select('.hover-container')
.select('.hover-line')
.attr('y2', horizonAreaHeight)
.attr('y2', horizonAreaHeight)
// 2. Draw horizon area.
this._drawHorizonArea(props.data, props.maxValue)
// 3. Update x axis.
svg.select('.x-axis')
;svg
.select('.x-axis')
.call(this._xAxis)
.attr('transform', `translate(0, ${props.chartHeight})`)
.selectAll('text')
::setStyles(X_AXIS_TEXT_STYLE)
::setStyles(X_AXIS_TEXT_STYLE)
// 4. Update label.
svg.select('.label')
svg
.select('.label')
.attr('dx', 5)
.attr('dy', 20)
.text(props.label)
@@ -223,11 +227,13 @@ class XoWeekChart extends Component {
const { props } = this
const hover = this._svg.select('.hover-container')
hover.select('.hover-line')
hover
.select('.hover-line')
.attr('x1', x)
.attr('x2', x)
hover.select('.hover-text')
hover
.select('.hover-text')
.attr('dx', x + 5)
.attr('dy', props.chartHeight / 2)
.text(props.valueRenderer(elem.value))
@@ -236,38 +242,44 @@ class XoWeekChart extends Component {
componentDidMount () {
// Horizon area ----------------------------------------
const svg = this._svg = d3.select(this.refs.chart)
const svg = (this._svg = d3
.select(this.refs.chart)
.append('svg')
.attr('transform', `translate(${HORIZON_AREA_MARGIN}, 0)`)
.attr('transform', `translate(${HORIZON_AREA_MARGIN}, 0)`))
svg.append('g')
;svg
.append('g')
.attr('class', 'x-axis')
::setStyles(X_AXIS_STYLE)
svg.append('g')
.attr('class', 'horizon-area')
svg.append('g').attr('class', 'horizon-area')
svg.append('text')
;svg
.append('text')
.attr('class', 'label')
::setStyles(LABEL_STYLE)
// Tooltip ---------------------------------------------
svg.append('rect')
;svg
.append('rect')
.attr('class', 'mouse-area')
.on('mousemove', this._handleMouseMove)
::setStyles(MOUSE_AREA_STYLE)
const hover = svg.append('g')
const hover = svg
.append('g')
.attr('class', 'hover-container')
::setStyles('pointer-events', 'none')
hover.append('line')
;hover
.append('line')
.attr('class', 'hover-line')
.attr('y1', 0)
::setStyles(HOVER_LINE_STYLE)
hover.append('text')
;hover
.append('text')
.attr('class', 'hover-text')
::setStyles(HOVER_TEXT_STYLE)
@@ -277,11 +289,14 @@ class XoWeekChart extends Component {
componentWillReceiveProps (nextProps) {
const { props } = this
if (!propsEqual(
props,
nextProps,
[ 'chartHeight', 'chartWidth', 'data', 'maxValue' ]
)) {
if (
!propsEqual(props, nextProps, [
'chartHeight',
'chartWidth',
'data',
'maxValue',
])
) {
this._draw(nextProps)
}
@@ -304,19 +319,19 @@ class XoWeekChart extends Component {
data: propTypes.arrayOf(
propTypes.shape({
date: propTypes.number.isRequired,
value: propTypes.number.isRequired
value: propTypes.number.isRequired,
})
).isRequired,
objectId: propTypes.string.isRequired
objectId: propTypes.string.isRequired,
})
).isRequired,
valueRenderer: propTypes.func
valueRenderer: propTypes.func,
})
export default class XoWeekCharts extends Component {
_handleResize = () => {
const { container } = this.refs
this.setState({
chartsWidth: container && container.offsetWidth
chartsWidth: container && container.offsetWidth,
})
}
@@ -337,7 +352,7 @@ export default class XoWeekCharts extends Component {
}
this.setState({
maxValue: max
maxValue: max,
})
}
@@ -348,7 +363,7 @@ export default class XoWeekCharts extends Component {
componentWillMount () {
this.setState({
tooltipX: 0
tooltipX: 0,
})
}
@@ -366,39 +381,29 @@ export default class XoWeekCharts extends Component {
}
render () {
const {
props,
state: {
chartsWidth,
maxValue,
tooltipX
}
} = this
const { props, state: { chartsWidth, maxValue, tooltipX } } = this
return (
<div>
<div>
<p className='mt-1'>
{_('weeklyChartsScaleInfo')}
{' '}
{_('weeklyChartsScaleInfo')}{' '}
<Toggle iconSize={1} icon='scale' onChange={this._updateScale} />
</p>
</div>
<div
ref='container'
className={styles.container}
>
{chartsWidth && map(props.series, (series, key) => (
<XoWeekChart
{...series}
chartWidth={chartsWidth}
key={key}
maxValue={maxValue}
onTooltipChange={this._handleTooltipChange}
tooltipX={tooltipX}
valueRenderer={props.valueRenderer}
/>
))}
<div ref='container' className={styles.container}>
{chartsWidth &&
map(props.series, (series, key) => (
<XoWeekChart
{...series}
chartWidth={chartsWidth}
key={key}
maxValue={maxValue}
onTooltipChange={this._handleTooltipChange}
tooltipX={tooltipX}
valueRenderer={props.valueRenderer}
/>
))}
</div>
</div>
)

View File

@@ -4,11 +4,7 @@ import map from 'lodash/map'
import moment from 'moment'
import sortBy from 'lodash/sortBy'
import times from 'lodash/times'
import {
extent,
interpolateViridis,
scaleSequential
} from 'd3'
import { extent, interpolateViridis, scaleSequential } from 'd3'
import { FormattedTime } from 'react-intl'
import _ from '../intl'
@@ -22,7 +18,7 @@ import styles from './index.css'
const DAY_TIME_FORMAT = {
day: 'numeric',
month: 'numeric'
month: 'numeric',
}
// ===================================================================
@@ -32,7 +28,7 @@ const computeColorGen = days => {
let max = Number.MIN_VALUE
forEach(days, day => {
const [ _min, _max ] = extent(day.hours, value => value && value.value)
const [_min, _max] = extent(day.hours, value => value && value.value)
if (_min < min) {
min = _min
@@ -62,12 +58,10 @@ const computeMissingDays = days => {
if (diff > 1) {
const missingDays = times(diff - 1, () => ({
hours,
timestamp: a.subtract(1, 'days').valueOf()
timestamp: a.subtract(1, 'days').valueOf(),
})).reverse()
correctedDays.splice.apply(
correctedDays, [i, 0].concat(missingDays)
)
correctedDays.splice.apply(correctedDays, [i, 0].concat(missingDays))
}
a = b
@@ -83,13 +77,13 @@ const computeMissingDays = days => {
data: propTypes.arrayOf(
propTypes.shape({
date: propTypes.number.isRequired,
value: propTypes.number.isRequired
value: propTypes.number.isRequired,
})
).isRequired
).isRequired,
})
export default class XoWeekHeatmap extends Component {
static defaultProps = {
cellRenderer: value => value
cellRenderer: value => value,
}
componentWillReceiveProps (nextProps) {
@@ -114,7 +108,7 @@ export default class XoWeekHeatmap extends Component {
if (!days[dayId]) {
days[dayId] = {
hours: new Array(24),
timestamp: elem.date
timestamp: elem.date,
}
}
@@ -124,7 +118,7 @@ export default class XoWeekHeatmap extends Component {
hours[hourId] = {
date,
nb: 1,
value
value,
}
} else {
const hour = hours[hourId]
@@ -146,9 +140,7 @@ export default class XoWeekHeatmap extends Component {
})
this.setState({
days: computeMissingDays(
sortBy(days, 'timestamp')
)
days: computeMissingDays(sortBy(days, 'timestamp')),
})
}
@@ -158,16 +150,26 @@ export default class XoWeekHeatmap extends Component {
<tbody>
<tr>
<th />
{times(24, hour => <th key={hour} className='text-xs-center'>{hour}</th>)}
{times(24, hour => (
<th key={hour} className='text-xs-center'>
{hour}
</th>
))}
</tr>
{map(this.state.days, (day, key) => (
<tr key={key}>
<th><FormattedTime value={day.timestamp} {...DAY_TIME_FORMAT} /></th>
<th>
<FormattedTime value={day.timestamp} {...DAY_TIME_FORMAT} />
</th>
{map(day.hours, (hour, key) => (
<Tooltip
content={hour
? _('weekHeatmapData', { date: hour.date, value: this.props.cellRenderer(hour.value) })
: _('weekHeatmapNoData')
content={
hour
? _('weekHeatmapData', {
date: hour.date,
value: this.props.cellRenderer(hour.value),
})
: _('weekHeatmapNoData')
}
key={key}
>

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